mirror of
https://github.com/certd/certd.git
synced 2026-08-02 02:44:49 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e0bb51ce | ||
|
|
1dc5a19c45 | ||
|
|
04aa9041e8 | ||
|
|
0d86c84b28 | ||
|
|
6605669113 | ||
|
|
4b8747b5da | ||
|
|
2ba4132c50 | ||
|
|
49dc2796cd | ||
|
|
1e07d69932 | ||
|
|
aabf73a736 | ||
|
|
c9be2293ab |
@@ -0,0 +1,133 @@
|
|||||||
|
```markdown
|
||||||
|
# certd Development Patterns
|
||||||
|
|
||||||
|
> Auto-generated skill from repository analysis
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
- **CamelCase** is used for file names.
|
||||||
|
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
|
||||||
|
|
||||||
|
### Import Style
|
||||||
|
|
||||||
|
- **Absolute imports** are preferred.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Style
|
||||||
|
|
||||||
|
- **Named exports** are used throughout the codebase.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
export function getUserPreferences() { ... }
|
||||||
|
export const PREFERENCE_KEYS = [ ... ];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Patterns
|
||||||
|
|
||||||
|
- **Conventional commits** are used, with the `feat` prefix for new features.
|
||||||
|
- Example:
|
||||||
|
```
|
||||||
|
feat: add account sync to preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Extend Preferences Feature
|
||||||
|
|
||||||
|
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
|
||||||
|
**Command:** `/extend-preferences`
|
||||||
|
|
||||||
|
Follow these steps to extend user preferences functionality:
|
||||||
|
|
||||||
|
1. **Update localization files**
|
||||||
|
Add or modify strings in:
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// en-US/preferences.ts
|
||||||
|
export default {
|
||||||
|
sync: "Sync Preferences",
|
||||||
|
import: "Import Preferences",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Modify or add Vue components for preferences UI**
|
||||||
|
Update or create components such as:
|
||||||
|
- `preferences-drawer.vue`
|
||||||
|
- `account-sync.ts`
|
||||||
|
```vue
|
||||||
|
<!-- preferences-drawer.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update or add supporting icon definitions**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
|
||||||
|
```typescript
|
||||||
|
export const SyncIcon = { /* icon definition */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Implement or update store logic for settings**
|
||||||
|
Update:
|
||||||
|
- `packages/ui/certd-client/src/store/settings/index.tsx`
|
||||||
|
```typescript
|
||||||
|
export function syncPreferencesToAccount() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add or update backend API/controller for user preferences**
|
||||||
|
Edit or add:
|
||||||
|
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// user-preferences.ts
|
||||||
|
export async function updateUserPreferences(req, res) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Write or update backend tests for new preference logic**
|
||||||
|
Add or update:
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
|
||||||
|
```typescript
|
||||||
|
test('should sync preferences', async () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Update backend models if necessary**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
|
||||||
|
```typescript
|
||||||
|
export interface UserPreferences { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
- **Test files** follow the `*.test.*` naming convention.
|
||||||
|
- Example: `user-preferences.test.ts`
|
||||||
|
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
|
||||||
|
- **Test Example:**
|
||||||
|
```typescript
|
||||||
|
test('should update preferences', async () => {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|----------------------|--------------------------------------------------------------|
|
||||||
|
| /extend-preferences | Guide to extend or enhance user preference functionality |
|
||||||
|
```
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Certd"
|
||||||
|
short_description: "Repo-specific patterns and workflows for certd"
|
||||||
|
default_prompt: "Use the certd repo skill to follow existing architecture, testing, and workflow conventions."
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
{
|
||||||
|
"version": "1.3",
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"generatedBy": "ecc-tools",
|
||||||
|
"generatedAt": "2026-07-30T02:07:44.046Z",
|
||||||
|
"repo": "https://github.com/certd/certd",
|
||||||
|
"referenceSetReadiness": {
|
||||||
|
"score": 0,
|
||||||
|
"present": 0,
|
||||||
|
"total": 7,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "deep-analyzer-corpus",
|
||||||
|
"label": "Deep analyzer corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rag-evaluator",
|
||||||
|
"label": "RAG/evaluator comparison",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pr-salvage",
|
||||||
|
"label": "PR salvage/review corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "discussion-triage",
|
||||||
|
"label": "Discussion triage corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "harness-compatibility",
|
||||||
|
"label": "Harness compatibility",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "security-evidence",
|
||||||
|
"label": "Security evidence",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ci-failure-mode",
|
||||||
|
"label": "CI failure-mode evidence",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"requested": "core",
|
||||||
|
"recommended": "core",
|
||||||
|
"effective": "core",
|
||||||
|
"requestedAlias": "core",
|
||||||
|
"recommendedAlias": "core",
|
||||||
|
"effectiveAlias": "core"
|
||||||
|
},
|
||||||
|
"requestedProfile": "core",
|
||||||
|
"profile": "core",
|
||||||
|
"recommendedProfile": "core",
|
||||||
|
"effectiveProfile": "core",
|
||||||
|
"tier": "free",
|
||||||
|
"requestedComponents": [
|
||||||
|
"repo-baseline"
|
||||||
|
],
|
||||||
|
"selectedComponents": [
|
||||||
|
"repo-baseline"
|
||||||
|
],
|
||||||
|
"requestedAddComponents": [],
|
||||||
|
"requestedRemoveComponents": [],
|
||||||
|
"blockedRemovalComponents": [],
|
||||||
|
"tierFilteredComponents": [],
|
||||||
|
"requestedRootPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"selectedRootPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedAddPackages": [],
|
||||||
|
"requestedRemovePackages": [],
|
||||||
|
"selectedPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"packages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"blockedRemovalPackages": [],
|
||||||
|
"tierFilteredRootPackages": [],
|
||||||
|
"tierFilteredPackages": [],
|
||||||
|
"conflictingPackages": [],
|
||||||
|
"dependencyGraph": {
|
||||||
|
"runtime-core": []
|
||||||
|
},
|
||||||
|
"resolutionOrder": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedModules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"selectedModules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"modules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"managedFiles": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
],
|
||||||
|
"packageFiles": {
|
||||||
|
"runtime-core": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"moduleFiles": {
|
||||||
|
"runtime-core": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/skills/certd/SKILL.md",
|
||||||
|
"description": "Repository-specific Claude Code skill generated from git history."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".agents/skills/certd/SKILL.md",
|
||||||
|
"description": "Codex-facing copy of the generated repository skill."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".agents/skills/certd/agents/openai.yaml",
|
||||||
|
"description": "Codex skill metadata so the repo skill appears cleanly in the skill interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/identity.json",
|
||||||
|
"description": "Suggested identity.json baseline derived from repository conventions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/config.toml",
|
||||||
|
"description": "Repo-local Codex MCP and multi-agent baseline aligned with ECC defaults."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/AGENTS.md",
|
||||||
|
"description": "Codex usage guide that points at the generated repo skill and workflow bundle."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/explorer.toml",
|
||||||
|
"description": "Read-only explorer role config for Codex multi-agent work."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/reviewer.toml",
|
||||||
|
"description": "Read-only reviewer role config focused on correctness and security."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/docs-researcher.toml",
|
||||||
|
"description": "Read-only docs researcher role config for API verification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/homunculus/instincts/inherited/certd-instincts.yaml",
|
||||||
|
"description": "Continuous-learning instincts derived from repository patterns."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workflows": [],
|
||||||
|
"adapters": {
|
||||||
|
"claudeCode": {
|
||||||
|
"skillPath": ".claude/skills/certd/SKILL.md",
|
||||||
|
"identityPath": ".claude/identity.json",
|
||||||
|
"commandPaths": []
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"configPath": ".codex/config.toml",
|
||||||
|
"agentsGuidePath": ".codex/AGENTS.md",
|
||||||
|
"skillPath": ".agents/skills/certd/SKILL.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# Instincts generated from https://github.com/certd/certd
|
||||||
|
# Generated: 2026-07-30T02:08:05.402Z
|
||||||
|
# Version: 2.0
|
||||||
|
# NOTE: This file supplements (does not replace) any existing curated instincts.
|
||||||
|
# High-confidence manually curated instincts should be preserved alongside these.
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-commit-conventional
|
||||||
|
trigger: "when writing a commit message"
|
||||||
|
confidence: 0.85
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Commit Conventional
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use conventional commit format with prefixes: feat
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- 2 commits analyzed
|
||||||
|
- Detected conventional commit pattern
|
||||||
|
- Examples: feat: 偏好设置支持从剪切板导入, feat: 偏好设置支持保存到账号并在登录后自动同步
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-commit-length
|
||||||
|
trigger: "when writing a commit message"
|
||||||
|
confidence: 0.6
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Commit Length
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Keep commit messages concise (~22 characters)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Average commit message length: 22 chars
|
||||||
|
- Based on 2 commits
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-naming-files
|
||||||
|
trigger: "when creating a new file"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Naming Files
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use camelCase naming convention
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Analyzed file naming patterns in repository
|
||||||
|
- Dominant pattern: camelCase
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-export-style
|
||||||
|
trigger: "when exporting from a module"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Export Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Prefer named exports
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Export pattern analysis
|
||||||
|
- Dominant style: named
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-arch-feature-based
|
||||||
|
trigger: "when adding a new feature"
|
||||||
|
confidence: 0.85
|
||||||
|
domain: architecture
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Arch Feature Based
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Create a new folder in src/features/ with all related code colocated
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Feature-based module organization detected
|
||||||
|
- Structure: src/features/[feature-name]/
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-workflow-extend-preferences-feature
|
||||||
|
trigger: "when doing extend preferences feature"
|
||||||
|
confidence: 0.6
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Workflow Extend Preferences Feature
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Follow the extend-preferences-feature workflow:
|
||||||
|
1. Update localization files for new preference-related strings
|
||||||
|
2. Modify or add Vue components for preferences UI
|
||||||
|
3. Update or add supporting icon definitions
|
||||||
|
4. Implement or update store logic for settings
|
||||||
|
5. Add or update backend API/controller for user preferences
|
||||||
|
6. Write or update backend tests for new preference logic
|
||||||
|
7. Update backend models if necessary
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow detected from commit patterns
|
||||||
|
- Frequency: ~2x per month
|
||||||
|
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts, packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-file-naming
|
||||||
|
trigger: "When creating a new file in the codebase"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct File Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Name the file using camelCase
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.files
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-function-naming
|
||||||
|
trigger: "When defining a new function"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Function Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use camelCase for function names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.functions
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-class-naming
|
||||||
|
trigger: "When defining a new class"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Class Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use PascalCase for class names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.classes
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-constant-naming
|
||||||
|
trigger: "When declaring a constant"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Constant Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use SCREAMING_SNAKE_CASE for constant names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.constants
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-import-style
|
||||||
|
trigger: "When importing modules"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Import Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use absolute import paths
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.importStyle
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-export-style
|
||||||
|
trigger: "When exporting from a module"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Export Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use named exports
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.exportStyle
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-try-catch
|
||||||
|
trigger: "When handling errors in code"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Try Catch
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use try-catch blocks for error handling
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in errorHandling.style
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-test-location
|
||||||
|
trigger: "When adding or updating tests"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: testing
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Test Location
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Place tests in both source and test-specific folders as appropriate (mixed location)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in architecture.folderStructure.testLocation
|
||||||
|
- Seen in files like packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-backend-test-pattern
|
||||||
|
trigger: "When adding backend logic for user preferences"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: testing
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Backend Test Pattern
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Create or update a corresponding .test.ts file in the same directory
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Seen in packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-conventional-commits
|
||||||
|
trigger: "When writing a commit message"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Conventional Commits
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use the conventional commit format with a type prefix (e.g., feat: ...)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in commits.type
|
||||||
|
- Examples: feat: 偏好设置支持从剪切板导入
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-commit-length
|
||||||
|
trigger: "When writing a commit message"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Commit Length
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Keep the commit message concise, around 22 characters on average
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in commits.averageLength
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-localization
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Localization
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update localization files for new preference-related strings
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-ui
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Ui
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Modify or add Vue components for the preferences UI
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-icons
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Icons
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update or add supporting icon definitions
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-client/src/vben/icons/lucide.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-store
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Store
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Implement or update store logic for settings
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-client/src/store/settings/index.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-backend-api
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Backend Api
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Add or update backend API/controller for user preferences
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-server/src/controller/user/mine/user-preferences.ts, packages/ui/certd-server/src/controller/user/mine/user-settings-controller.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-backend-test
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Backend Test
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Write or update backend tests for new preference logic
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-models
|
||||||
|
trigger: "When extending or adding a user preference feature and new data is needed"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Models
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update backend models if necessary
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-server/src/modules/mine/service/models.ts
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0",
|
||||||
|
"technicalLevel": "technical",
|
||||||
|
"preferredStyle": {
|
||||||
|
"verbosity": "detailed",
|
||||||
|
"codeComments": true,
|
||||||
|
"explanations": true
|
||||||
|
},
|
||||||
|
"domains": [
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"suggestedBy": "ecc-tools-repo-analysis",
|
||||||
|
"createdAt": "2026-07-30T02:08:05.402Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
```markdown
|
||||||
|
# certd Development Patterns
|
||||||
|
|
||||||
|
> Auto-generated skill from repository analysis
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
- **CamelCase** is used for file names.
|
||||||
|
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
|
||||||
|
|
||||||
|
### Import Style
|
||||||
|
|
||||||
|
- **Absolute imports** are preferred.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Style
|
||||||
|
|
||||||
|
- **Named exports** are used throughout the codebase.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
export function getUserPreferences() { ... }
|
||||||
|
export const PREFERENCE_KEYS = [ ... ];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Patterns
|
||||||
|
|
||||||
|
- **Conventional commits** are used, with the `feat` prefix for new features.
|
||||||
|
- Example:
|
||||||
|
```
|
||||||
|
feat: add account sync to preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Extend Preferences Feature
|
||||||
|
|
||||||
|
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
|
||||||
|
**Command:** `/extend-preferences`
|
||||||
|
|
||||||
|
Follow these steps to extend user preferences functionality:
|
||||||
|
|
||||||
|
1. **Update localization files**
|
||||||
|
Add or modify strings in:
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// en-US/preferences.ts
|
||||||
|
export default {
|
||||||
|
sync: "Sync Preferences",
|
||||||
|
import: "Import Preferences",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Modify or add Vue components for preferences UI**
|
||||||
|
Update or create components such as:
|
||||||
|
- `preferences-drawer.vue`
|
||||||
|
- `account-sync.ts`
|
||||||
|
```vue
|
||||||
|
<!-- preferences-drawer.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update or add supporting icon definitions**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
|
||||||
|
```typescript
|
||||||
|
export const SyncIcon = { /* icon definition */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Implement or update store logic for settings**
|
||||||
|
Update:
|
||||||
|
- `packages/ui/certd-client/src/store/settings/index.tsx`
|
||||||
|
```typescript
|
||||||
|
export function syncPreferencesToAccount() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add or update backend API/controller for user preferences**
|
||||||
|
Edit or add:
|
||||||
|
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// user-preferences.ts
|
||||||
|
export async function updateUserPreferences(req, res) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Write or update backend tests for new preference logic**
|
||||||
|
Add or update:
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
|
||||||
|
```typescript
|
||||||
|
test('should sync preferences', async () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Update backend models if necessary**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
|
||||||
|
```typescript
|
||||||
|
export interface UserPreferences { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
- **Test files** follow the `*.test.*` naming convention.
|
||||||
|
- Example: `user-preferences.test.ts`
|
||||||
|
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
|
||||||
|
- **Test Example:**
|
||||||
|
```typescript
|
||||||
|
test('should update preferences', async () => {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|----------------------|--------------------------------------------------------------|
|
||||||
|
| /extend-preferences | Guide to extend or enhance user preference functionality |
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ECC for Codex CLI
|
||||||
|
|
||||||
|
This supplements the root `AGENTS.md` with a repo-local ECC baseline.
|
||||||
|
|
||||||
|
## Repo Skill
|
||||||
|
|
||||||
|
- Repo-generated Codex skill: `.agents/skills/certd/SKILL.md`
|
||||||
|
- Claude-facing companion skill: `.claude/skills/certd/SKILL.md`
|
||||||
|
- Keep user-specific credentials and private MCPs in `~/.codex/config.toml`, not in this repo.
|
||||||
|
|
||||||
|
## MCP Baseline
|
||||||
|
|
||||||
|
Treat `.codex/config.toml` as the default ECC-safe baseline for work in this repository.
|
||||||
|
The generated baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking.
|
||||||
|
|
||||||
|
## Multi-Agent Support
|
||||||
|
|
||||||
|
- Explorer: read-only evidence gathering
|
||||||
|
- Reviewer: correctness, security, and regression review
|
||||||
|
- Docs researcher: API and release-note verification
|
||||||
|
|
||||||
|
## Workflow Files
|
||||||
|
|
||||||
|
- No dedicated workflow command files were generated for this repo.
|
||||||
|
|
||||||
|
Use these workflow files as reusable task scaffolds when the detected repository workflows recur.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "medium"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Verify APIs, framework behavior, and release-note claims against primary documentation before changes land.
|
||||||
|
Cite the exact docs or file paths that support each claim.
|
||||||
|
Do not invent undocumented behavior.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "medium"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Stay in exploration mode.
|
||||||
|
Trace the real execution path, cite files and symbols, and avoid proposing fixes unless the parent agent asks for them.
|
||||||
|
Prefer targeted search and file reads over broad scans.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "high"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Review like an owner.
|
||||||
|
Prioritize correctness, security, behavioral regressions, and missing tests.
|
||||||
|
Lead with concrete findings and avoid style-only feedback unless it hides a real bug.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#:schema https://developers.openai.com/codex/config-schema.json
|
||||||
|
|
||||||
|
# ECC Tools generated Codex baseline
|
||||||
|
approval_policy = "on-request"
|
||||||
|
sandbox_mode = "workspace-write"
|
||||||
|
web_search = "live"
|
||||||
|
|
||||||
|
[mcp_servers.github]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||||
|
|
||||||
|
[mcp_servers.context7]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@upstash/context7-mcp@latest"]
|
||||||
|
|
||||||
|
[mcp_servers.exa]
|
||||||
|
url = "https://mcp.exa.ai/mcp"
|
||||||
|
|
||||||
|
[mcp_servers.memory]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-memory"]
|
||||||
|
|
||||||
|
[mcp_servers.playwright]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@playwright/mcp@latest", "--extension"]
|
||||||
|
|
||||||
|
[mcp_servers.sequential-thinking]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
multi_agent = true
|
||||||
|
|
||||||
|
[agents]
|
||||||
|
max_threads = 6
|
||||||
|
max_depth = 1
|
||||||
|
|
||||||
|
[agents.explorer]
|
||||||
|
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
|
||||||
|
config_file = "agents/explorer.toml"
|
||||||
|
|
||||||
|
[agents.reviewer]
|
||||||
|
description = "PR reviewer focused on correctness, security, and missing tests."
|
||||||
|
config_file = "agents/reviewer.toml"
|
||||||
|
|
||||||
|
[agents.docs_researcher]
|
||||||
|
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
|
||||||
|
config_file = "agents/docs-researcher.toml"
|
||||||
@@ -105,10 +105,8 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
|
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
|
||||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
|
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
|
||||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:slim
|
greper/certd:slim
|
||||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim
|
greper/certd:${{steps.get_certd_version.outputs.result}}-slim
|
||||||
certd/certd:slim
|
|
||||||
certd/certd:${{steps.get_certd_version.outputs.result}}-slim
|
|
||||||
ghcr.io/${{ github.repository }}:slim
|
ghcr.io/${{ github.repository }}:slim
|
||||||
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
|
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
|
||||||
|
|
||||||
@@ -121,10 +119,8 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7
|
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7
|
||||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7
|
greper/certd:armv7
|
||||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||||
certd/certd:armv7
|
|
||||||
certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
|
||||||
ghcr.io/${{ github.repository }}:armv7
|
ghcr.io/${{ github.repository }}:armv7
|
||||||
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
|
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
name: stable-release
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "版本号(如 v1.42.5)"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
make-stable:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to aliyun container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: registry.cn-shenzhen.aliyuncs.com
|
|
||||||
username: ${{ secrets.aliyun_cs_username }}
|
|
||||||
password: ${{ secrets.aliyun_cs_password }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Packages
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.dockerhub_username }}
|
|
||||||
password: ${{ secrets.dockerhub_password }}
|
|
||||||
|
|
||||||
# stable 镜像:叠加 ENV 层
|
|
||||||
- name: Build and push stable images
|
|
||||||
run: |
|
|
||||||
echo "FROM greper/certd:${{ inputs.version }}
|
|
||||||
ENV certd_release_mode=stable" > Dockerfile.stable
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--push \
|
|
||||||
-f Dockerfile.stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-stable \
|
|
||||||
-t certd/certd:stable \
|
|
||||||
-t certd/certd:${{ inputs.version }}-stable \
|
|
||||||
-t ghcr.io/${{ github.repository }}:stable \
|
|
||||||
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-stable \
|
|
||||||
.
|
|
||||||
|
|
||||||
# slim-stable 镜像
|
|
||||||
- name: Build and push slim-stable images
|
|
||||||
run: |
|
|
||||||
echo "FROM greper/certd:${{ inputs.version }}-slim
|
|
||||||
ENV certd_release_mode=stable" > Dockerfile.slim-stable
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--push \
|
|
||||||
-f Dockerfile.slim-stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-slim-stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
|
|
||||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-slim-stable \
|
|
||||||
-t certd/certd:slim-stable \
|
|
||||||
-t certd/certd:${{ inputs.version }}-slim-stable \
|
|
||||||
-t ghcr.io/${{ github.repository }}:slim-stable \
|
|
||||||
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-slim-stable \
|
|
||||||
.
|
|
||||||
|
|
||||||
- name: Set AtomGit release as stable
|
|
||||||
run: |
|
|
||||||
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
|
|
||||||
export VERSION=${{ inputs.version }}
|
|
||||||
npm run set-release-stable
|
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
# 插件依赖按需加载方案
|
||||||
|
|
||||||
|
## 背景与目标
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
- `packages/ui/certd-server/node_modules` 包含 50+ 个插件的所有依赖,体积庞大
|
||||||
|
- 大量云厂商 SDK(AWS、阿里云、腾讯云、华为云等)只在特定插件中使用
|
||||||
|
- 用户通常只使用少数几个插件,但必须安装所有依赖
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
实现依赖的按需下载和加载:
|
||||||
|
1. 插件依赖独立管理,不占用主 `node_modules` 空间
|
||||||
|
2. 只有当用户首次使用某插件时,才动态下载该插件需要的依赖
|
||||||
|
3. 依赖安装完成后,通过 `await import()` 从独立路径加载
|
||||||
|
4. 保持现有插件代码的最小改动
|
||||||
|
|
||||||
|
## 当前架构分析
|
||||||
|
|
||||||
|
### 插件加载机制
|
||||||
|
- 插件位于 `packages/ui/certd-server/src/plugins/` 下(50+ 个插件目录)
|
||||||
|
- `AutoLoadPlugins` 类在启动时扫描 `dist/plugins` 目录并动态导入
|
||||||
|
- 插件注册到不同的 registry:`accessRegistry`, `pluginRegistry`, `dnsProviderRegistry` 等
|
||||||
|
- 插件代码已经使用 `await import()` 进行懒加载(如 `await import("@aws-sdk/client-acm")`)
|
||||||
|
|
||||||
|
### 重型依赖分布
|
||||||
|
从 `packages/ui/certd-server/package.json` 分析,以下依赖体积大且仅特定插件使用:
|
||||||
|
|
||||||
|
**云厂商 SDK(按插件分组):**
|
||||||
|
- **AWS 插件**:`@aws-sdk/client-acm`, `@aws-sdk/client-cloudfront`, `@aws-sdk/client-iam`, `@aws-sdk/client-route-53`, `@aws-sdk/client-s3`, `@aws-sdk/client-sts`
|
||||||
|
- **阿里云插件**:`@alicloud/openapi-client`, `@alicloud/pop-core`, `@alicloud/tea-typescript`, `@alicloud/fc20230330` 等
|
||||||
|
- **腾讯云插件**:`tencentcloud-sdk-nodejs`, `cos-nodejs-sdk-v5`
|
||||||
|
- **华为云插件**:`@huaweicloud/huaweicloud-sdk-cdn`, `@huaweicloud/huaweicloud-sdk-core` 等
|
||||||
|
- **Azure 插件**:`@azure/arm-dns`, `@azure/identity`
|
||||||
|
- **Google Cloud 插件**:`@google-cloud/dns`, `@google-cloud/publicca`
|
||||||
|
- **火山引擎插件**:`@volcengine/openapi`, `@volcengine/tos-sdk`
|
||||||
|
|
||||||
|
**网络/工具库:**
|
||||||
|
- `ssh2`, `socks`, `socks-proxy-agent`(SSH 相关插件)
|
||||||
|
- `ali-oss`, `qiniu`, `basic-ftp`(存储/传输插件)
|
||||||
|
- `nodemailer`(邮件通知插件)
|
||||||
|
|
||||||
|
**通用依赖(保留在主 package.json):**
|
||||||
|
- `@midwayjs/*` 系列(框架核心)
|
||||||
|
- `@certd/*` 系列(项目内部包)
|
||||||
|
- `axios`, `lodash-es`, `dayjs`, `js-yaml` 等基础工具
|
||||||
|
|
||||||
|
## 设计方案
|
||||||
|
|
||||||
|
### 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/ui/certd-server/
|
||||||
|
├── package.json # 主依赖(框架、通用工具)
|
||||||
|
├── node_modules/ # 主依赖安装目录
|
||||||
|
├── optional-deps/ # 新增:可选依赖管理目录
|
||||||
|
│ ├── package.json # 可选依赖总配置(用于 pnpm install)
|
||||||
|
│ ├── pnpm-lock.yaml # 可选依赖锁文件
|
||||||
|
│ └── node_modules/ # 可选依赖安装目录
|
||||||
|
├── src/
|
||||||
|
│ └── modules/
|
||||||
|
│ └── dependency/ # 新增:依赖管理模块
|
||||||
|
│ ├── dependency-manager.ts # 核心:依赖管理器
|
||||||
|
│ ├── dependency-registry.ts # 依赖注册表(插件 -> 依赖映射)
|
||||||
|
│ └── types.ts # 类型定义
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
#### 1. 依赖管理器(DependencyManager)
|
||||||
|
|
||||||
|
**职责:**
|
||||||
|
- 检查依赖是否已安装
|
||||||
|
- 动态执行 `pnpm install` 安装缺失依赖
|
||||||
|
- 提供从 `optional-deps/node_modules` 加载依赖的方法
|
||||||
|
- 并发控制:避免多个插件同时触发安装
|
||||||
|
|
||||||
|
**关键方法:**
|
||||||
|
```typescript
|
||||||
|
class DependencyManager {
|
||||||
|
// 确保依赖已安装,返回依赖模块
|
||||||
|
async ensureAndImport<T>(packageName: string): Promise<T>
|
||||||
|
|
||||||
|
// 检查依赖是否已安装
|
||||||
|
async isInstalled(packageName: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 安装依赖(带锁,避免并发)
|
||||||
|
async installDependencies(packages: string[]): Promise<void>
|
||||||
|
|
||||||
|
// 从 optional-deps/node_modules 加载依赖
|
||||||
|
async loadModule<T>(packageName: string): Promise<T>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现要点:**
|
||||||
|
- 使用文件锁(如 `proper-lockfile`)防止并发安装
|
||||||
|
- 安装前检查 `optional-deps/node_modules/{packageName}` 是否存在
|
||||||
|
- 安装命令:`pnpm install --dir optional-deps --ignore-workspace`
|
||||||
|
- 加载时使用绝对路径:`import('file:///absolute/path/to/optional-deps/node_modules/package')`
|
||||||
|
|
||||||
|
#### 2. 依赖注册表(DependencyRegistry)
|
||||||
|
|
||||||
|
**职责:**
|
||||||
|
- 维护插件名称到依赖列表的映射
|
||||||
|
- 提供依赖查询接口
|
||||||
|
|
||||||
|
**数据结构:**
|
||||||
|
```typescript
|
||||||
|
interface PluginDependencyConfig {
|
||||||
|
pluginName: string;
|
||||||
|
dependencies: {
|
||||||
|
packageName: string;
|
||||||
|
version: string;
|
||||||
|
optional?: boolean; // 是否可选(安装失败不阻塞)
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例注册
|
||||||
|
dependencyRegistry.register('plugin-aws', [
|
||||||
|
{ packageName: '@aws-sdk/client-acm', version: '^3.964.0' },
|
||||||
|
{ packageName: '@aws-sdk/client-cloudfront', version: '^3.964.0' },
|
||||||
|
{ packageName: '@aws-sdk/client-route-53', version: '^3.964.0' },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 插件集成
|
||||||
|
|
||||||
|
**改造现有插件代码:**
|
||||||
|
|
||||||
|
改造前(`plugin-aws/libs/aws-client.ts`):
|
||||||
|
```typescript
|
||||||
|
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
改造后:
|
||||||
|
```typescript
|
||||||
|
import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";
|
||||||
|
|
||||||
|
const depManager = new DependencyManager();
|
||||||
|
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
**简化方案(推荐):**
|
||||||
|
|
||||||
|
创建辅助函数,减少改动量:
|
||||||
|
```typescript
|
||||||
|
// src/modules/dependency/import-helper.ts
|
||||||
|
export async function importOptionalDep<T>(packageName: string): Promise<T> {
|
||||||
|
const depManager = new DependencyManager();
|
||||||
|
return await depManager.ensureAndImport<T>(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件中使用
|
||||||
|
import { importOptionalDep } from "../../../modules/dependency/import-helper.js";
|
||||||
|
const { ACMClient } = await importOptionalDep("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实施步骤
|
||||||
|
|
||||||
|
#### 阶段一:基础设施搭建
|
||||||
|
1. 创建 `optional-deps/` 目录结构
|
||||||
|
2. 生成 `optional-deps/package.json`(包含所有可选依赖)
|
||||||
|
3. 实现 `DependencyManager` 核心逻辑
|
||||||
|
4. 实现依赖安装锁机制
|
||||||
|
5. 编写单元测试
|
||||||
|
|
||||||
|
#### 阶段二:依赖迁移
|
||||||
|
6. 从主 `package.json` 移除可选依赖
|
||||||
|
7. 将依赖添加到 `optional-deps/package.json`
|
||||||
|
8. 创建依赖注册表,映射插件到依赖
|
||||||
|
|
||||||
|
#### 阶段三:插件改造
|
||||||
|
9. 创建 `import-helper.ts` 辅助函数
|
||||||
|
10. 逐步改造插件代码,使用 `importOptionalDep` 加载依赖
|
||||||
|
11. 优先改造重型依赖(AWS、阿里云、腾讯云等)
|
||||||
|
|
||||||
|
#### 阶段四:测试与优化
|
||||||
|
12. 端到端测试:验证依赖按需安装和加载
|
||||||
|
13. 性能优化:缓存已加载的模块
|
||||||
|
14. 错误处理:安装失败时的降级策略
|
||||||
|
15. 文档:编写使用说明和迁移指南
|
||||||
|
|
||||||
|
## 关键技术决策
|
||||||
|
|
||||||
|
### 1. 依赖分组策略
|
||||||
|
**选择:按插件分组**
|
||||||
|
- 每个插件声明自己需要的依赖
|
||||||
|
- 优点:职责清晰,易于维护
|
||||||
|
- 缺点:可能有重复依赖(但 pnpm 会去重)
|
||||||
|
|
||||||
|
**备选:按功能分组**
|
||||||
|
- 将依赖按功能分组(如 "aws-deps", "aliyun-deps")
|
||||||
|
- 优点:更细粒度控制
|
||||||
|
- 缺点:增加复杂度
|
||||||
|
|
||||||
|
### 2. 安装触发时机
|
||||||
|
**选择:首次使用时触发**
|
||||||
|
- 在插件的 `execute()` 或 `getClient()` 方法中触发安装
|
||||||
|
- 优点:真正的按需加载
|
||||||
|
- 缺点:首次使用有延迟
|
||||||
|
|
||||||
|
**备选:启动时预检查**
|
||||||
|
- 启动时扫描启用的插件,预安装依赖
|
||||||
|
- 优点:避免运行时延迟
|
||||||
|
- 缺点:可能安装不需要的依赖
|
||||||
|
|
||||||
|
### 3. 依赖路径解析
|
||||||
|
**选择:使用绝对路径 + `file://` 协议**
|
||||||
|
```typescript
|
||||||
|
const modulePath = path.resolve(__dirname, '../../optional-deps/node_modules', packageName);
|
||||||
|
return await import(`file://${modulePath}/index.js`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
- Node.js ESM 要求明确的 URL 格式
|
||||||
|
- 避免模块解析冲突
|
||||||
|
|
||||||
|
### 4. 并发控制
|
||||||
|
**选择:文件锁 + 内存锁双重保护**
|
||||||
|
- 使用 `proper-lockfile` 锁定 `optional-deps/` 目录
|
||||||
|
- 内存中使用 `Map` 记录正在安装的依赖
|
||||||
|
- 避免多个插件同时触发安装
|
||||||
|
|
||||||
|
### 5. 错误处理
|
||||||
|
**策略:**
|
||||||
|
- 安装失败时记录日志,抛出明确的错误信息
|
||||||
|
- 提供手动安装命令提示:`请运行: cd optional-deps && pnpm install`
|
||||||
|
- 支持降级:某些非核心依赖安装失败时,插件可以部分功能可用
|
||||||
|
|
||||||
|
## 验证方案
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
1. 测试 `DependencyManager.isInstalled()` 正确检测依赖状态
|
||||||
|
2. 测试 `DependencyManager.installDependencies()` 成功安装依赖
|
||||||
|
3. 测试并发安装时的锁机制
|
||||||
|
4. 测试从 `optional-deps/node_modules` 加载模块
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
1. 清空 `optional-deps/node_modules`
|
||||||
|
2. 启动服务,验证不触发安装
|
||||||
|
3. 调用 AWS 插件,验证触发安装并成功加载
|
||||||
|
4. 再次调用,验证不重复安装
|
||||||
|
5. 验证主 `node_modules` 体积减少
|
||||||
|
|
||||||
|
### 性能测试
|
||||||
|
1. 测量首次安装依赖的耗时
|
||||||
|
2. 测量后续加载的耗时(应该与正常 import 相近)
|
||||||
|
3. 对比改造前后的 `node_modules` 大小
|
||||||
|
|
||||||
|
## 风险与挑战
|
||||||
|
|
||||||
|
### 1. 首次使用延迟
|
||||||
|
**风险:** 用户首次使用插件时需要等待依赖安装(可能几十秒)
|
||||||
|
**缓解:**
|
||||||
|
- 在 UI 上显示安装进度
|
||||||
|
- 提供预安装命令:`pnpm run install-optional-deps`
|
||||||
|
- 文档说明首次使用会有延迟
|
||||||
|
|
||||||
|
### 2. 离线环境
|
||||||
|
**风险:** 离线环境无法下载依赖
|
||||||
|
**缓解:**
|
||||||
|
- 提供完整安装包(包含所有可选依赖)
|
||||||
|
- 支持手动复制 `node_modules`
|
||||||
|
|
||||||
|
### 3. 版本冲突
|
||||||
|
**风险:** 可选依赖与主依赖版本冲突
|
||||||
|
**缓解:**
|
||||||
|
- 使用 `--ignore-workspace` 隔离安装
|
||||||
|
- 定期同步主依赖版本
|
||||||
|
|
||||||
|
### 4. TypeScript 类型
|
||||||
|
**风险:** 动态导入的类型推断
|
||||||
|
**缓解:**
|
||||||
|
- 保留 `@types/*` 在主 `devDependencies`
|
||||||
|
- 使用泛型和类型断言
|
||||||
|
|
||||||
|
## 预期收益
|
||||||
|
|
||||||
|
1. **空间节省:** 主 `node_modules` 体积减少 60-70%(估算)
|
||||||
|
2. **安装速度:** 初始 `pnpm install` 速度提升 3-5 倍
|
||||||
|
3. **用户体验:** 不使用的插件不占用空间,按需加载
|
||||||
|
4. **维护性:** 依赖分组清晰,易于管理
|
||||||
|
|
||||||
|
## 后续优化
|
||||||
|
|
||||||
|
1. **依赖预热:** 在后台预安装常用插件依赖
|
||||||
|
2. **依赖缓存:** 支持从 CDN 或本地缓存安装
|
||||||
|
3. **依赖更新:** 提供命令批量更新可选依赖
|
||||||
|
4. **插件市场:** 支持从远程下载插件及其依赖配置
|
||||||
|
|
||||||
|
## 附录:依赖分类清单
|
||||||
|
|
||||||
|
### 可选依赖(迁移到 optional-deps/package.json)
|
||||||
|
|
||||||
|
**AWS 相关(plugin-aws, plugin-aws-cn):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@aws-sdk/client-acm": "^3.964.0",
|
||||||
|
"@aws-sdk/client-cloudfront": "^3.964.0",
|
||||||
|
"@aws-sdk/client-iam": "^3.964.0",
|
||||||
|
"@aws-sdk/client-route-53": "^3.964.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@aws-sdk/client-sts": "^3.990.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**阿里云相关(plugin-aliyun, plugin-lib/aliyun):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@alicloud/fc20230330": "^4.1.7",
|
||||||
|
"@alicloud/openapi-client": "^0.4.12",
|
||||||
|
"@alicloud/openapi-util": "^0.3.2",
|
||||||
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
|
"@alicloud/sts-sdk": "^1.0.2",
|
||||||
|
"@alicloud/tea-typescript": "^1.8.0",
|
||||||
|
"@alicloud/tea-util": "^1.4.10",
|
||||||
|
"ali-oss": "^6.21.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**腾讯云相关(plugin-tencent, plugin-lib/tencent):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
"cos-nodejs-sdk-v5": "^2.14.6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**华为云相关(plugin-huawei):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-core": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
|
||||||
|
"esdk-obs-nodejs": "^3.25.6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Azure 相关(plugin-azure):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@azure/arm-dns": "^5.1.0",
|
||||||
|
"@azure/identity": "^4.13.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Google Cloud 相关(plugin-google, plugin-cert/google):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@google-cloud/dns": "^5.3.1",
|
||||||
|
"@google-cloud/publicca": "^1.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**火山引擎相关(plugin-volcengine):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@volcengine/openapi": "^1.28.1",
|
||||||
|
"@volcengine/tos-sdk": "^2.9.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSH/网络相关(plugin-host, plugin-lib/ssh):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ssh2": "^1.17.0",
|
||||||
|
"socks": "^2.8.3",
|
||||||
|
"socks-proxy-agent": "^8.0.4",
|
||||||
|
"basic-ftp": "^5.0.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**其他存储/传输(plugin-qiniu, plugin-lib/qiniu):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qiniu": "^7.12.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**邮件通知(plugin-notification/email):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodemailer": "^6.9.16"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 主依赖(保留在主 package.json)
|
||||||
|
|
||||||
|
**框架核心:**
|
||||||
|
- `@midwayjs/*` 系列
|
||||||
|
- `@koa/cors`
|
||||||
|
- `typeorm`, `better-sqlite3`, `mysql2`, `pg`
|
||||||
|
|
||||||
|
**项目内部包:**
|
||||||
|
- `@certd/*` 系列
|
||||||
|
|
||||||
|
**通用工具:**
|
||||||
|
- `axios`, `lodash-es`, `dayjs`, `js-yaml`
|
||||||
|
- `crypto-js`, `jsonwebtoken`, `bcryptjs`
|
||||||
|
- `reflect-metadata`, `uuid`, `nanoid`
|
||||||
|
- 等等
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本方案通过引入独立的可选依赖管理机制,实现了插件依赖的按需下载和加载。核心思路是:
|
||||||
|
|
||||||
|
1. **隔离管理:** 在 `optional-deps/` 目录下维护独立的 `package.json` 和 `node_modules`
|
||||||
|
2. **动态安装:** 通过 `DependencyManager` 在首次使用时触发 `pnpm install`
|
||||||
|
3. **路径加载:** 使用绝对路径从独立目录加载依赖模块
|
||||||
|
4. **最小改动:** 通过辅助函数 `importOptionalDep` 简化插件代码改造
|
||||||
|
|
||||||
|
该方案可以显著减少主 `node_modules` 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。
|
||||||
Vendored
-14
@@ -87,20 +87,6 @@
|
|||||||
"plus_use_prod": "false",
|
"plus_use_prod": "false",
|
||||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server-local-comm",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"cwd": "${workspaceFolder}/packages/ui/certd-server",
|
|
||||||
"runtimeExecutable": "npm",
|
|
||||||
"runtimeArgs": ["run", "dev-localcomm"],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"env": {
|
|
||||||
"plus_use_prod": "false",
|
|
||||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
|
|||||||
@@ -139,9 +139,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
|||||||
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
|
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
|
||||||
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
|
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
|
||||||
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
||||||
- 前端组件样式统一写在 `<style>` / Less / CSS 文件里,通过样式名映射到元素;尽量不要在元素上直接写 `style`。
|
|
||||||
- 每个组件都要有一个稳定的根样式名,并把组件下方样式全部包在该根样式名内;尽量不要使用 `scoped`。
|
|
||||||
- 可复用的公共样式名放在 `packages/ui/certd-client/src/style` 下维护,优先使用 `cd-` 前缀,避免散落在业务组件里重复定义。
|
|
||||||
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
|
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
|
||||||
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`。
|
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`。
|
||||||
|
|
||||||
@@ -220,18 +217,3 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
|||||||
### 旧版数据兼容
|
### 旧版数据兼容
|
||||||
|
|
||||||
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||||
|
|
||||||
## 前端路由与国际化
|
|
||||||
|
|
||||||
- 路由 `meta.title` 是 **i18n 国际化 key**,必须在 `src/locales/langs/zh-CN/` 和 `src/locales/langs/en-US/` 对应的模块文件中添加翻译。
|
|
||||||
- 示例:路由 `title: "certd.auditLog"` 需要在中英 locales 文件中有对应 key(`"certd.auditLog": "操作日志"` / `"certd.auditLog": "Audit Log"`)。
|
|
||||||
- 菜单通过路由自动生成,需设置 `meta.isMenu: true` 才会出现在左侧菜单。
|
|
||||||
- Plus 版功能菜单需设置 `meta.show: () => { const settingStore = useSettingStore(); return settingStore.isPlus; }`。
|
|
||||||
|
|
||||||
## 审计日志
|
|
||||||
|
|
||||||
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
|
|
||||||
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
|
|
||||||
- Controller 中的 `@Post("/add", { summary: "xxxx" })`, 这个summary是必须要的,他是日志action字段的来源
|
|
||||||
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope(`/api/sys/` → system,其他 → user)。
|
|
||||||
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Certd
|
# Certd
|
||||||
|
|
||||||
中文 | [English](./README_en.md)
|
中文 | [English](./README_en.md)
|
||||||
|
|
||||||
@@ -105,56 +105,33 @@ https://certd.handfree.work/
|
|||||||
|
|
||||||
#### Docker镜像说明:
|
#### Docker镜像说明:
|
||||||
|
|
||||||
##### 1. 镜像地址格式:
|
**镜像版本:**
|
||||||
|
|
||||||
```
|
|
||||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
|
||||||
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
|
|
||||||
```
|
|
||||||
##### 2. 版本标签:
|
|
||||||
|
|
||||||
**最新版本标签:**
|
|
||||||
|
|
||||||
| 版本 | 标签 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
|
|
||||||
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
|
|
||||||
|
|
||||||
**系统分支版本:**
|
|
||||||
|
|
||||||
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
|
|
||||||
|
|
||||||
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` |
|
|
||||||
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` |
|
|
||||||
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` |
|
|
||||||
|
|
||||||
##### 2. 镜像地址:
|
|
||||||
|
|
||||||
| 镜像仓库 | 最新预览版 | slim | armv7 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
|
||||||
| Docker Hub | `greper/certd:latest` | `certd:slim` |
|
|
||||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
|
||||||
|
|
||||||
|
|
||||||
> 注意:
|
| 标签 | 指定版本 | 基础系统 | 说明 |
|
||||||
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
| --- | --- | --- | --- |
|
||||||
> 2. 稳定版在后面加 `-stable` 即可。
|
| `latest` | `[version]` | Alpine Linux | 默认版本,镜像体积小 |
|
||||||
> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。
|
| `slim` | `[version]-slim` | Debian slim | 基于glibc,dns解析兼容性好(可能需要配置security_opt -seccomp=unconfined) |
|
||||||
|
| `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
|
||||||
|
|
||||||
|
|
||||||
##### 3. 镜像构建说明:
|
**镜像地址:**
|
||||||
|
|
||||||
|
| 镜像仓库 | latest | slim | armv7 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 阿里云 | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
|
||||||
|
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
|
||||||
|
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
|
||||||
|
|
||||||
|
> 带版本号的标签请将 `latest` / `slim` / `armv7` 替换为 `[version]` / `[version]-slim` / `[version]-armv7`
|
||||||
|
|
||||||
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
|
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
|
||||||
- [点我查看预览版构建日志](https://github.com/certd/certd/actions/workflows/release-image.yml)
|
- [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
|
||||||
- [点我查看稳定版发布日志](https://github.com/certd/certd/actions/workflows/stable-release.yml)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 4. 安全注意事项:
|
> 注意:
|
||||||
|
>
|
||||||
> - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
|
> - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
|
||||||
> - 请务必使用HTTPS协议访问本应用,避免被中间人攻击
|
> - 请务必使用HTTPS协议访问本应用,避免被中间人攻击
|
||||||
> - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击
|
> - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击
|
||||||
|
|||||||
+13
-36
@@ -1,4 +1,4 @@
|
|||||||
# Certd
|
# Certd
|
||||||
|
|
||||||
[中文](./README.md) | English
|
[中文](./README.md) | English
|
||||||
|
|
||||||
@@ -95,44 +95,21 @@ You can choose one of the following deployment methods based on your needs:
|
|||||||
|
|
||||||
#### Docker Image Information:
|
#### Docker Image Information:
|
||||||
|
|
||||||
**Release channels:**
|
- Domestic Image Addresses:
|
||||||
|
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
|
||||||
|
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`, `[version]-armv7`
|
||||||
|
- DockerHub Addresses:
|
||||||
|
- `https://hub.docker.com/r/greper/certd`
|
||||||
|
- `greper/certd:latest`
|
||||||
|
- `greper/certd:armv7`, `greper/certd:[version]-armv7`
|
||||||
|
- GitHub Packages Addresses:
|
||||||
|
|
||||||
| Channel | Description |
|
- `ghcr.io/certd/certd:latest`
|
||||||
| --- | --- |
|
- `ghcr.io/certd/certd:armv7`, `ghcr.io/certd/certd:[version]-armv7`
|
||||||
| `stable` / `slim-stable` | **Stable version**, production-ready and fully tested, recommended for production environments |
|
|
||||||
| `latest` / `slim` / `armv7` | **Preview version**, latest development build with newest features but potentially less stable |
|
|
||||||
|
|
||||||
**Image tags:**
|
|
||||||
|
|
||||||
| Channel | Tag | Versioned Tag | Base System | Description |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| **Stable** | `stable` | `[version]-stable` | Alpine Linux | Recommended for production |
|
|
||||||
| | `slim-stable` | `[version]-slim-stable` | Debian slim | Better DNS resolution compatibility |
|
|
||||||
| **Preview** | `latest` | `[version]` | Alpine Linux | Default, small image size |
|
|
||||||
| | `slim` | `[version]-slim` | Debian slim | Better DNS resolution compatibility |
|
|
||||||
| | `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 architecture |
|
|
||||||
|
|
||||||
**Stable version image addresses:**
|
|
||||||
|
|
||||||
| Registry | `stable` | `slim-stable` |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable` |
|
|
||||||
| Docker Hub | `greper/certd:stable` | `greper/certd:slim-stable` |
|
|
||||||
| GitHub Packages | `ghcr.io/certd/certd:stable` | `ghcr.io/certd/certd:slim-stable` |
|
|
||||||
|
|
||||||
**Preview version image addresses:**
|
|
||||||
|
|
||||||
| Registry | `latest` | `slim` | `armv7` |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
|
|
||||||
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
|
|
||||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
|
|
||||||
|
|
||||||
> For versioned tags, replace tag name with `[version]-tag`, e.g. replace `stable` with `[version]-stable`
|
|
||||||
|
|
||||||
- Images are built automatically by `Actions`, with a transparent process. Please use them with confidence.
|
- Images are built automatically by `Actions`, with a transparent process. Please use them with confidence.
|
||||||
- [Click here to view preview version build logs](https://github.com/certd/certd/actions/workflows/release-image.yml)
|
- [Click here to view image build logs](https://github.com/certd/certd/actions/workflows/build-image.yml)
|
||||||
- [Click here to view stable version release logs](https://github.com/certd/certd/actions/workflows/stable-release.yml)
|
|
||||||

|

|
||||||
|
|
||||||
> Note:
|
> Note:
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ version: '3.3' # 兼容旧版docker-compose
|
|||||||
services:
|
services:
|
||||||
certd:
|
certd:
|
||||||
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
|
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
|
||||||
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
|
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
||||||
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
|
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
|
||||||
# image: greper/certd:latest
|
# image: greper/certd:latest
|
||||||
# --------- 生产建议使用稳定版, latest改成stable即可
|
|
||||||
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
|
|
||||||
|
|
||||||
|
|
||||||
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
|
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
|
||||||
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
|
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
|
||||||
container_name: certd # 容器名
|
container_name: certd # 容器名
|
||||||
|
|||||||
@@ -6,22 +6,13 @@
|
|||||||
|
|
||||||
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
|
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
|
||||||
|
|
||||||
**最新版本:**
|
| 版本标签 | 基础系统 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
|
||||||
|
| `slim` / `[version]-slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined)|
|
||||||
|
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
|
||||||
|
|
||||||
| 版本 | 标签 | 说明 |
|
> 如果您不确定使用哪个版本,请使用默认的 `latest` 版本。
|
||||||
| --- | --- | --- |
|
|
||||||
| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
|
|
||||||
| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
|
|
||||||
|
|
||||||
**系统版本分支:**
|
|
||||||
|
|
||||||
| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` |
|
|
||||||
| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined)| `certd:[version-]slim` | `certd:[version-]slim-stable` |
|
|
||||||
| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` |
|
|
||||||
|
|
||||||
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
|
||||||
|
|
||||||
### 一键脚本安装(推荐)
|
### 一键脚本安装(推荐)
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -19,7 +19,7 @@
|
|||||||
"devb": "lerna run dev-build",
|
"devb": "lerna run dev-build",
|
||||||
"i-all": "lerna link && lerna exec npm install ",
|
"i-all": "lerna link && lerna exec npm install ",
|
||||||
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
|
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
|
||||||
"publish2": " npm run pub_all && pnpm run afterpublishOnly",
|
"publish2":" npm run pub_all && pnpm run afterpublishOnly",
|
||||||
"afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll",
|
"afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll",
|
||||||
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
|
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
|
||||||
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata",
|
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata",
|
||||||
@@ -45,9 +45,7 @@
|
|||||||
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
|
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
|
||||||
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
|
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
|
||||||
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
|
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
|
||||||
"get_version": "node --experimental-json-modules ./scripts/version.js",
|
"get_version": "node --experimental-json-modules ./scripts/version.js"
|
||||||
"stable": "node ./scripts/stable.js",
|
|
||||||
"set-release-stable": "node ./scripts/set-release-stable.js"
|
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -389,9 +389,6 @@ export class Executor {
|
|||||||
};
|
};
|
||||||
await instance.setCtx(taskCtx);
|
await instance.setCtx(taskCtx);
|
||||||
|
|
||||||
if (!(instance instanceof AbstractTaskPlugin)) {
|
|
||||||
throw new Error(`插件类型错误:${step.type}不是AbstractTaskPlugin的实例`);
|
|
||||||
}
|
|
||||||
await instance.onInstance();
|
await instance.onInstance();
|
||||||
const result = await instance.execute();
|
const result = await instance.execute();
|
||||||
//执行结果处理
|
//执行结果处理
|
||||||
@@ -401,7 +398,6 @@ export class Executor {
|
|||||||
}
|
}
|
||||||
//输出上下文变量到output context
|
//输出上下文变量到output context
|
||||||
forEach(define.output, (item: any, key: any) => {
|
forEach(define.output, (item: any, key: any) => {
|
||||||
// @ts-ignore
|
|
||||||
step.status!.output[key] = instance[key];
|
step.status!.output[key] = instance[key];
|
||||||
// const stepOutputKey = `step.${step.id}.${key}`;
|
// const stepOutputKey = `step.${step.id}.${key}`;
|
||||||
// this.runtime.context[stepOutputKey] = instance[key];
|
// this.runtime.context[stepOutputKey] = instance[key];
|
||||||
@@ -415,8 +411,7 @@ export class Executor {
|
|||||||
merge(vars, instance._result.pipelineVars);
|
merge(vars, instance._result.pipelineVars);
|
||||||
await this.pipelineContext.setObj("vars", vars);
|
await this.pipelineContext.setObj("vars", vars);
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
|
||||||
if (Object.keys(instance._result?.pipelinePrivateVars).length > 0) {
|
|
||||||
// 判断 pipelineVars 有值时更新
|
// 判断 pipelineVars 有值时更新
|
||||||
let vars = await this.pipelineContext.getObj("privateVars");
|
let vars = await this.pipelineContext.getObj("privateVars");
|
||||||
vars = vars || {};
|
vars = vars || {};
|
||||||
|
|||||||
@@ -568,18 +568,11 @@ export class RuntimeDepsService {
|
|||||||
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
||||||
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
|
|
||||||
log.info("第三方依赖安装完成");
|
log.info("第三方依赖安装完成");
|
||||||
return { registryUrl: tryUrl, packageJsonPath };
|
return { registryUrl: tryUrl, packageJsonPath };
|
||||||
}
|
}
|
||||||
const errOutput = (result.stderr || "").trim();
|
lastError = result.stderr || result.stdout || "unknown error";
|
||||||
const outOutput = (result.stdout || "").trim();
|
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
||||||
lastError = errOutput || outOutput || "unknown error";
|
|
||||||
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
|
||||||
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
|
|
||||||
if (outOutput) {
|
|
||||||
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.writeInstallState(statePath, {
|
this.writeInstallState(statePath, {
|
||||||
...currentState,
|
...currentState,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export type IframeMessageData<T> = {
|
export type IframeMessageData<T> = {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -29,12 +29,10 @@ export class IframeClient {
|
|||||||
onError?: any;
|
onError?: any;
|
||||||
|
|
||||||
handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {};
|
handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {};
|
||||||
private messageHandler: (event: MessageEvent<IframeMessageData<any>>) => Promise<void>;
|
|
||||||
|
|
||||||
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
|
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
this.onError = onError;
|
this.onError = onError;
|
||||||
this.messageHandler = async (event: MessageEvent<IframeMessageData<any>>) => {
|
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (data.action) {
|
if (data.action) {
|
||||||
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
|
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
|
||||||
@@ -42,21 +40,20 @@ export class IframeClient {
|
|||||||
const handler = this.handlers[data.action];
|
const handler = this.handlers[data.action];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
const res = await handler(data);
|
const res = await handler(data);
|
||||||
if (data.id && data.action !== "reply") {
|
if (data.id && data.action !== 'reply') {
|
||||||
await this.send("reply", res, data.id);
|
await this.send('reply', res, data.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
|
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
await this.send("reply", {}, data.id, 500, e.message);
|
await this.send('reply', {}, data.id, 500, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
window.addEventListener("message", this.messageHandler);
|
|
||||||
|
|
||||||
this.register("reply", async data => {
|
this.register('reply', async data => {
|
||||||
const req = this.requestQueue[data.replyId!];
|
const req = this.requestQueue[data.replyId!];
|
||||||
if (req) {
|
if (req) {
|
||||||
req.onReply(data);
|
req.onReply(data);
|
||||||
@@ -64,17 +61,11 @@ export class IframeClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
isInFrame() {
|
||||||
public destroy() {
|
|
||||||
window.removeEventListener("message", this.messageHandler);
|
|
||||||
this.requestQueue = {};
|
|
||||||
this.handlers = {};
|
|
||||||
}
|
|
||||||
public isInFrame() {
|
|
||||||
return window.self !== window.top;
|
return window.self !== window.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
public register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
|
register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
|
||||||
this.handlers[action] = handler;
|
this.handlers[action] = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,12 +106,12 @@ export class IframeClient {
|
|||||||
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
|
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
|
||||||
if (!this.iframe) {
|
if (!this.iframe) {
|
||||||
if (!window.parent) {
|
if (!window.parent) {
|
||||||
reject("当前页面不在 iframe 中");
|
reject('当前页面不在 iframe 中');
|
||||||
}
|
}
|
||||||
window.parent.postMessage(reqMessageData, "*");
|
window.parent.postMessage(reqMessageData, '*');
|
||||||
} else {
|
} else {
|
||||||
//子页面
|
//子页面
|
||||||
this.iframe.contentWindow?.postMessage(reqMessageData, "*");
|
this.iframe.contentWindow?.postMessage(reqMessageData, '*');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/// <reference types="mocha" />
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { AuditLogContext } from "./audit.js";
|
|
||||||
|
|
||||||
// AuditLog decorator and getAuditLogOptions are removed since auditLog()
|
|
||||||
// now signals audit intent directly via ctx.auditLog.enabled
|
|
||||||
|
|
||||||
describe("AuditLogContext type", () => {
|
|
||||||
it("supports enabled flag", () => {
|
|
||||||
const ctx: AuditLogContext = {
|
|
||||||
type: "pipeline",
|
|
||||||
action: "删除流水线",
|
|
||||||
append: ["ID:5"],
|
|
||||||
content: "删除了流水线(ID:5)",
|
|
||||||
projectId: 3,
|
|
||||||
enabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.equal(ctx.enabled, true);
|
|
||||||
assert.equal(ctx.type, "pipeline");
|
|
||||||
assert.equal(ctx.content, "删除了流水线(ID:5)");
|
|
||||||
assert.equal(ctx.projectId, 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works with minimal fields", () => {
|
|
||||||
const ctx: AuditLogContext = {
|
|
||||||
enabled: true,
|
|
||||||
append: ["提交2条"],
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.equal(ctx.enabled, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
export type AuditLogOptions = {
|
|
||||||
type?: string;
|
|
||||||
action?: string;
|
|
||||||
content?: string;
|
|
||||||
template?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AuditLogContext = {
|
|
||||||
type?: string;
|
|
||||||
action?: string;
|
|
||||||
append?: string | string[];
|
|
||||||
content?: string;
|
|
||||||
projectId?: number;
|
|
||||||
enabled?: boolean;
|
|
||||||
scope?: string;
|
|
||||||
userId?: number;
|
|
||||||
username?: string;
|
|
||||||
success?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 审计日志方法的参数类型 */
|
|
||||||
export type AuditLogParam = {
|
|
||||||
type?: string;
|
|
||||||
action?: string;
|
|
||||||
content?: string;
|
|
||||||
append?: string | string[];
|
|
||||||
projectId?: number;
|
|
||||||
userId?: number;
|
|
||||||
username?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** AuditService.log() 参数类型 */
|
|
||||||
export type AuditLogWriteParam = {
|
|
||||||
userId: number;
|
|
||||||
type: string;
|
|
||||||
action: string;
|
|
||||||
content: string;
|
|
||||||
username?: string;
|
|
||||||
projectId?: number;
|
|
||||||
ipAddress?: string;
|
|
||||||
scope?: string;
|
|
||||||
success?: boolean;
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,6 @@ import type { IMidwayContainer } from "@midwayjs/core";
|
|||||||
import * as koa from "@midwayjs/koa";
|
import * as koa from "@midwayjs/koa";
|
||||||
import { Constants } from "./constants.js";
|
import { Constants } from "./constants.js";
|
||||||
import { isEnterprise } from "./mode.js";
|
import { isEnterprise } from "./mode.js";
|
||||||
import type { AuditLogContext, AuditLogParam } from "./audit.js";
|
|
||||||
|
|
||||||
export abstract class BaseController {
|
export abstract class BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -128,43 +127,4 @@ export abstract class BaseController {
|
|||||||
}
|
}
|
||||||
return { projectId, userId };
|
return { projectId, userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuditType(): string {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
auditLog(bean: AuditLogParam = {}) {
|
|
||||||
const auditLog = this.ensureAuditLogContext();
|
|
||||||
auditLog.enabled = true;
|
|
||||||
if (bean.userId != null) {
|
|
||||||
auditLog.userId = bean.userId;
|
|
||||||
}
|
|
||||||
if (bean.username != null) {
|
|
||||||
auditLog.username = bean.username;
|
|
||||||
}
|
|
||||||
if (bean.type != null) {
|
|
||||||
auditLog.type = bean.type;
|
|
||||||
}
|
|
||||||
if (bean.action != null) {
|
|
||||||
auditLog.action = bean.action;
|
|
||||||
}
|
|
||||||
if (bean.projectId != null) {
|
|
||||||
auditLog.projectId = bean.projectId;
|
|
||||||
}
|
|
||||||
if (bean.content) {
|
|
||||||
auditLog.content = bean.content;
|
|
||||||
}
|
|
||||||
if (bean.append) {
|
|
||||||
const items = Array.isArray(bean.append) ? bean.append : [bean.append];
|
|
||||||
const old = Array.isArray(auditLog.append) ? auditLog.append : auditLog.append ? [auditLog.append] : [];
|
|
||||||
auditLog.append = [...old, ...items].filter(item => item && String(item).trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureAuditLogContext(): AuditLogContext {
|
|
||||||
if (!this.ctx.auditLog) {
|
|
||||||
this.ctx.auditLog = {};
|
|
||||||
}
|
|
||||||
return this.ctx.auditLog;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export const Constants = {
|
export const Constants = {
|
||||||
dataDir: "./data",
|
dataDir: './data',
|
||||||
role: {
|
role: {
|
||||||
defaultUser: 3,
|
defaultUser: 3,
|
||||||
},
|
},
|
||||||
per: {
|
per: {
|
||||||
//无需登录
|
//无需登录
|
||||||
guest: "_guest_",
|
guest: '_guest_',
|
||||||
//无需登录
|
//无需登录
|
||||||
anonymous: "_guest_",
|
anonymous: '_guest_',
|
||||||
//无需登录,有 token 时解析当前用户
|
//无需登录,有 token 时解析当前用户
|
||||||
guestOptionalAuth: "_guestOptionalAuth_",
|
guestOptionalAuth: '_guestOptionalAuth_',
|
||||||
//仅需要登录
|
//仅需要登录
|
||||||
authOnly: "_authOnly_",
|
authOnly: '_authOnly_',
|
||||||
//仅需要登录
|
//仅需要登录
|
||||||
loginOnly: "_authOnly_",
|
loginOnly: '_authOnly_',
|
||||||
|
|
||||||
open: "_open_",
|
open: '_open_',
|
||||||
},
|
},
|
||||||
res: {
|
res: {
|
||||||
serverError(message: string) {
|
serverError(message: string) {
|
||||||
@@ -26,102 +26,102 @@ export const Constants = {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
code: 1,
|
code: 1,
|
||||||
message: "Internal server error",
|
message: 'Internal server error',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
code: 0,
|
code: 0,
|
||||||
message: "success",
|
message: 'success',
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
code: 10,
|
code: 10,
|
||||||
message: "参数错误",
|
message: '参数错误',
|
||||||
},
|
},
|
||||||
needvip: {
|
needvip: {
|
||||||
code: 88,
|
code: 88,
|
||||||
message: "需要VIP",
|
message: '需要VIP',
|
||||||
},
|
},
|
||||||
needsuite: {
|
needsuite: {
|
||||||
code: 89,
|
code: 89,
|
||||||
message: "需要购买或升级套餐",
|
message: '需要购买或升级套餐',
|
||||||
},
|
},
|
||||||
loginError: {
|
loginError: {
|
||||||
code: 2,
|
code: 2,
|
||||||
message: "登录失败",
|
message: '登录失败',
|
||||||
},
|
},
|
||||||
codeError: {
|
codeError: {
|
||||||
code: 3,
|
code: 3,
|
||||||
message: "验证码错误",
|
message: '验证码错误',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
code: 401,
|
code: 401,
|
||||||
message: "您还未登录或token已过期",
|
message: '您还未登录或token已过期',
|
||||||
},
|
},
|
||||||
permission: {
|
permission: {
|
||||||
code: 402,
|
code: 402,
|
||||||
message: "您没有权限",
|
message: '您没有权限',
|
||||||
},
|
},
|
||||||
param: {
|
param: {
|
||||||
code: 400,
|
code: 400,
|
||||||
message: "参数错误",
|
message: '参数错误',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
code: 404,
|
code: 404,
|
||||||
message: "页面/文件/资源不存在",
|
message: '页面/文件/资源不存在',
|
||||||
},
|
},
|
||||||
|
|
||||||
preview: {
|
preview: {
|
||||||
code: 10001,
|
code: 10001,
|
||||||
message: "对不起,预览环境不允许修改此数据",
|
message: '对不起,预览环境不允许修改此数据',
|
||||||
},
|
},
|
||||||
siteOff: {
|
siteOff:{
|
||||||
code: 10010,
|
code: 10010,
|
||||||
message: "站点已关闭",
|
message: '站点已关闭',
|
||||||
},
|
},
|
||||||
need2fa: {
|
need2fa:{
|
||||||
code: 10020,
|
code: 10020,
|
||||||
message: "需要2FA认证",
|
message: '需要2FA认证',
|
||||||
},
|
},
|
||||||
openKeyError: {
|
openKeyError: {
|
||||||
code: 20000,
|
code: 20000,
|
||||||
message: "ApiToken错误",
|
message: 'ApiToken错误',
|
||||||
},
|
},
|
||||||
openKeySignError: {
|
openKeySignError: {
|
||||||
code: 20001,
|
code: 20001,
|
||||||
message: "ApiToken签名错误",
|
message: 'ApiToken签名错误',
|
||||||
},
|
},
|
||||||
openKeyExpiresError: {
|
openKeyExpiresError: {
|
||||||
code: 20002,
|
code: 20002,
|
||||||
message: "ApiToken时间戳错误",
|
message: 'ApiToken时间戳错误',
|
||||||
},
|
},
|
||||||
openKeySignTypeError: {
|
openKeySignTypeError: {
|
||||||
code: 20003,
|
code: 20003,
|
||||||
message: "ApiToken签名类型不支持",
|
message: 'ApiToken签名类型不支持',
|
||||||
},
|
},
|
||||||
openParamError: {
|
openParamError: {
|
||||||
code: 20010,
|
code: 20010,
|
||||||
message: "请求参数错误",
|
message: '请求参数错误',
|
||||||
},
|
},
|
||||||
openCertNotFound: {
|
openCertNotFound: {
|
||||||
code: 20011,
|
code: 20011,
|
||||||
message: "证书不存在",
|
message: '证书不存在',
|
||||||
},
|
},
|
||||||
openCertNotReady: {
|
openCertNotReady: {
|
||||||
code: 20012,
|
code: 20012,
|
||||||
message: "证书还未生成",
|
message: '证书还未生成',
|
||||||
},
|
},
|
||||||
openCertApplying: {
|
openCertApplying: {
|
||||||
code: 20013,
|
code: 20013,
|
||||||
message: "证书正在申请中,请稍后重新获取",
|
message: '证书正在申请中,请稍后重新获取',
|
||||||
},
|
},
|
||||||
openDomainNoVerifier: {
|
openDomainNoVerifier:{
|
||||||
code: 20014,
|
code: 20014,
|
||||||
message: "域名校验方式未配置",
|
message: '域名校验方式未配置',
|
||||||
},
|
},
|
||||||
openEmailNotFound: {
|
openEmailNotFound: {
|
||||||
code: 20021,
|
code: 20021,
|
||||||
message: "用户邮箱还未配置",
|
message: '用户邮箱还未配置',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
systemUserId: 0, // 系统级别userid固定为0
|
systemUserId: 0, // 系统级别userid固定为0
|
||||||
enterpriseUserId: -1, // 企业模式用户id固定为-1
|
enterpriseUserId: -1 // 企业模式用户id固定为-1
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { createRequestParamDecorator } from "@midwayjs/core";
|
|
||||||
|
|
||||||
export const AuditLog = (opts: { type?: string; action?: string; content?: string; enabled?: boolean } = {}) => {
|
|
||||||
return createRequestParamDecorator(ctx => {
|
|
||||||
if (!ctx.auditLog) {
|
|
||||||
ctx.auditLog = {};
|
|
||||||
}
|
|
||||||
ctx.auditLog.enabled = opts.enabled !== false;
|
|
||||||
if (opts.type != null) {
|
|
||||||
ctx.auditLog.type = opts.type;
|
|
||||||
}
|
|
||||||
if (opts.action != null) {
|
|
||||||
ctx.auditLog.action = opts.action;
|
|
||||||
}
|
|
||||||
if (opts.content != null) {
|
|
||||||
ctx.auditLog.content = opts.content;
|
|
||||||
}
|
|
||||||
return ctx.auditLog;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./decoractor.js";
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ALL, Body, Post, Query } from "@midwayjs/core";
|
import { ALL, Body, Post, Query } from '@midwayjs/core';
|
||||||
import { BaseController } from "./base-controller.js";
|
import { BaseController } from './base-controller.js';
|
||||||
|
|
||||||
export abstract class CrudController<T> extends BaseController {
|
export abstract class CrudController<T> extends BaseController {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
abstract getService<T>();
|
abstract getService<T>();
|
||||||
|
|
||||||
@Post("/page")
|
@Post('/page')
|
||||||
async page(@Body(ALL) body: any) {
|
async page(@Body(ALL) body: any) {
|
||||||
const pageRet = await this.getService().page({
|
const pageRet = await this.getService().page({
|
||||||
query: body.query ?? {},
|
query: body.query ?? {},
|
||||||
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
|
|||||||
return this.ok(pageRet);
|
return this.ok(pageRet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/list")
|
@Post('/list')
|
||||||
async list(@Body(ALL) body: any) {
|
async list(@Body(ALL) body: any) {
|
||||||
const listRet = await this.getService().list({
|
const listRet = await this.getService().list({
|
||||||
query: body.query ?? {},
|
query: body.query ?? {},
|
||||||
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
|
|||||||
return this.ok(listRet);
|
return this.ok(listRet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/add")
|
@Post('/add')
|
||||||
async add(@Body(ALL) bean: any) {
|
async add(@Body(ALL) bean: any) {
|
||||||
delete bean.id;
|
delete bean.id;
|
||||||
const id = await this.getService().add(bean);
|
const id = await this.getService().add(bean);
|
||||||
return this.ok(id);
|
return this.ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/info")
|
@Post('/info')
|
||||||
async info(@Query("id") id: number) {
|
async info(@Query('id') id: number) {
|
||||||
const bean = await this.getService().info(id);
|
const bean = await this.getService().info(id);
|
||||||
return this.ok(bean);
|
return this.ok(bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/update")
|
@Post('/update')
|
||||||
async update(@Body(ALL) bean: any) {
|
async update(@Body(ALL) bean: any) {
|
||||||
await this.getService().update(bean);
|
await this.getService().update(bean);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/delete")
|
@Post('/delete')
|
||||||
async delete(@Query("id") id: number) {
|
async delete(@Query('id') id: number) {
|
||||||
await this.getService().delete([id]);
|
await this.getService().delete([id]);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/deleteByIds")
|
@Post('/deleteByIds')
|
||||||
async deleteByIds(@Body("ids") ids: number[]) {
|
async deleteByIds(@Body('ids') ids: number[]) {
|
||||||
await this.getService().delete(ids);
|
await this.getService().delete(ids);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 通用异常
|
* 通用异常
|
||||||
*/
|
*/
|
||||||
export class LoginErrorException extends BaseException {
|
export class LoginErrorException extends BaseException {
|
||||||
leftCount: number;
|
leftCount: number;
|
||||||
userId?: number;
|
constructor(message, leftCount: number) {
|
||||||
constructor(message, leftCount: number, userId?: number) {
|
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
|
||||||
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
|
|
||||||
this.leftCount = leftCount;
|
this.leftCount = leftCount;
|
||||||
this.userId = userId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export * from "./base-controller.js";
|
export * from './base-controller.js';
|
||||||
export * from "./constants.js";
|
export * from './constants.js';
|
||||||
export * from "./crud-controller.js";
|
export * from './crud-controller.js';
|
||||||
export * from "./enum-item.js";
|
export * from './enum-item.js';
|
||||||
export * from "./exception/index.js";
|
export * from './exception/index.js';
|
||||||
export * from "./result.js";
|
export * from './result.js';
|
||||||
export * from "./base-service.js";
|
export * from './base-service.js';
|
||||||
export * from "./audit.js";
|
export * from "./mode.js"
|
||||||
export * from "./mode.js";
|
|
||||||
export * from "./core/index.js";
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
let adminMode = "saas";
|
let adminMode = "saas"
|
||||||
|
|
||||||
export function setAdminMode(mode: string = "saas") {
|
export function setAdminMode(mode:string = "saas"){
|
||||||
adminMode = mode;
|
adminMode = mode
|
||||||
}
|
}
|
||||||
export function getAdminMode() {
|
export function getAdminMode(){
|
||||||
return adminMode;
|
return adminMode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEnterprise() {
|
export function isEnterprise(){
|
||||||
return adminMode === "enterprise";
|
return adminMode === "enterprise"
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ export class PlusService {
|
|||||||
baseURL: plusRequestService.getBaseURL(),
|
baseURL: plusRequestService.getBaseURL(),
|
||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Berear ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await http.request(config);
|
const res = await http.request(config);
|
||||||
@@ -173,9 +173,4 @@ export class PlusService {
|
|||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async request(config: HttpRequestConfig) {
|
|
||||||
const plusRequestService = await this.getPlusRequestService();
|
|
||||||
return await plusRequestService.request(config);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,11 +279,3 @@ export class SysSafeSetting extends BaseSettings {
|
|||||||
autoHiddenTimes: 5,
|
autoHiddenTimes: 5,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SysPluginSetting extends BaseSettings {
|
|
||||||
static __title__ = "系统插件设置";
|
|
||||||
static __key__ = "sys.plugin";
|
|
||||||
static __access__ = "private";
|
|
||||||
|
|
||||||
lastSyncTime?: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -93,21 +93,19 @@ export class Flyway {
|
|||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
this.errorTip(err);
|
|
||||||
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
|
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
|
|
||||||
|
if (err.code === "SQLITE_IOERR_WRITE") {
|
||||||
|
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.info("[ midfly ] end-------------");
|
this.logger.info("[ midfly ] end-------------");
|
||||||
}
|
}
|
||||||
|
|
||||||
private errorTip(err: any) {
|
|
||||||
if (err.code === "SQLITE_IOERR_WRITE") {
|
|
||||||
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
|
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
|
||||||
const hash = await this.getFileHash(filepath);
|
const hash = await this.getFileHash(filepath);
|
||||||
//先删除
|
//先删除
|
||||||
@@ -267,7 +265,6 @@ export class Flyway {
|
|||||||
await queryRunner.query(sql);
|
await queryRunner.query(sql);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error("exec sql error : ", err.message, err);
|
this.logger.error("exec sql error : ", err.message, err);
|
||||||
this.errorTip(err);
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG base_type=alpine
|
ARG base_type=alpine
|
||||||
|
|
||||||
# 根据 base_type 参数选择基础镜像系列
|
# 根据 base_type 参数选择基础镜像系列
|
||||||
FROM --platform=linux/amd64 node:22-alpine AS base-amd64-alpine
|
FROM --platform=linux/amd64 node:22-alpine AS base-amd64-alpine
|
||||||
@@ -99,7 +99,6 @@ RUN ARCH=$(uname -m) && \
|
|||||||
|
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV certd_release_mode=latest
|
|
||||||
ENV MIDWAY_SERVER_ENV=production
|
ENV MIDWAY_SERVER_ENV=production
|
||||||
|
|
||||||
RUN npm install -g pnpm@10.33.4
|
RUN npm install -g pnpm@10.33.4
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ async function doActive() {
|
|||||||
title: t("vip.successTitle"),
|
title: t("vip.successTitle"),
|
||||||
content: t("vip.successContent", {
|
content: t("vip.successContent", {
|
||||||
vipLabel,
|
vipLabel,
|
||||||
expireDate: settingStore.plusInfo.expireTime === -1 ? t("vip.permanent") : dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD"),
|
expireDate: dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD"),
|
||||||
}),
|
}),
|
||||||
onOk() {
|
onOk() {
|
||||||
if (!(settingStore.installInfo.bindUserId > 0)) {
|
if (!(settingStore.installInfo.bindUserId > 0)) {
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ const projectStore = useProjectStore();
|
|||||||
<div v-if="!settingStore.isComm" class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full hidden md:block">
|
<div v-if="!settingStore.isComm" class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full hidden md:block">
|
||||||
<fs-button shape="circle" type="text" icon="ion:logo-github" :text="null" @click="goGithub" />
|
<fs-button shape="circle" type="text" icon="ion:logo-github" :text="null" @click="goGithub" />
|
||||||
</div>
|
</div>
|
||||||
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<PageFooter></PageFooter>
|
<PageFooter></PageFooter>
|
||||||
|
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
||||||
</template>
|
</template>
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<router-view> </router-view>
|
<router-view> </router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<!--<script lang="ts" setup>-->
|
||||||
import { useSettingStore } from "../store/settings";
|
<!--import { usePageStore } from "/@/store/modules/page";-->
|
||||||
|
|
||||||
const settingsStore = useSettingStore();
|
<!--const pageStore = usePageStore();-->
|
||||||
</script>
|
<!--const keepAlive = pageStore.keepAlive;-->
|
||||||
|
<!--</script>-->
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export default {
|
|||||||
default: "Default",
|
default: "Default",
|
||||||
success: "Success",
|
success: "Success",
|
||||||
test: "Test",
|
test: "Test",
|
||||||
operation: "Operation",
|
|
||||||
testButton: "Test",
|
testButton: "Test",
|
||||||
operationSuccess: "Operation successful",
|
operationSuccess: "Operation successful",
|
||||||
batch_delete: "Batch Delete",
|
batch_delete: "Batch Delete",
|
||||||
|
|||||||
@@ -25,7 +25,5 @@ export default {
|
|||||||
expiringCerts: "Soon-to-Expire Certificates",
|
expiringCerts: "Soon-to-Expire Certificates",
|
||||||
supportedTasks: "Overview of Supported Deployment Tasks",
|
supportedTasks: "Overview of Supported Deployment Tasks",
|
||||||
changeLog: "Change Log",
|
changeLog: "Change Log",
|
||||||
stableRelease: "Stable",
|
|
||||||
latestRelease: "Preview",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export default {
|
|||||||
openKey: "Open API Key",
|
openKey: "Open API Key",
|
||||||
notification: "Notification Settings",
|
notification: "Notification Settings",
|
||||||
siteMonitorSetting: "Site Monitor Settings",
|
siteMonitorSetting: "Site Monitor Settings",
|
||||||
auditLog: "Audit Log",
|
|
||||||
userSecurity: "Security Settings",
|
userSecurity: "Security Settings",
|
||||||
userProfile: "Account Info",
|
userProfile: "Account Info",
|
||||||
userGrant: "Grant Delegation",
|
userGrant: "Grant Delegation",
|
||||||
@@ -40,6 +39,7 @@ export default {
|
|||||||
headerMenus: "Top Menu Settings",
|
headerMenus: "Top Menu Settings",
|
||||||
sysAccess: "System-level Authorization",
|
sysAccess: "System-level Authorization",
|
||||||
sysPlugin: "Plugin Management",
|
sysPlugin: "Plugin Management",
|
||||||
|
sysPluginEdit: "Edit Plugin",
|
||||||
sysPluginConfig: "Certificate Plugin Configuration",
|
sysPluginConfig: "Certificate Plugin Configuration",
|
||||||
accountBind: "Account Binding",
|
accountBind: "Account Binding",
|
||||||
permissionManager: "Permission Management",
|
permissionManager: "Permission Management",
|
||||||
@@ -67,7 +67,6 @@ export default {
|
|||||||
projectJoin: "Join Project",
|
projectJoin: "Join Project",
|
||||||
currentProject: "Current Project",
|
currentProject: "Current Project",
|
||||||
projectMemberManager: "Project Member",
|
projectMemberManager: "Project Member",
|
||||||
auditLog: "Audit Log",
|
|
||||||
domainMonitorSetting: "Domain Monitor Settings",
|
domainMonitorSetting: "Domain Monitor Settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,72 +42,6 @@ export default {
|
|||||||
pluginGroup: "Plugin Group",
|
pluginGroup: "Plugin Group",
|
||||||
pluginManagement: "Plugin Management",
|
pluginManagement: "Plugin Management",
|
||||||
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
||||||
localPlugin: "Installed",
|
|
||||||
pluginMarket: "Plugin Market",
|
|
||||||
installedStorePlugin: "Installed",
|
|
||||||
onlinePluginManagement: "Online Plugins",
|
|
||||||
onlinePluginSearch: "Search online plugins",
|
|
||||||
onlinePluginSync: "Sync Plugin Market",
|
|
||||||
onlinePluginSyncFirst: "Sync the plugin market list first, then search and install plugins",
|
|
||||||
onlinePluginSyncSuccess: "Plugin market list synced successfully",
|
|
||||||
onlinePluginLastSyncTime: "Last synced: {time}",
|
|
||||||
onlinePluginNotSynced: "Plugin market has not been synced",
|
|
||||||
onlinePluginRefresh: "Refresh List",
|
|
||||||
onlinePluginInstall: "Install",
|
|
||||||
onlinePluginUpdate: "Update",
|
|
||||||
onlinePluginReinstall: "Reinstall",
|
|
||||||
onlinePluginUninstall: "Uninstall",
|
|
||||||
onlinePluginStatus: "Status",
|
|
||||||
onlinePluginInstalled: "Installed",
|
|
||||||
onlinePluginAiReviewPassed: "AI reviewed",
|
|
||||||
onlinePluginEnabled: "Enabled",
|
|
||||||
onlinePluginDisabled: "Disabled",
|
|
||||||
onlinePluginClickToEnable: "Click to enable",
|
|
||||||
onlinePluginClickToDisable: "Click to disable",
|
|
||||||
onlinePluginDownloadCount: "Downloads: {count}",
|
|
||||||
onlinePluginNotInstalled: "Not Installed",
|
|
||||||
onlinePluginUpgradeAvailable: "Upgradeable",
|
|
||||||
onlinePluginCurrentVersion: "Current {version}",
|
|
||||||
onlinePluginAlreadyLatest: "Already up to date",
|
|
||||||
onlinePluginClickToUpdate: "Click to update",
|
|
||||||
onlinePluginSelfAuthored: "Mine",
|
|
||||||
onlinePluginInstallSuccess: "Online plugin installed successfully",
|
|
||||||
onlinePluginUninstallSuccess: "Online plugin uninstalled successfully",
|
|
||||||
onlinePluginDeleteConfirm: 'Are you sure you want to uninstall online plugin "{name}"? Pipelines using this plugin may fail after deletion.',
|
|
||||||
onlinePluginPublish: "Publish to Plugin Market",
|
|
||||||
onlinePluginPublishManage: "Plugin Publish Management",
|
|
||||||
onlinePluginPublishConfirm: 'Publish "{name}" to the plugin market? It will be checked by AI and then wait for administrator review.',
|
|
||||||
onlinePluginPublishConfirmTip: "Please confirm the plugin information. It will be checked by AI and then wait for administrator review.",
|
|
||||||
onlinePluginPublishManageTip: "Confirm the current plugin information. If it has been submitted before, versions, publish status, and rejection reasons are shown here.",
|
|
||||||
onlinePluginPublishSubmit: "Submit Publish",
|
|
||||||
onlinePluginPublishSuccess: "Submitted, waiting for AI security review",
|
|
||||||
onlinePluginPublishVersionRequired: "Please set the plugin version before publishing",
|
|
||||||
onlinePluginPublishStatus: "Publish Status",
|
|
||||||
onlinePluginNotSubmitted: "Not Submitted",
|
|
||||||
onlinePluginVersionManagement: "Versions",
|
|
||||||
onlinePluginLatestVersion: "Latest Version",
|
|
||||||
onlinePluginCurrentRelease: "This Release",
|
|
||||||
onlinePluginPublishPrompt: "Release Notes",
|
|
||||||
onlinePluginVersionRejectedReason: "Rejection Reason",
|
|
||||||
onlinePluginNoSubmittedVersion: "No release has been submitted yet",
|
|
||||||
onlinePluginVersionFormatError: "Use a dotted numeric version, for example 1.0.0",
|
|
||||||
onlinePluginVersionBaselineError: "Enter a version greater than v{version}",
|
|
||||||
onlinePluginStatusDraft: "Draft",
|
|
||||||
onlinePluginStatusReviewing: "Reviewing",
|
|
||||||
onlinePluginStatusPublished: "Published",
|
|
||||||
onlinePluginStatusRejected: "Rejected",
|
|
||||||
onlinePluginStatusOffline: "Offline",
|
|
||||||
onlinePluginReviewAiPending: "Pending AI Review",
|
|
||||||
onlinePluginReviewPending: "Pending Manual Review",
|
|
||||||
onlinePluginReviewPassed: "Passed",
|
|
||||||
onlinePluginReviewRejected: "Rejected",
|
|
||||||
onlinePluginAuthorRegister: "Register Plugin Author",
|
|
||||||
onlinePluginAuthorName: "Author Name",
|
|
||||||
onlinePluginAuthorDisplayName: "Display Name",
|
|
||||||
onlinePluginAuthorNotRegistered: "Not Registered",
|
|
||||||
onlinePluginAuthorNameRequired: "Please enter an author name",
|
|
||||||
onlinePluginAuthorNameHelper: "Used as the plugin market author prefix. Each bound user can register only one author.",
|
|
||||||
onlinePluginAuthorNameRuleMsg: "Must start with a letter and contain only letters, digits, hyphens, or underscores",
|
|
||||||
pleaseSelectRecord: "Please select records first",
|
pleaseSelectRecord: "Please select records first",
|
||||||
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
||||||
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
||||||
|
|||||||
@@ -104,5 +104,4 @@ export default {
|
|||||||
not_effective: "Not effective or duration not sync?",
|
not_effective: "Not effective or duration not sync?",
|
||||||
learn_more: "More privileges",
|
learn_more: "More privileges",
|
||||||
question: "More VIP related questions",
|
question: "More VIP related questions",
|
||||||
permanent: "Permanent",
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export default {
|
|||||||
default: "默认",
|
default: "默认",
|
||||||
success: "成功",
|
success: "成功",
|
||||||
test: "测试",
|
test: "测试",
|
||||||
operation: "操作",
|
|
||||||
testButton: "测试",
|
testButton: "测试",
|
||||||
operationSuccess: "操作成功",
|
operationSuccess: "操作成功",
|
||||||
batch_delete: "批量删除",
|
batch_delete: "批量删除",
|
||||||
|
|||||||
@@ -26,7 +26,5 @@ export default {
|
|||||||
expiringCerts: "最快到期证书",
|
expiringCerts: "最快到期证书",
|
||||||
supportedTasks: "已支持的部署任务总览",
|
supportedTasks: "已支持的部署任务总览",
|
||||||
changeLog: "更新日志",
|
changeLog: "更新日志",
|
||||||
stableRelease: "稳定版",
|
|
||||||
latestRelease: "预览版",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export default {
|
|||||||
openKey: "开放接口密钥",
|
openKey: "开放接口密钥",
|
||||||
notification: "通知设置",
|
notification: "通知设置",
|
||||||
siteMonitorSetting: "站点监控设置",
|
siteMonitorSetting: "站点监控设置",
|
||||||
auditLog: "操作日志",
|
|
||||||
userSecurity: "认证安全设置",
|
userSecurity: "认证安全设置",
|
||||||
userProfile: "账号信息",
|
userProfile: "账号信息",
|
||||||
userGrant: "授权委托",
|
userGrant: "授权委托",
|
||||||
@@ -41,6 +40,7 @@ export default {
|
|||||||
headerMenus: "顶部菜单设置",
|
headerMenus: "顶部菜单设置",
|
||||||
sysAccess: "系统级授权",
|
sysAccess: "系统级授权",
|
||||||
sysPlugin: "插件管理",
|
sysPlugin: "插件管理",
|
||||||
|
sysPluginEdit: "编辑插件",
|
||||||
sysPluginConfig: "证书插件配置",
|
sysPluginConfig: "证书插件配置",
|
||||||
accountBind: "账号绑定",
|
accountBind: "账号绑定",
|
||||||
permissionManager: "权限管理",
|
permissionManager: "权限管理",
|
||||||
@@ -67,7 +67,6 @@ export default {
|
|||||||
projectJoin: "加入项目",
|
projectJoin: "加入项目",
|
||||||
currentProject: "当前项目",
|
currentProject: "当前项目",
|
||||||
projectMemberManager: "项目成员管理",
|
projectMemberManager: "项目成员管理",
|
||||||
auditLog: "审计日志",
|
|
||||||
domainMonitorSetting: "域名监控设置",
|
domainMonitorSetting: "域名监控设置",
|
||||||
jobHistory: "监控执行记录",
|
jobHistory: "监控执行记录",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,72 +41,6 @@ export default {
|
|||||||
pluginGroup: "插件分组",
|
pluginGroup: "插件分组",
|
||||||
pluginManagement: "插件管理",
|
pluginManagement: "插件管理",
|
||||||
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
||||||
localPlugin: "本地插件",
|
|
||||||
pluginMarket: "插件市场",
|
|
||||||
installedStorePlugin: "插件市场",
|
|
||||||
onlinePluginManagement: "在线插件",
|
|
||||||
onlinePluginSearch: "搜索在线插件",
|
|
||||||
onlinePluginSync: "同步插件市场",
|
|
||||||
onlinePluginSyncFirst: "请先同步插件市场列表,然后再搜索和安装插件",
|
|
||||||
onlinePluginSyncSuccess: "插件市场列表同步成功",
|
|
||||||
onlinePluginLastSyncTime: "上次同步时间:{time}",
|
|
||||||
onlinePluginNotSynced: "尚未同步插件市场",
|
|
||||||
onlinePluginRefresh: "刷新列表",
|
|
||||||
onlinePluginInstall: "安装",
|
|
||||||
onlinePluginUpdate: "更新",
|
|
||||||
onlinePluginReinstall: "重新安装",
|
|
||||||
onlinePluginUninstall: "卸载",
|
|
||||||
onlinePluginStatus: "状态",
|
|
||||||
onlinePluginInstalled: "已安装",
|
|
||||||
onlinePluginAiReviewPassed: "AI审核通过",
|
|
||||||
onlinePluginEnabled: "已启用",
|
|
||||||
onlinePluginDisabled: "已禁用",
|
|
||||||
onlinePluginClickToEnable: "点击启用",
|
|
||||||
onlinePluginClickToDisable: "点击禁用",
|
|
||||||
onlinePluginDownloadCount: "下载量:{count}",
|
|
||||||
onlinePluginNotInstalled: "未安装",
|
|
||||||
onlinePluginUpgradeAvailable: "可更新",
|
|
||||||
onlinePluginCurrentVersion: "当前 {version}",
|
|
||||||
onlinePluginAlreadyLatest: "当前已是最新版本",
|
|
||||||
onlinePluginClickToUpdate: "点击更新",
|
|
||||||
onlinePluginSelfAuthored: "我的",
|
|
||||||
onlinePluginInstallSuccess: "在线插件安装成功",
|
|
||||||
onlinePluginUninstallSuccess: "在线插件卸载成功",
|
|
||||||
onlinePluginDeleteConfirm: "确定要卸载在线插件「{name}」吗?如果该插件已被流水线使用,删除可能会导致执行失败。",
|
|
||||||
onlinePluginPublish: "发布到插件市场",
|
|
||||||
onlinePluginPublishManage: "插件发布管理",
|
|
||||||
onlinePluginPublishConfirm: "确定要将「{name}」发布到插件市场吗?提交后会进行 AI 安全审查,通过后等待管理员审核。",
|
|
||||||
onlinePluginPublishConfirmTip: "请确认以下插件信息,提交后会进行 AI 安全审查,通过后等待管理员审核。",
|
|
||||||
onlinePluginPublishManageTip: "请确认当前插件信息;如已提交过发布,可以在这里查看版本、发布状态和拒绝原因。",
|
|
||||||
onlinePluginPublishSubmit: "提交发布",
|
|
||||||
onlinePluginPublishSuccess: "已提交,等待 AI 安全审查",
|
|
||||||
onlinePluginPublishVersionRequired: "请先设置插件版本号后再发布",
|
|
||||||
onlinePluginPublishStatus: "发布状态",
|
|
||||||
onlinePluginNotSubmitted: "未提交",
|
|
||||||
onlinePluginVersionManagement: "版本管理",
|
|
||||||
onlinePluginLatestVersion: "最新版本",
|
|
||||||
onlinePluginCurrentRelease: "本次发布",
|
|
||||||
onlinePluginPublishPrompt: "发布提示",
|
|
||||||
onlinePluginVersionRejectedReason: "拒绝原因",
|
|
||||||
onlinePluginNoSubmittedVersion: "尚未提交过发布版本",
|
|
||||||
onlinePluginVersionFormatError: "版本号只能使用数字点分格式,例如 1.0.0",
|
|
||||||
onlinePluginVersionBaselineError: "请输入大于 v{version} 的版本号",
|
|
||||||
onlinePluginStatusDraft: "草稿",
|
|
||||||
onlinePluginStatusReviewing: "审核中",
|
|
||||||
onlinePluginStatusPublished: "已发布",
|
|
||||||
onlinePluginStatusRejected: "已拒绝",
|
|
||||||
onlinePluginStatusOffline: "已下架",
|
|
||||||
onlinePluginReviewAiPending: "待 AI 审查",
|
|
||||||
onlinePluginReviewPending: "待人工审核",
|
|
||||||
onlinePluginReviewPassed: "已通过",
|
|
||||||
onlinePluginReviewRejected: "已拒绝",
|
|
||||||
onlinePluginAuthorRegister: "注册插件作者",
|
|
||||||
onlinePluginAuthorName: "作者名称",
|
|
||||||
onlinePluginAuthorDisplayName: "显示名称",
|
|
||||||
onlinePluginAuthorNotRegistered: "未注册",
|
|
||||||
onlinePluginAuthorNameRequired: "请输入作者名称",
|
|
||||||
onlinePluginAuthorNameHelper: "作为插件市场作者前缀,同一个绑定用户只能注册一个作者",
|
|
||||||
onlinePluginAuthorNameRuleMsg: "必须以英文字母开头,只能包含英文字母、数字、中划线或下划线",
|
|
||||||
pleaseSelectRecord: "请先勾选记录",
|
pleaseSelectRecord: "请先勾选记录",
|
||||||
clearRuntimeDeps: "清理第三方依赖缓存",
|
clearRuntimeDeps: "清理第三方依赖缓存",
|
||||||
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
||||||
|
|||||||
@@ -103,5 +103,4 @@ export default {
|
|||||||
not_effective: "VIP没有生效/时长未同步?",
|
not_effective: "VIP没有生效/时长未同步?",
|
||||||
learn_more: "更多特权(加VIP群等)",
|
learn_more: "更多特权(加VIP群等)",
|
||||||
question: "更多VIP相关问题",
|
question: "更多VIP相关问题",
|
||||||
permanent: "永久",
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -287,22 +287,6 @@ export const certdResources = [
|
|||||||
isMenu: true,
|
isMenu: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "certd.auditLog",
|
|
||||||
name: "AuditLog",
|
|
||||||
path: "/certd/audit",
|
|
||||||
component: "/certd/audit/index.vue",
|
|
||||||
meta: {
|
|
||||||
icon: "ion:document-text-outline",
|
|
||||||
auth: true,
|
|
||||||
keepAlive: true,
|
|
||||||
isMenu: true,
|
|
||||||
show: () => {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
return settingStore.isPlus;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "certd.userSecurity",
|
title: "certd.userSecurity",
|
||||||
name: "UserSecurity",
|
name: "UserSecurity",
|
||||||
|
|||||||
@@ -154,6 +154,19 @@ export const sysResources = [
|
|||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "certd.sysResources.sysPluginEdit",
|
||||||
|
name: "SysPluginEdit",
|
||||||
|
path: "/sys/plugin/edit",
|
||||||
|
component: "/sys/plugin/edit.vue",
|
||||||
|
meta: {
|
||||||
|
isMenu: false,
|
||||||
|
icon: "ion:extension-puzzle",
|
||||||
|
permission: "sys:settings:view",
|
||||||
|
keepAlive: true,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "certd.sysResources.sysPluginConfig",
|
title: "certd.sysResources.sysPluginConfig",
|
||||||
name: "SysPluginConfig",
|
name: "SysPluginConfig",
|
||||||
@@ -397,22 +410,6 @@ export const sysResources = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "certd.sysResources.auditLog",
|
|
||||||
name: "SysAuditLog",
|
|
||||||
path: "/sys/enterprise/audit",
|
|
||||||
component: "/sys/enterprise/audit/index.vue",
|
|
||||||
meta: {
|
|
||||||
icon: "ion:document-text-outline",
|
|
||||||
keepAlive: true,
|
|
||||||
auth: true,
|
|
||||||
isMenu: true,
|
|
||||||
show: () => {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
return settingStore.isPlus;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "certd.sysResources.netTest",
|
title: "certd.sysResources.netTest",
|
||||||
name: "NetTest",
|
name: "NetTest",
|
||||||
|
|||||||
@@ -153,22 +153,6 @@ export class PluginGroups {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterInstalledPluginGroups(groups: { [key: string]: PluginGroup }) {
|
|
||||||
const filteredGroups: { [key: string]: PluginGroup } = {};
|
|
||||||
for (const [key, group] of Object.entries(groups)) {
|
|
||||||
filteredGroups[key] = {
|
|
||||||
...group,
|
|
||||||
plugins: group.plugins.filter(plugin => {
|
|
||||||
if (plugin.type !== "store") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return plugin.installed === true;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return filteredGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePluginStore = defineStore({
|
export const usePluginStore = defineStore({
|
||||||
id: "app.plugin",
|
id: "app.plugin",
|
||||||
state: (): PluginState => ({
|
state: (): PluginState => ({
|
||||||
@@ -178,9 +162,8 @@ export const usePluginStore = defineStore({
|
|||||||
actions: {
|
actions: {
|
||||||
async reload() {
|
async reload() {
|
||||||
const groups = await api.GetGroups({});
|
const groups = await api.GetGroups({});
|
||||||
const installedGroups = filterInstalledPluginGroups(groups);
|
this.group = new PluginGroups(groups, { mergeSetting: true });
|
||||||
this.group = new PluginGroups(installedGroups, { mergeSetting: true });
|
this.originGroup = new PluginGroups(cloneDeep(groups));
|
||||||
this.originGroup = new PluginGroups(cloneDeep(installedGroups));
|
|
||||||
console.log("group", this.group);
|
console.log("group", this.group);
|
||||||
console.log("originGroup", this.originGroup);
|
console.log("originGroup", this.originGroup);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type AppInfo = {
|
|||||||
version?: string;
|
version?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
deltaTime?: number;
|
deltaTime?: number;
|
||||||
releaseMode?: string;
|
|
||||||
};
|
};
|
||||||
export type SiteInfo = {
|
export type SiteInfo = {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export interface SettingState {
|
|||||||
version?: string;
|
version?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
deltaTime?: number;
|
deltaTime?: number;
|
||||||
releaseMode?: string;
|
|
||||||
};
|
};
|
||||||
productInfo: {
|
productInfo: {
|
||||||
notice?: string;
|
notice?: string;
|
||||||
@@ -110,7 +109,6 @@ export const useSettingStore = defineStore({
|
|||||||
version: "",
|
version: "",
|
||||||
time: 0,
|
time: 0,
|
||||||
deltaTime: 0,
|
deltaTime: 0,
|
||||||
releaseMode: "latest",
|
|
||||||
},
|
},
|
||||||
productInfo: {
|
productInfo: {
|
||||||
notice: "",
|
notice: "",
|
||||||
@@ -231,9 +229,6 @@ export const useSettingStore = defineStore({
|
|||||||
this.app.time = appInfo.time;
|
this.app.time = appInfo.time;
|
||||||
this.app.version = appInfo.version;
|
this.app.version = appInfo.version;
|
||||||
this.app.deltaTime = new Date().getTime() - this.app.time;
|
this.app.deltaTime = new Date().getTime() - this.app.time;
|
||||||
if (appInfo.releaseMode) {
|
|
||||||
this.app.releaseMode = appInfo.releaseMode;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
initSiteInfo(siteInfo: SiteInfo) {
|
initSiteInfo(siteInfo: SiteInfo) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
|||||||
@@ -75,9 +75,3 @@ footer {
|
|||||||
.ant-input-number {
|
.ant-input-number {
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-dropdown-menu-title-content{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
@@ -36,116 +36,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cd-card-section {
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid #e8edf3;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-card-section-title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #1f2937;
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
&.cd-card-section-title--compact {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-meta-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-meta-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 72px minmax(0, 1fr);
|
|
||||||
align-items: start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-meta-label {
|
|
||||||
color: #7b8794;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-meta-value {
|
|
||||||
min-width: 0;
|
|
||||||
color: #1f2937;
|
|
||||||
font-weight: 500;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-tip-box {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #e8edf3;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f6f8fb;
|
|
||||||
color: #526172;
|
|
||||||
line-height: 22px;
|
|
||||||
|
|
||||||
&.cd-tip-box-warning {
|
|
||||||
border-color: #ffd591;
|
|
||||||
background: #fff7e6;
|
|
||||||
color: #ad6800;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cd-tip-box-danger {
|
|
||||||
border-color: #ffccc7;
|
|
||||||
background: #fff2f0;
|
|
||||||
color: #cf1322;
|
|
||||||
line-height: 21px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-tip-title {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 82px minmax(0, 1fr);
|
|
||||||
align-items: start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-form-label {
|
|
||||||
padding-top: 5px;
|
|
||||||
color: #5f6b7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-text-input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 32px;
|
|
||||||
padding: 4px 11px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #1f2937;
|
|
||||||
|
|
||||||
&.cd-text-input-error {
|
|
||||||
border-color: #ff4d4f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-field-error {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: #cf1322;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-text-danger {
|
|
||||||
color: #cf1322;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-text-muted {
|
|
||||||
color: #8c8c8c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-content {
|
.ant-drawer-content {
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -250,9 +140,3 @@ button.ant-btn.ant-btn-default.isPlus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-form-none-content {
|
|
||||||
.fs-form-wrapper{
|
|
||||||
.fs-form-content{display: none;}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ export type FormOptionReq = {
|
|||||||
initialForm?: any;
|
initialForm?: any;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
wrapper?: any;
|
wrapper?: any;
|
||||||
noneForm?: boolean; //是否隐藏表单,只显示注入的body
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useFormDialog() {
|
export function useFormDialog() {
|
||||||
const { openCrudFormDialog } = useFormWrapper();
|
const { openCrudFormDialog } = useFormWrapper();
|
||||||
|
|
||||||
async function openFormDialog(req: FormOptionReq) {
|
async function openFormDialog(req: FormOptionReq) {
|
||||||
const noneForm = req.noneForm ?? Object.keys(req.columns).length === 0;
|
|
||||||
function createCrudOptions() {
|
function createCrudOptions() {
|
||||||
const warpper = merge(
|
const warpper = merge(
|
||||||
{
|
{
|
||||||
@@ -26,7 +24,6 @@ export function useFormDialog() {
|
|||||||
slots: {
|
slots: {
|
||||||
"form-body-top": req.body,
|
"form-body-top": req.body,
|
||||||
},
|
},
|
||||||
wrapClassName: noneForm ? "fs-form-none-content" : "",
|
|
||||||
},
|
},
|
||||||
req.wrapper
|
req.wrapper
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { request } from "/src/api/service";
|
|
||||||
|
|
||||||
const apiPrefix = "/pi/audit";
|
|
||||||
|
|
||||||
export function createAuditApi() {
|
|
||||||
return {
|
|
||||||
async GetList(query: any) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/page",
|
|
||||||
method: "post",
|
|
||||||
data: query,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async DelObj(id: number) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/delete",
|
|
||||||
method: "post",
|
|
||||||
params: { id },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async GetDict() {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/dict",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { ColumnProps, DataFormatterContext, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, UserPageQuery, UserPageRes, dict } from "@fast-crud/fast-crud";
|
|
||||||
import { useI18n } from "/src/locales";
|
|
||||||
import { useDicts } from "../dicts";
|
|
||||||
|
|
||||||
const typeDict = dict({
|
|
||||||
url: "/pi/audit/dict",
|
|
||||||
getData: async () => {
|
|
||||||
const { createAuditApi } = await import("./api");
|
|
||||||
const api = createAuditApi();
|
|
||||||
const res = await api.GetDict();
|
|
||||||
return res.types || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { myProjectDict } = useDicts();
|
|
||||||
const api = context.api;
|
|
||||||
|
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
|
||||||
return await api.GetList(query);
|
|
||||||
};
|
|
||||||
const delRequest = async (req: DelReq) => {
|
|
||||||
return await api.DelObj(req.row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
crudOptions: {
|
|
||||||
request: { pageRequest, delRequest },
|
|
||||||
toolbar: {
|
|
||||||
buttons: {
|
|
||||||
export: { show: true },
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
dataFrom: "search",
|
|
||||||
columnFilter: (col: ColumnProps) => col.show === true,
|
|
||||||
dataFormatter: (opts: DataFormatterContext) => {
|
|
||||||
const { row, originalRow, col } = opts;
|
|
||||||
const key = col.key;
|
|
||||||
if (key === "createTime" && originalRow[key]) {
|
|
||||||
row[key] = new Date(originalRow[key]).toLocaleString();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actionbar: {
|
|
||||||
buttons: {
|
|
||||||
add: { show: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowHandle: {
|
|
||||||
width: 120,
|
|
||||||
fixed: "right",
|
|
||||||
buttons: {
|
|
||||||
view: { show: false },
|
|
||||||
edit: { show: false },
|
|
||||||
remove: { show: true },
|
|
||||||
copy: { show: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
id: {
|
|
||||||
title: "ID",
|
|
||||||
type: "number",
|
|
||||||
column: { width: 80 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
createTime: {
|
|
||||||
title: "操作时间",
|
|
||||||
type: "datetime",
|
|
||||||
search: {
|
|
||||||
show: true,
|
|
||||||
component: {
|
|
||||||
name: "a-range-picker",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
column: { width: 170, sorter: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
title: "操作类型",
|
|
||||||
type: "dict-select",
|
|
||||||
dict: typeDict,
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 120 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title: "操作动作",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 200, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
title: "结果",
|
|
||||||
type: "dict-switch",
|
|
||||||
dict: dict({
|
|
||||||
data: [
|
|
||||||
{ value: true, label: "成功", color: "success" },
|
|
||||||
{ value: false, label: "失败", color: "error" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
column: { width: 100, align: "center" },
|
|
||||||
form: { show: false },
|
|
||||||
search: { show: true },
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
title: "备注",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 700, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
ipAddress: {
|
|
||||||
title: "IP地址",
|
|
||||||
type: "text",
|
|
||||||
column: { width: 140 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
projectId: {
|
|
||||||
title: t("certd.fields.projectName"),
|
|
||||||
type: "dict-select",
|
|
||||||
dict: myProjectDict,
|
|
||||||
form: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<fs-page>
|
|
||||||
<template #header>
|
|
||||||
<div class="title">
|
|
||||||
操作日志
|
|
||||||
<span class="sub">查看您的操作记录</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
|
||||||
</fs-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useFs } from "@fast-crud/fast-crud";
|
|
||||||
import createCrudOptions from "./crud";
|
|
||||||
import { createAuditApi } from "./api";
|
|
||||||
import { useMounted } from "/@/use/use-mounted";
|
|
||||||
|
|
||||||
defineOptions({ name: "AuditLog" });
|
|
||||||
|
|
||||||
const api = createAuditApi();
|
|
||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
|
|
||||||
useMounted(() => crudExpose.doRefresh());
|
|
||||||
</script>
|
|
||||||
+119
-140
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
|
<a-drawer v-model:open="stepDrawerVisible" :wrap-style="{ maxWidth: '100vw' }" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div>
|
<div>
|
||||||
编辑步骤
|
编辑步骤
|
||||||
@@ -12,25 +12,63 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<a-space>
|
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
||||||
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="currentStep">
|
<template v-if="currentStep">
|
||||||
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
||||||
|
<template #header>
|
||||||
|
<a-row :gutter="10" class="mb-10">
|
||||||
|
<a-col :span="24" style="padding-left: 20px">
|
||||||
|
<a-input-search v-model:value="pluginSearch.keyword" placeholder="搜索插件" :allow-clear="true" :show-search="true"></a-input-search>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
<div class="flex-col h-100 overflow-hidden md:ml-5 md:mr-5 step-form-body">
|
<div class="flex-col h-100 overflow-hidden md:ml-5 md:mr-5 step-form-body">
|
||||||
<a-tabs v-model:active-key="pluginSourceActive" class="step-plugin-source-tabs flex-1 overflow-hidden h-full">
|
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="flex-1 overflow-hidden h-full">
|
||||||
<a-tab-pane key="local" tab="已安装插件" class="h-full">
|
<template v-for="group of computedPluginGroups" :key="group.key">
|
||||||
<LocalPluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" />
|
<a-tab-pane v-if="(group.key === 'admin' && userStore.isAdmin) || group.key !== 'admin'" :key="group.key" class="scroll-y">
|
||||||
</a-tab-pane>
|
<template #tab>
|
||||||
<a-tab-pane v-if="userStore.isAdmin" key="market" tab="插件市场" class="h-full step-market-pane">
|
<div class="cd-step-form-tab-label">
|
||||||
<OnlinePluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" @uninstalled="handleOnlinePluginUninstalled" />
|
<fs-icon :icon="group.icon" class="mr-2" />
|
||||||
</a-tab-pane>
|
<div>{{ group.title }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-row v-if="!group.plugins || group.plugins.length === 0" :gutter="10">
|
||||||
|
<a-col class="flex-o">
|
||||||
|
<div class="flex-o m-10">没有找到插件</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row v-else :gutter="10">
|
||||||
|
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin w-full md:w-[50%]">
|
||||||
|
<a-card
|
||||||
|
hoverable
|
||||||
|
:class="{ current: item.name === currentStep.type }"
|
||||||
|
@click="stepTypeSelected(item)"
|
||||||
|
@dblclick="
|
||||||
|
stepTypeSelected(item);
|
||||||
|
stepTypeSave();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-card-meta>
|
||||||
|
<template #title>
|
||||||
|
<fs-icon class="plugin-icon" :icon="item.icon || 'clarity:plugin-line'"></fs-icon>
|
||||||
|
<span class="title" :title="item.title">{{ item.title }}</span>
|
||||||
|
<vip-button v-if="item.needPlus" mode="icon" />
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<span :title="item.desc" v-html="transformDesc(item.desc)"></span>
|
||||||
|
</template>
|
||||||
|
</a-card-meta>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-tab-pane>
|
||||||
|
</template>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="bottom-button">
|
<div style="padding: 20px; margin-left: 100px">
|
||||||
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定</a-button>
|
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,30 +117,28 @@
|
|||||||
|
|
||||||
<script lang="tsx" setup>
|
<script lang="tsx" setup>
|
||||||
import { message, Modal } from "ant-design-vue";
|
import { message, Modal } from "ant-design-vue";
|
||||||
import { provide, ref, Ref } from "vue";
|
import { computed, provide, ref, Ref, watch } from "vue";
|
||||||
import { merge, cloneDeep } from "lodash-es";
|
import { merge, cloneDeep } from "lodash-es";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
import { usePluginStore, PluginGroups } from "/@/store/plugin";
|
||||||
import { useCompute } from "@fast-crud/fast-crud";
|
import { useCompute } from "@fast-crud/fast-crud";
|
||||||
import { useReference } from "/@/use/use-refrence";
|
import { useReference } from "/@/use/use-refrence";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import { mitter } from "/@/utils/util.mitt";
|
import { mitter } from "/@/utils/util.mitt";
|
||||||
import { utils } from "/@/utils";
|
import { utils } from "/@/utils";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
import LocalPluginSelector from "./local-plugin-selector.vue";
|
|
||||||
import OnlinePluginSelector from "./online-plugin-selector.vue";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PiStepForm",
|
name: "PiStepForm",
|
||||||
});
|
});
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["update"]);
|
const emit = defineEmits(["update"]);
|
||||||
|
|
||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -135,9 +171,6 @@ function useStepForm() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stepTypeSelected = (item: any) => {
|
const stepTypeSelected = (item: any) => {
|
||||||
if (item.__online) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.needPlus && !settingStore.isPlus) {
|
if (item.needPlus && !settingStore.isPlus) {
|
||||||
message.warn("此插件需要开通Certd专业版才能使用");
|
message.warn("此插件需要开通Certd专业版才能使用");
|
||||||
mitter.emit("openVipModal");
|
mitter.emit("openVipModal");
|
||||||
@@ -306,24 +339,55 @@ function useStepForm() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const pluginSourceActive = ref("local");
|
const pluginSearch = ref({
|
||||||
const handlePluginConfirm = async (item: any) => {
|
keyword: "",
|
||||||
stepTypeSelected(item);
|
result: [],
|
||||||
await stepTypeSave();
|
});
|
||||||
};
|
const pluginGroupActive = ref("all");
|
||||||
|
const pluginGroup: Ref = ref();
|
||||||
|
const pluginStore = usePluginStore();
|
||||||
|
|
||||||
const handleOnlinePluginUninstalled = (plugin: any) => {
|
async function loadPluginGroups() {
|
||||||
if (currentStep.value.type === plugin.fullName) {
|
pluginGroup.value = await pluginStore.getGroups();
|
||||||
currentStep.value.type = undefined;
|
}
|
||||||
currentStep.value.title = "新任务";
|
|
||||||
|
loadPluginGroups();
|
||||||
|
const computedPluginGroups: any = computed(() => {
|
||||||
|
if (!pluginGroup.value) {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
};
|
const group = pluginGroup.value as PluginGroups;
|
||||||
|
const groups = group.groups;
|
||||||
|
if (pluginSearch.value.keyword) {
|
||||||
|
const keyword = pluginSearch.value.keyword.toLowerCase();
|
||||||
|
const list = groups.all.plugins.filter((plugin: any) => {
|
||||||
|
return plugin.title?.toLowerCase().includes(keyword) || plugin.desc?.toLowerCase().includes(keyword) || plugin.name?.toLowerCase().includes(keyword);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
search: { key: "search", title: "搜索结果", plugins: list },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return pluginSearch.value.keyword;
|
||||||
|
},
|
||||||
|
(val: any) => {
|
||||||
|
if (val) {
|
||||||
|
pluginGroupActive.value = "search";
|
||||||
|
} else {
|
||||||
|
pluginGroupActive.value = "all";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pluginSourceActive,
|
pluginGroupActive,
|
||||||
|
computedPluginGroups,
|
||||||
|
pluginSearch,
|
||||||
stepTypeSelected,
|
stepTypeSelected,
|
||||||
handlePluginConfirm,
|
|
||||||
handleOnlinePluginUninstalled,
|
|
||||||
stepTypeSave,
|
stepTypeSave,
|
||||||
stepFormRef,
|
stepFormRef,
|
||||||
mode,
|
mode,
|
||||||
@@ -372,34 +436,34 @@ const labelCol = ref({ span: 6 });
|
|||||||
const wrapperCol = ref({ span: 16 });
|
const wrapperCol = ref({ span: 16 });
|
||||||
|
|
||||||
const stepFormRes = useStepForm();
|
const stepFormRes = useStepForm();
|
||||||
const {
|
const { pluginGroupActive, computedPluginGroups, pluginSearch, stepTypeSelected, stepTypeSave, stepFormRef, stepDrawerVisible, currentStep, currentPlugin, stepSave, stepDelete, getScopeFunc, fullscreen } = stepFormRes;
|
||||||
pluginSourceActive,
|
|
||||||
stepTypeSelected,
|
|
||||||
handlePluginConfirm,
|
|
||||||
handleOnlinePluginUninstalled,
|
|
||||||
stepTypeSave,
|
|
||||||
stepFormRef,
|
|
||||||
stepDrawerVisible,
|
|
||||||
currentStep,
|
|
||||||
currentPlugin,
|
|
||||||
stepSave,
|
|
||||||
stepDelete,
|
|
||||||
getScopeFunc,
|
|
||||||
fullscreen,
|
|
||||||
} = stepFormRes;
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
...stepFormRes,
|
...stepFormRes,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
.cd-step-form-tab-label {
|
||||||
|
// 包括dropdown
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
//width: 120px;
|
||||||
|
.fs-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #00b7ff;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.step-form-drawer {
|
.step-form-drawer {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
.ant-drawer-content-wrapper {
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-nav .ant-tabs-tab {
|
.ant-tabs-nav .ant-tabs-tab {
|
||||||
margin-top: 10px !important;
|
margin-top: 10px !important;
|
||||||
padding: 8px 14px !important;
|
padding: 8px 14px !important;
|
||||||
@@ -438,83 +502,6 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pi-step-form {
|
.pi-step-form {
|
||||||
.step-plugin-source-pane-local,
|
|
||||||
.step-plugin-source-pane-online {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-plugin-search {
|
|
||||||
flex: none;
|
|
||||||
padding: 0 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-plugin-selector-tabs {
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
> .ant-tabs-nav {
|
|
||||||
width: 136px;
|
|
||||||
flex: 0 0 136px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ant-tabs-nav .ant-tabs-nav-wrap,
|
|
||||||
> .ant-tabs-nav .ant-tabs-nav-list {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ant-tabs-nav .ant-tabs-tab {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-step-form-tab-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #00b7ff;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: middle !important;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> div:last-child {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ant-tabs-content-holder {
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ant-tabs-content-holder > .ant-tabs-content {
|
|
||||||
height: auto;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane {
|
|
||||||
padding-right: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-button {
|
.bottom-button {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
@@ -527,10 +514,6 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
|
||||||
width: 22px;
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
@@ -559,9 +542,7 @@ defineExpose({
|
|||||||
.ant-card-meta-title {
|
.ant-card-meta-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
min-height: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-avatar {
|
.ant-avatar {
|
||||||
@@ -577,21 +558,19 @@ defineExpose({
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
line-height: 22px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
min-height: 100px;
|
height: 100px;
|
||||||
padding-bottom: 6px;
|
|
||||||
|
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
.ant-card-meta-description {
|
.ant-card-meta-description {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
min-height: 40px;
|
height: 40px;
|
||||||
color: #7f7f7f;
|
color: #7f7f7f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="step-plugin-source-pane-local">
|
|
||||||
<div class="step-plugin-search">
|
|
||||||
<a-input-search v-model:value="localPluginSearch.keyword" placeholder="搜索本地插件" :allow-clear="true" :show-search="true"></a-input-search>
|
|
||||||
</div>
|
|
||||||
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="step-plugin-selector-tabs flex-1 overflow-hidden h-full">
|
|
||||||
<a-tab-pane v-for="group of computedLocalPluginGroups" :key="group.key" class="step-plugin-list-pane">
|
|
||||||
<template #tab>
|
|
||||||
<div class="cd-step-form-tab-label" @click="handleLocalPluginGroupChange(group.key)">
|
|
||||||
<fs-icon :icon="group.icon" class="mr-2" />
|
|
||||||
<div>{{ group.title }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="!group.plugins || group.plugins.length === 0" class="step-plugin-empty">没有找到插件</div>
|
|
||||||
<a-row v-else :gutter="10">
|
|
||||||
<a-col v-for="item of group.plugins" :key="item.key || item.name" class="step-plugin w-full md:w-[50%]">
|
|
||||||
<a-card hoverable :class="{ current: item.name === selectedType }" @click="emit('select', item)" @dblclick="emit('confirm', item)">
|
|
||||||
<a-card-meta>
|
|
||||||
<template #title>
|
|
||||||
<fs-icon class="plugin-icon" :icon="item.icon || 'clarity:plugin-line'"></fs-icon>
|
|
||||||
<span class="title" :title="item.title">{{ item.title }}</span>
|
|
||||||
<vip-button v-if="item.needPlus" mode="icon" />
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
<div class="plugin-card-desc" :title="item.desc" v-html="transformDesc(item.desc)"></div>
|
|
||||||
</template>
|
|
||||||
</a-card-meta>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, Ref, watch } from "vue";
|
|
||||||
import { PluginGroups, usePluginStore } from "/@/store/plugin";
|
|
||||||
import { useUserStore } from "/@/store/user";
|
|
||||||
import { utils } from "/@/utils";
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
selectedType: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "select", plugin: any): void;
|
|
||||||
(e: "confirm", plugin: any): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const pluginGroupActive = ref("all");
|
|
||||||
const pluginGroup: Ref<PluginGroups | undefined> = ref();
|
|
||||||
const localPluginSearch = ref({
|
|
||||||
keyword: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
function transformDesc(desc: string = "") {
|
|
||||||
return utils.transformLink(desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchKeyword(plugin: any, keyword: string) {
|
|
||||||
const fields = [plugin.title, plugin.desc, plugin.name, plugin.fullName, plugin.author, plugin.latest];
|
|
||||||
return fields.some(field =>
|
|
||||||
String(field || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(keyword)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPluginGroups() {
|
|
||||||
pluginGroup.value = await pluginStore.getGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLocalPluginGroupChange(groupKey: string) {
|
|
||||||
pluginGroupActive.value = groupKey;
|
|
||||||
localPluginSearch.value.keyword = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedLocalPluginGroups: any = computed(() => {
|
|
||||||
if (!pluginGroup.value) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const groups = pluginGroup.value.groups;
|
|
||||||
const keyword = localPluginSearch.value.keyword.trim().toLowerCase();
|
|
||||||
const visibleGroups: any = {};
|
|
||||||
for (const groupKey of Object.keys(groups)) {
|
|
||||||
if (groupKey === "admin" && !userStore.isAdmin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
visibleGroups[groupKey] = groups[groupKey];
|
|
||||||
}
|
|
||||||
if (!keyword) {
|
|
||||||
return visibleGroups;
|
|
||||||
}
|
|
||||||
const filteredGroups: any = {};
|
|
||||||
for (const groupKey of Object.keys(visibleGroups)) {
|
|
||||||
const currentGroup = visibleGroups[groupKey];
|
|
||||||
filteredGroups[groupKey] = {
|
|
||||||
...currentGroup,
|
|
||||||
plugins: currentGroup.plugins.filter((plugin: any) => {
|
|
||||||
return matchKeyword(plugin, keyword);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return filteredGroups;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
return localPluginSearch.value.keyword;
|
|
||||||
},
|
|
||||||
val => {
|
|
||||||
if (val) {
|
|
||||||
pluginGroupActive.value = "all";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
loadPluginGroups();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.step-plugin-source-pane-local {
|
|
||||||
.cd-step-form-tab-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #00b7ff;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: middle !important;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-plugin-source-pane {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-plugin-search {
|
|
||||||
flex: none;
|
|
||||||
padding: 0 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-plugin-empty {
|
|
||||||
display: flex;
|
|
||||||
min-height: 320px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #7b8794;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-desc {
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
-376
@@ -1,376 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="step-plugin-source-pane-online">
|
|
||||||
<div class="step-plugin-search">
|
|
||||||
<a-input-search v-model:value="onlinePluginSearch.keyword" placeholder="搜索插件市场" :allow-clear="true" :show-search="true" :loading="onlineLoading" @search="handleOnlinePluginSearch"></a-input-search>
|
|
||||||
</div>
|
|
||||||
<a-tabs v-model:active-key="onlinePluginGroupActive" tab-position="left" class="step-plugin-selector-tabs step-market-tabs flex-1 overflow-hidden h-full">
|
|
||||||
<a-tab-pane v-for="group of computedOnlinePluginGroups" :key="group.key" class="step-plugin-list-pane">
|
|
||||||
<template #tab>
|
|
||||||
<div class="cd-step-form-tab-label" @click="handleOnlinePluginGroupChange(group.key)">
|
|
||||||
<fs-icon :icon="group.icon" class="mr-2" />
|
|
||||||
<div>{{ group.title }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="onlineLoading" class="step-market-state">正在加载插件市场...</div>
|
|
||||||
<div v-else-if="!group.plugins || group.plugins.length === 0" class="step-plugin-empty">没有找到插件</div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="step-market-content">
|
|
||||||
<a-row :gutter="[12, 12]">
|
|
||||||
<a-col v-for="item of group.plugins" :key="item.key || item.fullName" class="step-plugin market-plugin-col w-full md:w-[50%]">
|
|
||||||
<PluginItemCard :plugin="item" simple :current="isOnlinePluginCurrent(item)" @click="onlinePluginSelected(item)" @dblclick="handleOnlinePluginCardDblclick(item)" @changed="handleOnlinePluginChanged" />
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<div class="online-plugin-pagination">
|
|
||||||
<a-pagination v-model:current="onlinePage" size="small" :page-size="onlinePageSize" :total="onlineCurrentTotal" :show-size-changer="false" simple />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, Ref, watch } from "vue";
|
|
||||||
import { PluginGroups, usePluginStore } from "/@/store/plugin";
|
|
||||||
import { useUserStore } from "/@/store/user";
|
|
||||||
import PluginItemCard from "/@/views/sys/plugin/components/plugin-item-card.vue";
|
|
||||||
import * as pluginApi from "/@/views/sys/plugin/api";
|
|
||||||
import { useI18n } from "/src/locales";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
selectedType: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "select", plugin: any): void;
|
|
||||||
(e: "confirm", plugin: any): void;
|
|
||||||
(e: "uninstalled", plugin: any): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const pluginGroup: Ref<PluginGroups | undefined> = ref();
|
|
||||||
const onlinePlugins: Ref<pluginApi.OnlinePluginBean[]> = ref([]);
|
|
||||||
const onlineGroupPlugins: Ref<pluginApi.OnlinePluginBean[]> = ref([]);
|
|
||||||
const onlineLoading: Ref<boolean> = ref(false);
|
|
||||||
const onlineGroupLoaded: Ref<boolean> = ref(false);
|
|
||||||
const onlinePluginSearch = ref({
|
|
||||||
keyword: "",
|
|
||||||
});
|
|
||||||
const onlinePluginQueryKeyword = ref("");
|
|
||||||
const onlinePluginGroupActive = ref("all");
|
|
||||||
const onlinePage = ref(1);
|
|
||||||
const onlinePageSize = 8;
|
|
||||||
let onlineRequestId = 0;
|
|
||||||
|
|
||||||
async function loadPluginGroups() {
|
|
||||||
pluginGroup.value = await pluginStore.getGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOnlinePluginListReq(req?: { groupKey?: string; keyword?: string }) {
|
|
||||||
const groupKey = req?.groupKey ?? onlinePluginGroupActive.value;
|
|
||||||
const keyword = (req?.keyword ?? onlinePluginQueryKeyword.value).trim();
|
|
||||||
const query: { pluginType: string; group?: string; keyword?: string } = {
|
|
||||||
pluginType: "deploy",
|
|
||||||
};
|
|
||||||
if (groupKey && groupKey !== "all") {
|
|
||||||
query.group = groupKey;
|
|
||||||
}
|
|
||||||
if (keyword) {
|
|
||||||
query.keyword = keyword;
|
|
||||||
}
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOnlinePluginGroups(force = false) {
|
|
||||||
if (!userStore.isAdmin) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (onlineGroupLoaded.value && !force) {
|
|
||||||
return onlineGroupPlugins.value;
|
|
||||||
}
|
|
||||||
const list = await pluginApi.OnlinePluginList({ pluginType: "deploy" });
|
|
||||||
onlineGroupPlugins.value = list;
|
|
||||||
onlineGroupLoaded.value = true;
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOnlinePlugins(force = false, options?: { silent?: boolean }) {
|
|
||||||
if (!userStore.isAdmin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const requestId = ++onlineRequestId;
|
|
||||||
if (!options?.silent) {
|
|
||||||
onlineLoading.value = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const groupPlugins = await loadOnlinePluginGroups(force);
|
|
||||||
if (requestId !== onlineRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const query = getOnlinePluginListReq();
|
|
||||||
const isFullQuery = !query.group && !query.keyword;
|
|
||||||
if (isFullQuery) {
|
|
||||||
onlinePlugins.value = groupPlugins;
|
|
||||||
} else {
|
|
||||||
const list = await pluginApi.OnlinePluginList(query);
|
|
||||||
if (requestId !== onlineRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onlinePlugins.value = list;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!options?.silent && requestId === onlineRequestId) {
|
|
||||||
onlineLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOnlinePluginDisplayName(plugin: pluginApi.OnlinePluginBean) {
|
|
||||||
if (plugin.fullName) {
|
|
||||||
return plugin.fullName;
|
|
||||||
}
|
|
||||||
if (plugin.author && plugin.name) {
|
|
||||||
return `${plugin.author}/${plugin.name}`;
|
|
||||||
}
|
|
||||||
return plugin.name || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildOnlinePluginCard(plugin: pluginApi.OnlinePluginBean) {
|
|
||||||
const fullName = getOnlinePluginDisplayName(plugin);
|
|
||||||
return {
|
|
||||||
...plugin,
|
|
||||||
key: `online:${fullName}`,
|
|
||||||
name: fullName,
|
|
||||||
fullName,
|
|
||||||
title: plugin.title || plugin.name || fullName,
|
|
||||||
desc: plugin.desc,
|
|
||||||
icon: plugin.icon || "clarity:plugin-line",
|
|
||||||
__online: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOnlinePluginGroupKey(plugin: pluginApi.OnlinePluginBean) {
|
|
||||||
return (plugin.group || "other").trim() || "other";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOnlinePluginGroupMeta(groupKey: string) {
|
|
||||||
const localGroups = pluginGroup.value?.groups || {};
|
|
||||||
const localGroup = localGroups[groupKey];
|
|
||||||
if (localGroup) {
|
|
||||||
return {
|
|
||||||
title: localGroup.title,
|
|
||||||
icon: localGroup.icon,
|
|
||||||
order: localGroup.order,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (groupKey === "other") {
|
|
||||||
return {
|
|
||||||
title: "其他",
|
|
||||||
icon: "clarity:plugin-line",
|
|
||||||
order: 9999,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: groupKey,
|
|
||||||
icon: "clarity:plugin-line",
|
|
||||||
order: 1000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilteredOnlinePlugins() {
|
|
||||||
return onlinePlugins.value.map(plugin => buildOnlinePluginCard(plugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOnlinePluginsByGroup(groupKey: string) {
|
|
||||||
const list = getFilteredOnlinePlugins();
|
|
||||||
if (groupKey === "all") {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
return list.filter(plugin => getOnlinePluginGroupKey(plugin) === groupKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPagedOnlinePlugins(groupKey: string) {
|
|
||||||
const list = getOnlinePluginsByGroup(groupKey);
|
|
||||||
const start = (onlinePage.value - 1) * onlinePageSize;
|
|
||||||
return list.slice(start, start + onlinePageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleOnlinePluginChanged(payload: { plugin: any; action: string }) {
|
|
||||||
if (payload.action === "uninstall" && props.selectedType === payload.plugin.fullName) {
|
|
||||||
emit("uninstalled", payload.plugin);
|
|
||||||
}
|
|
||||||
if (payload.action === "install" || payload.action === "uninstall") {
|
|
||||||
await pluginStore.reload();
|
|
||||||
}
|
|
||||||
await loadPluginGroups();
|
|
||||||
await loadOnlinePlugins(true, { silent: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInstalledPlugin(plugin: any) {
|
|
||||||
if (!plugin.installed || !plugin.fullName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const groups = pluginGroup.value;
|
|
||||||
if (!groups) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const candidateNames = [plugin.fullName, plugin.name, plugin.author && plugin.name ? `${plugin.author}/${plugin.name}` : ""].filter(Boolean);
|
|
||||||
for (const name of candidateNames) {
|
|
||||||
const installedPlugin = groups.get(name);
|
|
||||||
if (installedPlugin) {
|
|
||||||
return installedPlugin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups.groups.all.plugins.find((item: any) => {
|
|
||||||
if (plugin.localPluginId && item.id === plugin.localPluginId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return item.author === plugin.author && item.name === plugin.name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOnlinePluginCurrent(plugin: any) {
|
|
||||||
const installedPlugin = getInstalledPlugin(plugin);
|
|
||||||
return !!installedPlugin && installedPlugin.name === props.selectedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onlinePluginSelected(plugin: any) {
|
|
||||||
const installedPlugin = getInstalledPlugin(plugin);
|
|
||||||
if (!installedPlugin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit("select", installedPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOnlinePluginCardDblclick(plugin: any) {
|
|
||||||
const installedPlugin = getInstalledPlugin(plugin);
|
|
||||||
if (!installedPlugin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit("confirm", installedPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOnlinePluginGroupChange(groupKey: string) {
|
|
||||||
onlinePluginGroupActive.value = groupKey;
|
|
||||||
onlinePluginSearch.value.keyword = "";
|
|
||||||
onlinePluginQueryKeyword.value = "";
|
|
||||||
onlinePage.value = 1;
|
|
||||||
loadOnlinePlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOnlinePluginSearch(value: string) {
|
|
||||||
onlinePluginQueryKeyword.value = (value || "").trim();
|
|
||||||
onlinePluginGroupActive.value = "all";
|
|
||||||
onlinePage.value = 1;
|
|
||||||
loadOnlinePlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedOnlinePluginGroups: any = computed(() => {
|
|
||||||
const allPlugins = onlineGroupPlugins.value.map(plugin => buildOnlinePluginCard(plugin));
|
|
||||||
const groups: any = {
|
|
||||||
all: {
|
|
||||||
key: "all",
|
|
||||||
title: t("certd.all"),
|
|
||||||
order: 0,
|
|
||||||
icon: "material-symbols:border-all-rounded",
|
|
||||||
plugins: getPagedOnlinePlugins("all"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const groupKeys = Array.from(new Set(allPlugins.map(plugin => getOnlinePluginGroupKey(plugin))));
|
|
||||||
groupKeys.sort((left, right) => {
|
|
||||||
const leftMeta = getOnlinePluginGroupMeta(left);
|
|
||||||
const rightMeta = getOnlinePluginGroupMeta(right);
|
|
||||||
return leftMeta.order - rightMeta.order || left.localeCompare(right);
|
|
||||||
});
|
|
||||||
for (const groupKey of groupKeys) {
|
|
||||||
const meta = getOnlinePluginGroupMeta(groupKey);
|
|
||||||
groups[groupKey] = {
|
|
||||||
key: groupKey,
|
|
||||||
title: meta.title,
|
|
||||||
order: meta.order,
|
|
||||||
icon: meta.icon,
|
|
||||||
plugins: getPagedOnlinePlugins(groupKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onlineTotalPage = computed(() => {
|
|
||||||
const total = getOnlinePluginsByGroup(onlinePluginGroupActive.value).length;
|
|
||||||
return Math.max(1, Math.ceil(total / onlinePageSize));
|
|
||||||
});
|
|
||||||
|
|
||||||
const onlineCurrentTotal = computed(() => {
|
|
||||||
return getOnlinePluginsByGroup(onlinePluginGroupActive.value).length;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
return onlinePluginGroupActive.value;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
onlinePage.value = 1;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
return onlineTotalPage.value;
|
|
||||||
},
|
|
||||||
totalPage => {
|
|
||||||
if (onlinePage.value > totalPage) {
|
|
||||||
onlinePage.value = totalPage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
loadPluginGroups();
|
|
||||||
loadOnlinePlugins();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.step-plugin-source-pane-online {
|
|
||||||
.step-market-content {
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 2px 2px 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-market-state {
|
|
||||||
display: flex;
|
|
||||||
min-height: 320px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #7b8794;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.market-plugin-col {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.online-plugin-pagination {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 10px 2px 0;
|
|
||||||
color: #5f6b7a;
|
|
||||||
font-size: 12px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
|
|
||||||
.ant-pagination {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-pagination-simple-pager {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-user">
|
<div class="dashboard-user">
|
||||||
<div class="header-profile flex-wrap bg-white dark:bg-black">
|
<div class="header-profile flex-wrap bg-white dark:bg-black">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
<template v-if="userStore.isAdmin">
|
<template v-if="userStore.isAdmin">
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
<a-badge :dot="hasNewVersion">
|
<a-badge :dot="hasNewVersion">
|
||||||
<a-tag color="blue" class="flex-inline pointer mr-0" :title="'v' + version + ' ' + (settingsStore.app.releaseMode === 'stable' ? '稳定版' : '')" @click="openUpgradeUrl()">
|
<a-tag color="blue" class="flex-inline pointer mr-0" :title="t('certd.dashboard.latestVersion', { version: latestVersion })" @click="openUpgradeUrl()">
|
||||||
<fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon>
|
<fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon>
|
||||||
v{{ version }} {{ settingsStore.app.releaseMode === "stable" ? "stable" : "" }}
|
v{{ version }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</a-badge>
|
</a-badge>
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
@@ -179,6 +179,7 @@ defineOptions({
|
|||||||
|
|
||||||
const version = ref(import.meta.env.VITE_APP_VERSION);
|
const version = ref(import.meta.env.VITE_APP_VERSION);
|
||||||
const latestVersion = ref("");
|
const latestVersion = ref("");
|
||||||
|
|
||||||
function isNewVersion(version: string, latestVersion: string) {
|
function isNewVersion(version: string, latestVersion: string) {
|
||||||
if (!latestVersion) {
|
if (!latestVersion) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { IframeClient } from "@certd/lib-iframe";
|
import { IframeClient } from "@certd/lib-iframe";
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
@@ -23,7 +23,6 @@ defineOptions({
|
|||||||
name: "AccountBind",
|
name: "AccountBind",
|
||||||
});
|
});
|
||||||
const iframeRef = ref();
|
const iframeRef = ref();
|
||||||
let iframeClient: IframeClient | undefined;
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
@@ -41,10 +40,9 @@ type SubjectInfo = {
|
|||||||
installAt?: number;
|
installAt?: number;
|
||||||
vipType?: string;
|
vipType?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
userId?: number;
|
|
||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
iframeClient = new IframeClient(iframeRef.value, (e: any) => {
|
const iframeClient = new IframeClient(iframeRef.value, (e: any) => {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: " error",
|
message: " error",
|
||||||
description: e.message,
|
description: e.message,
|
||||||
@@ -56,7 +54,6 @@ onMounted(() => {
|
|||||||
installAt: settingStore.installInfo.installTime,
|
installAt: settingStore.installInfo.installTime,
|
||||||
vipType: settingStore.plusInfo.vipType || "free",
|
vipType: settingStore.plusInfo.vipType || "free",
|
||||||
expiresAt: settingStore.plusInfo.expireTime,
|
expiresAt: settingStore.plusInfo.expireTime,
|
||||||
userId: settingStore.installInfo.bindUserId || undefined,
|
|
||||||
};
|
};
|
||||||
return subjectInfo;
|
return subjectInfo;
|
||||||
});
|
});
|
||||||
@@ -87,11 +84,6 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
iframeClient?.destroy();
|
|
||||||
iframeClient = undefined;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { request } from "/src/api/service";
|
|
||||||
|
|
||||||
const apiPrefix = "/sys/audit";
|
|
||||||
|
|
||||||
export function createSysAuditApi() {
|
|
||||||
return {
|
|
||||||
async GetList(query: any) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/page",
|
|
||||||
method: "post",
|
|
||||||
data: query,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async DelObj(id: number) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/delete",
|
|
||||||
method: "post",
|
|
||||||
params: { id },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async Clean(retentionDays: number) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/clean",
|
|
||||||
method: "post",
|
|
||||||
data: { retentionDays },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async GetDict() {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/dict",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import { CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, UserPageQuery, UserPageRes, dict } from "@fast-crud/fast-crud";
|
|
||||||
import { Modal } from "ant-design-vue";
|
|
||||||
import { useI18n } from "/src/locales";
|
|
||||||
import { useDicts } from "/@/views/certd/dicts";
|
|
||||||
|
|
||||||
const typeDict = dict({
|
|
||||||
url: "/sys/audit/dict",
|
|
||||||
getData: async () => {
|
|
||||||
const { createSysAuditApi } = await import("./api");
|
|
||||||
const api = createSysAuditApi();
|
|
||||||
const res = await api.GetDict();
|
|
||||||
return res.types || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionDict = dict({
|
|
||||||
url: "/sys/audit/dict",
|
|
||||||
getData: async () => {
|
|
||||||
const { createSysAuditApi } = await import("./api");
|
|
||||||
const api = createSysAuditApi();
|
|
||||||
const res = await api.GetDict();
|
|
||||||
return res.actions || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { myProjectDict } = useDicts();
|
|
||||||
const api = context.api;
|
|
||||||
|
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
|
||||||
return await api.GetList(query);
|
|
||||||
};
|
|
||||||
const delRequest = async (req: DelReq) => {
|
|
||||||
return await api.DelObj(req.row.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanExpired = async () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: "确认清理",
|
|
||||||
content: "确定要清理90天前的审计日志吗?此操作不可撤销。",
|
|
||||||
okText: "确认清理",
|
|
||||||
okType: "danger",
|
|
||||||
cancelText: "取消",
|
|
||||||
async onOk() {
|
|
||||||
await api.Clean(90);
|
|
||||||
crudExpose.doRefresh();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
crudOptions: {
|
|
||||||
request: { pageRequest, delRequest },
|
|
||||||
tabs: {
|
|
||||||
name: "scope",
|
|
||||||
show: true,
|
|
||||||
dict: {
|
|
||||||
data: [
|
|
||||||
{ value: "system", label: "系统级", color: "red" },
|
|
||||||
{ value: "user", label: "用户级", color: "blue" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actionbar: {
|
|
||||||
buttons: {
|
|
||||||
add: { show: false },
|
|
||||||
clean: {
|
|
||||||
text: "清理过期日志(90天)",
|
|
||||||
type: "primary",
|
|
||||||
danger: true,
|
|
||||||
click: cleanExpired,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowHandle: {
|
|
||||||
width: 120,
|
|
||||||
fixed: "right",
|
|
||||||
buttons: {
|
|
||||||
view: { show: false },
|
|
||||||
edit: { show: false },
|
|
||||||
remove: { show: true },
|
|
||||||
copy: { show: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
initialForm: { scope: "system" },
|
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
id: {
|
|
||||||
title: "ID",
|
|
||||||
type: "number",
|
|
||||||
column: { width: 80 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
createTime: {
|
|
||||||
title: "操作时间",
|
|
||||||
type: "datetime",
|
|
||||||
search: {
|
|
||||||
show: true,
|
|
||||||
component: {
|
|
||||||
name: "a-range-picker",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
column: { width: 170, sorter: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
title: "操作人",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 120 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
title: "类型",
|
|
||||||
type: "dict-select",
|
|
||||||
dict: typeDict,
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 120 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title: "操作",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 200, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
title: "结果",
|
|
||||||
type: "dict-switch",
|
|
||||||
dict: dict({
|
|
||||||
data: [
|
|
||||||
{ value: true, label: "成功", color: "success" },
|
|
||||||
{ value: false, label: "失败", color: "error" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
column: { width: 100, align: "center" },
|
|
||||||
form: { show: false },
|
|
||||||
search: { show: true },
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
title: "备注",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 500, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
ipAddress: {
|
|
||||||
title: "IP地址",
|
|
||||||
type: "text",
|
|
||||||
column: { width: 140 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
projectId: {
|
|
||||||
title: t("certd.fields.projectName"),
|
|
||||||
type: "dict-select",
|
|
||||||
dict: myProjectDict,
|
|
||||||
form: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scope: {
|
|
||||||
title: "范围",
|
|
||||||
type: "dict-select",
|
|
||||||
dict: dict({
|
|
||||||
data: [
|
|
||||||
{ value: "system", label: "系统级", color: "blue" },
|
|
||||||
{ value: "user", label: "用户级", color: "green" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 100 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<fs-page>
|
|
||||||
<template #header>
|
|
||||||
<div class="title">
|
|
||||||
操作日志
|
|
||||||
<span class="sub">查看所有用户的操作记录</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
|
||||||
</fs-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useFs } from "@fast-crud/fast-crud";
|
|
||||||
import createCrudOptions from "./crud";
|
|
||||||
import { createSysAuditApi } from "./api";
|
|
||||||
import { useMounted } from "/@/use/use-mounted";
|
|
||||||
|
|
||||||
defineOptions({ name: "SysAuditLog" });
|
|
||||||
|
|
||||||
const api = createSysAuditApi();
|
|
||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
|
|
||||||
useMounted(() => crudExpose.doRefresh());
|
|
||||||
</script>
|
|
||||||
@@ -1,35 +1,75 @@
|
|||||||
import { request } from "/src/api/service";
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
const apiPrefix = "/sys/audit";
|
const apiPrefix = "/sys/project/provider";
|
||||||
|
|
||||||
export function createSysAuditApi() {
|
export async function GetList(query: any) {
|
||||||
return {
|
return await request({
|
||||||
async GetList(query: any) {
|
url: apiPrefix + "/page",
|
||||||
return await request({
|
method: "post",
|
||||||
url: apiPrefix + "/page",
|
data: query,
|
||||||
method: "post",
|
});
|
||||||
data: query,
|
}
|
||||||
});
|
|
||||||
},
|
export async function AddObj(obj: any) {
|
||||||
async DelObj(id: number) {
|
return await request({
|
||||||
return await request({
|
url: apiPrefix + "/add",
|
||||||
url: apiPrefix + "/delete",
|
method: "post",
|
||||||
method: "post",
|
data: obj,
|
||||||
params: { id },
|
});
|
||||||
});
|
}
|
||||||
},
|
|
||||||
async Clean(retentionDays: number) {
|
export async function UpdateObj(obj: any) {
|
||||||
return await request({
|
return await request({
|
||||||
url: apiPrefix + "/clean",
|
url: apiPrefix + "/update",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: { retentionDays },
|
data: obj,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
async GetDict() {
|
|
||||||
return await request({
|
export async function DelObj(id: any) {
|
||||||
url: apiPrefix + "/dict",
|
return await request({
|
||||||
method: "post",
|
url: apiPrefix + "/delete",
|
||||||
});
|
method: "post",
|
||||||
},
|
params: { id },
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetObj(id: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/info",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetDetail(id: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/detail",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DeleteBatch(ids: any[]) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/deleteByIds",
|
||||||
|
method: "post",
|
||||||
|
data: { ids },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetDefault(id: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/setDefault",
|
||||||
|
method: "post",
|
||||||
|
data: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetDisabled(id: any, disabled: boolean) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/setDisabled",
|
||||||
|
method: "post",
|
||||||
|
data: { id, disabled },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +1,252 @@
|
|||||||
import { ColumnProps, DataFormatterContext, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, UserPageQuery, UserPageRes, dict } from "@fast-crud/fast-crud";
|
import * as api from "./api";
|
||||||
import { Modal } from "ant-design-vue";
|
|
||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
import { useDicts } from "/@/views/certd/dicts";
|
import { computed, Ref, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
const typeDict = dict({
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
|
||||||
url: "/sys/audit/dict",
|
import { useUserStore } from "/@/store/user";
|
||||||
getData: async () => {
|
import { useSettingStore } from "/@/store/settings";
|
||||||
const { createSysAuditApi } = await import("./api");
|
import { Modal } from "ant-design-vue";
|
||||||
const api = createSysAuditApi();
|
|
||||||
const res = await api.GetDict();
|
|
||||||
return res.types || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionDict = dict({
|
|
||||||
url: "/sys/audit/dict",
|
|
||||||
getData: async () => {
|
|
||||||
const { createSysAuditApi } = await import("./api");
|
|
||||||
const api = createSysAuditApi();
|
|
||||||
const res = await api.GetDict();
|
|
||||||
return res.actions || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { myProjectDict } = useDicts();
|
|
||||||
const api = context.api;
|
|
||||||
|
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
return await api.GetList(query);
|
return await api.GetList(query);
|
||||||
};
|
};
|
||||||
const delRequest = async (req: DelReq) => {
|
const editRequest = async ({ form, row }: EditReq) => {
|
||||||
return await api.DelObj(req.row.id);
|
form.id = row.id;
|
||||||
|
const res = await api.UpdateObj(form);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const delRequest = async ({ row }: DelReq) => {
|
||||||
|
return await api.DelObj(row.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanExpired = async () => {
|
const addRequest = async ({ form }: AddReq) => {
|
||||||
Modal.confirm({
|
const res = await api.AddObj(form);
|
||||||
title: "确认清理",
|
return res;
|
||||||
content: "确定要清理90天前的审计日志吗?此操作不可撤销。",
|
|
||||||
okText: "确认清理",
|
|
||||||
okType: "danger",
|
|
||||||
cancelText: "取消",
|
|
||||||
async onOk() {
|
|
||||||
await api.Clean(90);
|
|
||||||
crudExpose.doRefresh();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const selectedRowKeys: Ref<any[]> = ref([]);
|
||||||
|
context.selectedRowKeys = selectedRowKeys;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
crudOptions: {
|
crudOptions: {
|
||||||
request: { pageRequest, delRequest },
|
settings: {
|
||||||
tabs: {
|
plugins: {
|
||||||
name: "scope",
|
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
|
||||||
show: true,
|
rowSelection: {
|
||||||
dict: {
|
enabled: true,
|
||||||
data: [
|
order: -2,
|
||||||
{ value: "system", label: "系统级", color: "red" },
|
before: true,
|
||||||
{ value: "user", label: "用户级", color: "blue" },
|
// handle: (pluginProps,useCrudProps)=>CrudOptions,
|
||||||
],
|
props: {
|
||||||
},
|
multiple: true,
|
||||||
},
|
crossPage: true,
|
||||||
toolbar: {
|
selectedRowKeys,
|
||||||
buttons: {
|
},
|
||||||
export: { show: true },
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
dataFrom: "search",
|
|
||||||
columnFilter: (col: ColumnProps) => col.show === true,
|
|
||||||
dataFormatter: (opts: DataFormatterContext) => {
|
|
||||||
const { row, originalRow, col } = opts;
|
|
||||||
const key = col.key;
|
|
||||||
if (key === "createTime" && originalRow[key]) {
|
|
||||||
row[key] = new Date(originalRow[key]).toLocaleString();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actionbar: {
|
request: {
|
||||||
buttons: {
|
pageRequest,
|
||||||
add: { show: false },
|
addRequest,
|
||||||
clean: {
|
editRequest,
|
||||||
text: "清理过期日志(90天)",
|
delRequest,
|
||||||
type: "primary",
|
|
||||||
danger: true,
|
|
||||||
click: cleanExpired,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
rowHandle: {
|
rowHandle: {
|
||||||
width: 120,
|
minWidth: 200,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
buttons: {
|
|
||||||
view: { show: false },
|
|
||||||
edit: { show: false },
|
|
||||||
remove: { show: true },
|
|
||||||
copy: { show: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
initialForm: { scope: "system" },
|
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
id: {
|
id: {
|
||||||
title: "ID",
|
title: "ID",
|
||||||
|
key: "id",
|
||||||
type: "number",
|
type: "number",
|
||||||
column: { width: 80 },
|
column: {
|
||||||
form: { show: false },
|
width: 100,
|
||||||
},
|
|
||||||
createTime: {
|
|
||||||
title: "操作时间",
|
|
||||||
type: "datetime",
|
|
||||||
search: {
|
|
||||||
show: true,
|
|
||||||
component: {
|
|
||||||
name: "a-range-picker",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
column: { width: 170, sorter: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
title: "操作人",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 120 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
title: "类型",
|
|
||||||
type: "dict-select",
|
|
||||||
dict: typeDict,
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 120 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title: "操作",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 200, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
title: "结果",
|
|
||||||
type: "dict-switch",
|
|
||||||
dict: dict({
|
|
||||||
data: [
|
|
||||||
{ value: true, label: "成功", color: "success" },
|
|
||||||
{ value: false, label: "失败", color: "error" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
column: { width: 100, align: "center" },
|
|
||||||
form: { show: false },
|
|
||||||
search: { show: true },
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
title: "备注",
|
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
|
||||||
column: { width: 500, tooltip: true },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
ipAddress: {
|
|
||||||
title: "IP地址",
|
|
||||||
type: "text",
|
|
||||||
column: { width: 140 },
|
|
||||||
form: { show: false },
|
|
||||||
},
|
|
||||||
projectId: {
|
|
||||||
title: t("certd.fields.projectName"),
|
|
||||||
type: "dict-select",
|
|
||||||
dict: myProjectDict,
|
|
||||||
form: {
|
form: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scope: {
|
domain: {
|
||||||
title: "范围",
|
title: t("certd.cnameDomain"),
|
||||||
|
type: "text",
|
||||||
|
editForm: {
|
||||||
|
component: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
component: {
|
||||||
|
placeholder: t("certd.cnameDomainPlaceholder"),
|
||||||
|
},
|
||||||
|
helper: t("certd.cnameDomainHelper"),
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: t("certd.requiredField") },
|
||||||
|
{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dnsProviderType: {
|
||||||
|
title: t("certd.dnsProvider"),
|
||||||
|
type: "dict-select",
|
||||||
|
search: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
dict: dict({
|
||||||
|
url: "pi/dnsProvider/list",
|
||||||
|
value: "key",
|
||||||
|
label: "title",
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
rules: [{ required: true, message: t("certd.requiredField") }],
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 150,
|
||||||
|
component: {
|
||||||
|
color: "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accessId: {
|
||||||
|
title: t("certd.dnsProviderAuthorization"),
|
||||||
type: "dict-select",
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
url: "/pi/access/list",
|
||||||
|
value: "id",
|
||||||
|
label: "name",
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
vModel: "modelValue",
|
||||||
|
type: compute(({ form }) => {
|
||||||
|
return form.dnsProviderType;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: t("certd.requiredField") }],
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 150,
|
||||||
|
component: {
|
||||||
|
color: "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isDefault: {
|
||||||
|
title: t("certd.isDefault"),
|
||||||
|
type: "dict-switch",
|
||||||
dict: dict({
|
dict: dict({
|
||||||
data: [
|
data: [
|
||||||
{ value: "system", label: "系统级", color: "blue" },
|
{ label: t("certd.yes"), value: true, color: "success" },
|
||||||
{ value: "user", label: "用户级", color: "green" },
|
{ label: t("certd.no"), value: false, color: "default" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
search: { show: true },
|
form: {
|
||||||
column: { width: 100 },
|
value: false,
|
||||||
form: { show: false },
|
rules: [{ required: true, message: t("certd.selectIsDefault") }],
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
align: "center",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setDefault: {
|
||||||
|
title: t("certd.setDefault"),
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 100,
|
||||||
|
align: "center",
|
||||||
|
conditionalRenderDisabled: true,
|
||||||
|
cellRender: ({ row }) => {
|
||||||
|
if (row.isDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onClick = async () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("certd.prompt"),
|
||||||
|
content: t("certd.confirmSetDefault"),
|
||||||
|
onOk: async () => {
|
||||||
|
await api.SetDefault(row.id);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a-button type={"link"} size={"small"} onClick={onClick}>
|
||||||
|
{t("certd.setAsDefault")}
|
||||||
|
</a-button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
title: t("certd.disabled"),
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: t("certd.enabled"), value: false, color: "success" },
|
||||||
|
{ label: t("certd.disabledLabel"), value: true, color: "error" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 100,
|
||||||
|
component: {
|
||||||
|
title: t("certd.clickToToggle"),
|
||||||
|
on: {
|
||||||
|
async click({ value, row }) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("certd.prompt"),
|
||||||
|
content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
|
||||||
|
onOk: async () => {
|
||||||
|
await api.SetDisabled(row.id, !value);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
title: t("certd.createTime"),
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
sorter: true,
|
||||||
|
width: 160,
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateTime: {
|
||||||
|
title: t("certd.updateTime"),
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
show: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<fs-page>
|
<fs-page class="page-cert">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
操作日志
|
{{ t("certd.cnameTitle") }}
|
||||||
<span class="sub">查看所有用户的操作记录</span>
|
<span class="sub">
|
||||||
|
{{ t("certd.cnameDescription") }}
|
||||||
|
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">
|
||||||
|
{{ t("certd.cnameLinkText") }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
<template #pagination-left>
|
||||||
|
<a-tooltip :title="t('certd.batchDelete')">
|
||||||
|
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</fs-crud>
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useMounted } from "/@/use/use-mounted";
|
||||||
import { useFs } from "@fast-crud/fast-crud";
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
import { createSysAuditApi } from "./api";
|
import { message, Modal } from "ant-design-vue";
|
||||||
import { useMounted } from "/@/use/use-mounted";
|
import { DeleteBatch } from "./api";
|
||||||
|
import { useI18n } from "/src/locales";
|
||||||
|
import { useCrudPermission } from "/@/plugin/permission";
|
||||||
|
|
||||||
defineOptions({ name: "SysAuditLog" });
|
const { t } = useI18n();
|
||||||
|
|
||||||
const api = createSysAuditApi();
|
defineOptions({
|
||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
|
name: "CnameProvider",
|
||||||
useMounted(() => crudExpose.doRefresh());
|
});
|
||||||
|
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
const selectedRowKeys = context.selectedRowKeys;
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRowKeys.value?.length > 0) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("certd.confirmTitle"),
|
||||||
|
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
|
||||||
|
async onOk() {
|
||||||
|
await DeleteBatch(selectedRowKeys.value);
|
||||||
|
message.info(t("certd.deleteSuccess"));
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(t("certd.selectRecordsFirst"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面打开后获取列表数据
|
||||||
|
useMounted(async () => {
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="less"></style>
|
||||||
|
|||||||
@@ -82,176 +82,6 @@ export async function ImportPlugin(body: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OnlinePluginBean = {
|
|
||||||
id?: number;
|
|
||||||
appId?: number;
|
|
||||||
developerId?: number;
|
|
||||||
author?: string;
|
|
||||||
type?: string;
|
|
||||||
pluginType?: string;
|
|
||||||
name?: string;
|
|
||||||
fullName?: string;
|
|
||||||
title?: string;
|
|
||||||
icon?: string;
|
|
||||||
group?: string;
|
|
||||||
desc?: string;
|
|
||||||
latest?: string;
|
|
||||||
status?: string;
|
|
||||||
downloadCount?: number;
|
|
||||||
score?: number;
|
|
||||||
aiCheckStatus?: string;
|
|
||||||
selfAuthored?: boolean;
|
|
||||||
installed?: boolean;
|
|
||||||
installedVersion?: string;
|
|
||||||
upgradeAvailable?: boolean;
|
|
||||||
localPluginId?: number;
|
|
||||||
localDisabled?: boolean;
|
|
||||||
localEditable?: boolean;
|
|
||||||
syncTime?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OnlinePluginVersionBean = {
|
|
||||||
id?: number;
|
|
||||||
pluginId?: number;
|
|
||||||
version?: string;
|
|
||||||
minAppVersion?: string;
|
|
||||||
maxAppVersion?: string;
|
|
||||||
status?: string;
|
|
||||||
publishedAt?: number;
|
|
||||||
reviewStatus?: string;
|
|
||||||
reviewReason?: string;
|
|
||||||
reviewedAt?: number;
|
|
||||||
aiCheckStatus?: string;
|
|
||||||
aiCheckResult?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OnlinePluginCommentBean = {
|
|
||||||
id?: number;
|
|
||||||
createdAt?: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
userId?: number;
|
|
||||||
score?: number;
|
|
||||||
comment?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function OnlinePluginList(body: { pluginType?: string; group?: string; keyword?: string }): Promise<OnlinePluginBean[]> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/list",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginSync(): Promise<OnlinePluginBean[]> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/sync",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginSetting(): Promise<{ lastSyncTime?: number }> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/setting",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginInstall(body: { fullName: string; version?: string }) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/install",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginDetail(body: { pluginId?: number; fullName?: string; commentPageNo?: number; commentPageSize?: number }): Promise<{
|
|
||||||
plugin: OnlinePluginBean;
|
|
||||||
versions: OnlinePluginVersionBean[];
|
|
||||||
myScore?: number;
|
|
||||||
myComment?: string;
|
|
||||||
comments?: OnlinePluginCommentBean[];
|
|
||||||
commentsTotal?: number;
|
|
||||||
}> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/detail",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginSource(body: { pluginId?: number; fullName?: string; version?: string }): Promise<{ plugin: OnlinePluginBean; version: OnlinePluginVersionBean; content: string }> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/source",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginRate(body: { pluginId?: number; fullName?: string; score: number; comment: string }): Promise<{ plugin: OnlinePluginBean; myScore?: number; myComment?: string }> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/rate",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginSubmitVersion(body: { fullName: string; version: string; content: string; minAppVersion?: string; maxAppVersion?: string }) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/version/submit",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginPublish(body: { id: number; version?: string; minAppVersion?: string; maxAppVersion?: string }) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/publish",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginPublishInfo(body: { id: number }): Promise<{
|
|
||||||
localPlugin: OnlinePluginBean;
|
|
||||||
authorRegistered?: boolean;
|
|
||||||
author?: OnlinePluginAuthorBean;
|
|
||||||
marketPlugin?: OnlinePluginBean;
|
|
||||||
versions: OnlinePluginVersionBean[];
|
|
||||||
}> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/publish/info",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OnlinePluginAuthorBean = {
|
|
||||||
id?: number;
|
|
||||||
appId?: number;
|
|
||||||
appOwnerId?: number;
|
|
||||||
developerId?: number;
|
|
||||||
name?: string;
|
|
||||||
displayName?: string;
|
|
||||||
avatar?: string;
|
|
||||||
desc?: string;
|
|
||||||
status?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function OnlinePluginAuthorGet(): Promise<{ registered?: boolean; author?: OnlinePluginAuthorBean }> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/author/get",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OnlinePluginAuthorAdd(body: { name: string; displayName?: string; avatar?: string; desc?: string }): Promise<OnlinePluginAuthorBean> {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/online/author/add",
|
|
||||||
method: "post",
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PluginConfigBean = {
|
export type PluginConfigBean = {
|
||||||
name: string;
|
name: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -264,9 +94,6 @@ export type CertApplyPluginSysInput = {
|
|||||||
googleCommonEabAccessId?: number;
|
googleCommonEabAccessId?: number;
|
||||||
zerosslCommonEabAccessId?: number;
|
zerosslCommonEabAccessId?: number;
|
||||||
litesslCommonEabAccessId?: number;
|
litesslCommonEabAccessId?: number;
|
||||||
googleCommonAcmeAccountAccessId?: number;
|
|
||||||
zerosslCommonAcmeAccountAccessId?: number;
|
|
||||||
litesslCommonAcmeAccountAccessId?: number;
|
|
||||||
};
|
};
|
||||||
export type PluginSysSetting<T> = {
|
export type PluginSysSetting<T> = {
|
||||||
sysSetting: {
|
sysSetting: {
|
||||||
|
|||||||
-136
@@ -1,136 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-modal v-model:open="visible" :width="'min(1540px, calc(100vw - 48px))'" :footer="null" destroy-on-close class="online-plugin-detail-modal">
|
|
||||||
<a-spin :spinning="loading">
|
|
||||||
<iframe v-if="iframeSrc" ref="iframeRef" class="online-plugin-detail-modal__iframe" :src="iframeSrc" @load="handleIframeLoad" />
|
|
||||||
<a-empty v-else description="插件详情地址未配置" />
|
|
||||||
</a-spin>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
||||||
import { message } from "ant-design-vue";
|
|
||||||
import { IframeClient } from "@certd/lib-iframe";
|
|
||||||
import * as api from "../api";
|
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
|
||||||
import { useSettingStore } from "/src/store/settings";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
open: boolean;
|
|
||||||
plugin?: any;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:open", value: boolean): void;
|
|
||||||
(e: "installed", value: { plugin: any; version?: string }): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const iframeRef = ref<HTMLIFrameElement>();
|
|
||||||
const loading = ref(false);
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.open,
|
|
||||||
set: value => emit("update:open", value),
|
|
||||||
});
|
|
||||||
const iframeSrc = computed(() => {
|
|
||||||
const plugin = props.plugin;
|
|
||||||
if (!plugin) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const baseUrl = String(settingStore.installInfo?.accountServerBaseUrl || "").replace(/\/$/, "");
|
|
||||||
if (!baseUrl) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
embedded: "true",
|
|
||||||
fullName: plugin.fullName || "",
|
|
||||||
installedVersion: plugin.installedVersion || "",
|
|
||||||
t: `${Date.now()}`,
|
|
||||||
});
|
|
||||||
return `${baseUrl}/#/app/certd/plugin/${plugin.id || 0}?${params.toString()}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
let iframeClient: IframeClient;
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.open,
|
|
||||||
async open => {
|
|
||||||
if (!open || !props.plugin) {
|
|
||||||
loading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
await nextTick();
|
|
||||||
setupIframeClient();
|
|
||||||
if (!iframeSrc.value) {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
iframeClient?.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupIframeClient() {
|
|
||||||
//@ts-ignore
|
|
||||||
iframeClient?.destroy();
|
|
||||||
if (!iframeRef.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
iframeClient = new IframeClient(iframeRef.value, (error: any) => {
|
|
||||||
message.error(error?.message || "插件操作失败");
|
|
||||||
});
|
|
||||||
iframeClient.register("installPlugin", async req => {
|
|
||||||
return await installPluginVersion(req.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIframeLoad() {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installPluginVersion(data: { fullName?: string; version?: string }) {
|
|
||||||
const fullName = data?.fullName || props.plugin?.fullName;
|
|
||||||
if (!fullName || !data?.version) {
|
|
||||||
throw new Error("插件版本信息不完整");
|
|
||||||
}
|
|
||||||
await api.OnlinePluginInstall({
|
|
||||||
fullName,
|
|
||||||
version: data.version,
|
|
||||||
});
|
|
||||||
await pluginStore.reload();
|
|
||||||
emit("installed", {
|
|
||||||
plugin: props.plugin,
|
|
||||||
version: data.version,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
version: data.version,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.online-plugin-detail-modal {
|
|
||||||
.ant-modal-body {
|
|
||||||
height: 80vh;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-spin-nested-loading,
|
|
||||||
.ant-spin-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__iframe {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 80vh;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="plugin-author-field">
|
|
||||||
<span v-if="authorName" class="plugin-author-field__name">{{ authorName }}</span>
|
|
||||||
<a-button v-else type="link" size="small" :loading="loading" @click="registerAuthor">注册作者</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref } from "vue";
|
|
||||||
import * as api from "../api";
|
|
||||||
import { usePluginPublish } from "../use-publish";
|
|
||||||
|
|
||||||
defineOptions({ name: "PluginAuthorField" });
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue?: string }>();
|
|
||||||
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
|
|
||||||
const loading = ref(false);
|
|
||||||
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
|
|
||||||
const { registerPluginAuthor } = usePluginPublish();
|
|
||||||
const authorName = computed(() => props.modelValue || registeredAuthor.value?.name || "");
|
|
||||||
|
|
||||||
async function loadAuthor() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const result = await api.OnlinePluginAuthorGet();
|
|
||||||
registeredAuthor.value = result.author;
|
|
||||||
if (!props.modelValue && result.author?.name) {
|
|
||||||
emit("update:modelValue", result.author.name);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 作者为非必填项,未绑定账号或尚未注册作者时保持为空。
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerAuthor() {
|
|
||||||
const author = await registerPluginAuthor();
|
|
||||||
if (!author?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
registeredAuthor.value = author;
|
|
||||||
emit("update:modelValue", author.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadAuthor);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.plugin-author-field {
|
|
||||||
display: flex;
|
|
||||||
min-height: 32px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="plugin-edit-dialog-body">
|
|
||||||
<div class="plugin-edit-dialog-body__header">
|
|
||||||
<span>插件名称:</span>
|
|
||||||
<fs-copyable :model-value="pluginName" />
|
|
||||||
<a-button v-if="canEditPlugin" class="plugin-edit-dialog-body__publish" :loading="isPublishingPlugin(plugin)" @click="doPublish">发布到插件市场</a-button>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-edit-dialog-body__content">
|
|
||||||
<section class="plugin-edit-dialog-body__base">
|
|
||||||
<a-tabs type="card">
|
|
||||||
<a-tab-pane key="base" tab="插件信息" />
|
|
||||||
</a-tabs>
|
|
||||||
<div class="plugin-edit-dialog-body__base-content">
|
|
||||||
<fs-form ref="baseFormRef" v-bind="formOptionsRef" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="plugin-edit-dialog-body__editor plugin-edit-dialog-body__metadata">
|
|
||||||
<a-tabs type="card">
|
|
||||||
<a-tab-pane key="metadata" tab="元数据" />
|
|
||||||
</a-tabs>
|
|
||||||
<code-editor :id="`plugin-metadata-${pluginId}`" v-model:model-value="plugin.metadata" language="yaml" @save="doSave" />
|
|
||||||
</section>
|
|
||||||
<section class="plugin-edit-dialog-body__editor plugin-edit-dialog-body__script">
|
|
||||||
<a-tabs type="card">
|
|
||||||
<a-tab-pane key="script" tab="脚本" />
|
|
||||||
</a-tabs>
|
|
||||||
<code-editor :id="`plugin-content-${pluginId}`" v-model:model-value="plugin.content" language="javascript" @save="doSave" />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, provide, ref, type Ref } from "vue";
|
|
||||||
import { notification } from "ant-design-vue";
|
|
||||||
import { useColumns } from "@fast-crud/fast-crud";
|
|
||||||
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import * as api from "../api";
|
|
||||||
import createCrudOptions from "../crud";
|
|
||||||
import { usePluginPublish } from "../use-publish";
|
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
|
||||||
import { useSettingStore } from "/@/store/settings";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PluginEditDialogBody",
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
pluginId: number | string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "saved"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const plugin = ref<any>({});
|
|
||||||
const formOptionsRef: Ref = ref();
|
|
||||||
const baseFormRef: Ref = ref({});
|
|
||||||
const saveLoading = ref(false);
|
|
||||||
const { isPublishingPlugin, publishLocalPlugin } = usePluginPublish();
|
|
||||||
|
|
||||||
function initFormOptions() {
|
|
||||||
const formCrudOptions = createCrudOptions({
|
|
||||||
// 编辑弹框只复用插件表单字段,不需要 CRUD 实例。
|
|
||||||
crudExpose: {},
|
|
||||||
context: {},
|
|
||||||
});
|
|
||||||
const { buildFormOptions } = useColumns();
|
|
||||||
const formOptions = buildFormOptions(formCrudOptions.crudOptions, {});
|
|
||||||
formOptions.mode = "edit";
|
|
||||||
formOptions.col = { span: 24 };
|
|
||||||
formOptions.labelCol = { style: { width: "100px" } };
|
|
||||||
formOptionsRef.value = formOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
initFormOptions();
|
|
||||||
|
|
||||||
const pluginName = computed(() => {
|
|
||||||
if (plugin.value.author) {
|
|
||||||
return `${plugin.value.author}/${plugin.value.name}`;
|
|
||||||
}
|
|
||||||
return plugin.value.name || "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const canEditPlugin = computed(() => {
|
|
||||||
const current = plugin.value || {};
|
|
||||||
if (current.type === "custom") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (current.type !== "store") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
|
||||||
return !current.developerId || (!!bindUserId && Number(current.developerId) === bindUserId);
|
|
||||||
});
|
|
||||||
|
|
||||||
provide("get:plugin", () => plugin);
|
|
||||||
|
|
||||||
async function loadPlugin() {
|
|
||||||
const pluginObj = await api.GetObj(props.pluginId);
|
|
||||||
plugin.value = pluginObj;
|
|
||||||
const baseForm = { ...pluginObj };
|
|
||||||
if (baseForm.extra) {
|
|
||||||
baseForm.extra = yaml.load(baseForm.extra);
|
|
||||||
}
|
|
||||||
delete baseForm.metadata;
|
|
||||||
delete baseForm.content;
|
|
||||||
baseFormRef.value.setFormData(baseForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate() {
|
|
||||||
try {
|
|
||||||
yaml.load(plugin.value.metadata);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = `元数据校验失败:${error.message}`;
|
|
||||||
notification.error({ message });
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSubmitForm() {
|
|
||||||
const baseForm = baseFormRef.value.getFormData();
|
|
||||||
const form = { ...plugin.value, ...baseForm };
|
|
||||||
if (form.extra) {
|
|
||||||
form.extra = yaml.dump(form.extra);
|
|
||||||
}
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doSave() {
|
|
||||||
if (!canEditPlugin.value) {
|
|
||||||
notification.warning({ message: "当前绑定账号无权编辑该插件" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validate();
|
|
||||||
saveLoading.value = true;
|
|
||||||
try {
|
|
||||||
const form = buildSubmitForm();
|
|
||||||
await api.UpdateObj(form);
|
|
||||||
plugin.value = form;
|
|
||||||
pluginStore.clear();
|
|
||||||
notification.success({ message: "保存成功" });
|
|
||||||
emit("saved");
|
|
||||||
return form;
|
|
||||||
} finally {
|
|
||||||
saveLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doPublish() {
|
|
||||||
const baseForm = baseFormRef.value.getFormData();
|
|
||||||
const currentPlugin = { ...plugin.value, ...baseForm };
|
|
||||||
await publishLocalPlugin(currentPlugin, {
|
|
||||||
beforePublish: doSave,
|
|
||||||
async afterPublish() {
|
|
||||||
await loadPlugin();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadPlugin);
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
save: doSave,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.plugin-edit-dialog-body {
|
|
||||||
display: flex;
|
|
||||||
min-height: 680px;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
display: flex;
|
|
||||||
min-height: 32px;
|
|
||||||
align-items: center;
|
|
||||||
color: #5f6b7a;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__publish {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
display: grid;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
grid-template-columns: minmax(260px, 0.72fr) minmax(330px, 1fr) minmax(430px, 1.35fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__base,
|
|
||||||
&__editor {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__base {
|
|
||||||
padding-right: 16px;
|
|
||||||
border-right: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__base-content {
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fs-editor-code {
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .plugin-edit-dialog-body {
|
|
||||||
&__base {
|
|
||||||
border-right-color: #303030;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, Ref, inject, toRef } from "vue";
|
import { ref, Ref, inject, toRef } from "vue";
|
||||||
|
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||||
|
import yaml from "js-yaml";
|
||||||
const activeKey = ref([]);
|
const activeKey = ref([]);
|
||||||
|
|
||||||
const getPlugin: any = inject("get:plugin");
|
const getPlugin: any = inject("get:plugin");
|
||||||
|
|||||||
@@ -1,949 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-card
|
|
||||||
hoverable
|
|
||||||
class="plugin-card plugin-item-card"
|
|
||||||
:class="{
|
|
||||||
current,
|
|
||||||
'is-simple': simple,
|
|
||||||
'is-local': source === 'local',
|
|
||||||
'is-installed': isInstalled,
|
|
||||||
'is-disabled': isDisabled,
|
|
||||||
}"
|
|
||||||
@click="handleCardClick"
|
|
||||||
@dblclick="handleCardDoubleClick"
|
|
||||||
>
|
|
||||||
<div class="plugin-card__head">
|
|
||||||
<div class="plugin-card__main">
|
|
||||||
<fs-icon class="plugin-icon plugin-card__icon" :icon="plugin.icon || 'clarity:plugin-line'" />
|
|
||||||
<div class="plugin-card__title-wrap">
|
|
||||||
<a v-if="showEditButton" class="plugin-card__title" :title="plugin.title || plugin.name" href="#" @click.stop.prevent="editPlugin">
|
|
||||||
{{ plugin.title || plugin.name }}
|
|
||||||
</a>
|
|
||||||
<span v-else class="plugin-card__title" :title="plugin.title || plugin.name">{{ plugin.title || plugin.name }}</span>
|
|
||||||
<a-tooltip v-if="plugin.aiCheckStatus === 'passed'" :title="t('certd.onlinePluginAiReviewPassed')">
|
|
||||||
<fs-icon class="plugin-card__ai-check-icon" icon="ion:shield-checkmark-outline" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-card__actions">
|
|
||||||
<div v-if="showEditButton || showConfigButton || showToolMenu" class="plugin-card__tools">
|
|
||||||
<a-tooltip v-if="showEditButton" title="编辑">
|
|
||||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="editPlugin">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:create-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip v-if="copyHandler && canCopyPlugin" title="复制">
|
|
||||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="copyPlugin">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:copy-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip v-if="isOwnImportedPlugin" :title="t('certd.export')">
|
|
||||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="exportPlugin">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:download-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip v-if="canPublishPlugin" :title="t('certd.onlinePluginPublish')">
|
|
||||||
<a-button class="plugin-card__tool" type="text" size="small" :loading="isPublishingPlugin(plugin)" @click.stop="publishPlugin">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:cloud-upload-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip v-if="showConfigButton" title="配置">
|
|
||||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="openConfig">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:settings-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
<div v-if="source === 'local' && canRemoveLocal" class="plugin-card__action-zone plugin-card__local-action-zone">
|
|
||||||
<a-tooltip title="删除">
|
|
||||||
<a-button class="plugin-card__action-button" type="text" size="small" danger :loading="isActionLoading('remove')" @click.stop="removePlugin">
|
|
||||||
<template #icon>
|
|
||||||
<fs-icon icon="ion:trash-outline" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
<template v-else-if="plugin.installed && source !== 'local'">
|
|
||||||
<div v-if="plugin.localPluginId" class="plugin-card__action-zone" :class="{ 'is-loading': isActionLoading('uninstall') }">
|
|
||||||
<a-tag color="green" class="plugin-status-tag">{{ t("certd.onlinePluginInstalled") }}</a-tag>
|
|
||||||
<a-button class="plugin-card__action-button" size="small" danger ghost :loading="isActionLoading('uninstall')" @click.stop="uninstallPlugin">
|
|
||||||
{{ t("certd.onlinePluginUninstall") }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<a-tag v-else color="green" class="plugin-status-tag">{{ t("certd.onlinePluginInstalled") }}</a-tag>
|
|
||||||
</template>
|
|
||||||
<div v-else-if="source !== 'local'" class="plugin-card__action-zone plugin-card__install-zone" :class="{ 'is-loading': isActionLoading('install') }">
|
|
||||||
<a-button class="plugin-card__action-button" size="small" type="primary" :loading="isActionLoading('install')" @click.stop="installPlugin">
|
|
||||||
{{ t("certd.onlinePluginInstall") }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="plugin-card__desc" :title="plugin.desc">
|
|
||||||
{{ plugin.desc || "暂无描述" }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="plugin-card__meta">
|
|
||||||
<template v-if="source === 'local'">
|
|
||||||
<a-tooltip :title="toggleTitle">
|
|
||||||
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
|
|
||||||
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
|
||||||
</a-tag>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag>{{ pluginTypeLabel }}</a-tag>
|
|
||||||
<a-tag>{{ sourceLabel }}</a-tag>
|
|
||||||
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="!simple">
|
|
||||||
<a-tooltip v-if="plugin.installed && plugin.localPluginId" :title="toggleTitle">
|
|
||||||
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.localDisabled ? 'default' : 'green'" @click.stop="togglePlugin">
|
|
||||||
{{ plugin.localDisabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
|
||||||
</a-tag>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag>{{ pluginTypeLabel }}</a-tag>
|
|
||||||
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
|
|
||||||
<a-tooltip :title="t('certd.onlinePluginDownloadCount', { count: plugin.downloadCount || 0 })">
|
|
||||||
<a-tag class="plugin-card__download-count">
|
|
||||||
<fs-icon icon="ion:cloud-download-outline" />
|
|
||||||
<span>{{ formatDownloadCount(plugin.downloadCount || 0) }}</span>
|
|
||||||
</a-tag>
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
<a-tooltip v-if="!isBuiltInPlugin" :title="versionTitle">
|
|
||||||
<span class="plugin-card__version" :class="{ 'is-upgradable': plugin.upgradeAvailable }" @click.stop="handleVersionClick">
|
|
||||||
v{{ currentVersion }}
|
|
||||||
<fs-icon v-if="plugin.upgradeAvailable" class="plugin-card__version-icon" icon="carbon:upgrade" />
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip v-if="source !== 'local' && authorName" :title="plugin.selfAuthored ? '这是我自己提交的插件' : `${t('certd.author')}:${authorName}`">
|
|
||||||
<span class="plugin-card__author" :class="{ 'is-self-authored': plugin.selfAuthored }">
|
|
||||||
<fs-icon :icon="plugin.selfAuthored ? 'ion:person-circle' : 'ion:person-circle-outline'" />
|
|
||||||
<span>{{ authorName }}</span>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, h, ref } from "vue";
|
|
||||||
import { message, Modal } from "ant-design-vue";
|
|
||||||
import { useFormDialog } from "/@/use/use-dialog";
|
|
||||||
import { useI18n } from "/src/locales";
|
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
|
||||||
import * as api from "../api";
|
|
||||||
import { usePluginConfig } from "../use-config";
|
|
||||||
import { usePluginPublish } from "../use-publish";
|
|
||||||
import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PluginItemCard",
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
plugin: any;
|
|
||||||
source?: "local" | "market";
|
|
||||||
current?: boolean;
|
|
||||||
simple?: boolean;
|
|
||||||
showConfig?: boolean;
|
|
||||||
editable?: boolean;
|
|
||||||
copyHandler?: (plugin: any) => void | Promise<void>;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
source: "market",
|
|
||||||
copyHandler: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "click", plugin: any): void;
|
|
||||||
(e: "dblclick", plugin: any): void;
|
|
||||||
(e: "changed", payload: { plugin: any; action: PluginCardAction }): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const { openConfigDialog } = usePluginConfig();
|
|
||||||
const { isPublishingPlugin, publishLocalPlugin } = usePluginPublish();
|
|
||||||
const { openFormDialog } = useFormDialog();
|
|
||||||
const actionLoading = ref<PluginCardAction | "">("");
|
|
||||||
const editDialogBodyRef = ref();
|
|
||||||
|
|
||||||
type PluginCardAction = "edit" | "copy" | "export" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
|
|
||||||
|
|
||||||
const editPluginId = computed(() => {
|
|
||||||
return props.plugin.localPluginId || props.plugin.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const canEditPlugin = computed(() => {
|
|
||||||
if (props.editable != null) {
|
|
||||||
return props.editable;
|
|
||||||
}
|
|
||||||
return props.plugin.type === "custom";
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOwnImportedPlugin = computed(() => {
|
|
||||||
return props.source === "local" && canEditPlugin.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAvailableLocally = computed(() => {
|
|
||||||
return props.source === "local" || !!props.plugin.installed;
|
|
||||||
});
|
|
||||||
|
|
||||||
const canCopyPlugin = computed(() => {
|
|
||||||
return isOwnImportedPlugin.value || (props.source === "market" && !!props.plugin.selfAuthored && isAvailableLocally.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showEditButton = computed(() => {
|
|
||||||
return canEditPlugin.value && isAvailableLocally.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showConfigButton = computed(() => {
|
|
||||||
return !!props.showConfig && isAvailableLocally.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isInstalled = computed(() => {
|
|
||||||
return props.source !== "local" && props.plugin.installed;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDisabled = computed(() => {
|
|
||||||
return props.source === "local" ? props.plugin.disabled : props.plugin.localDisabled;
|
|
||||||
});
|
|
||||||
|
|
||||||
const canRemoveLocal = computed(() => {
|
|
||||||
return isOwnImportedPlugin.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isBuiltInPlugin = computed(() => {
|
|
||||||
return props.source === "local" && props.plugin.type === "builtIn";
|
|
||||||
});
|
|
||||||
|
|
||||||
const canPublishPlugin = computed(() => {
|
|
||||||
return canEditPlugin.value && isAvailableLocally.value && (!props.plugin.status || !!props.plugin.selfAuthored);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showToolMenu = computed(() => {
|
|
||||||
return !isBuiltInPlugin.value && (isOwnImportedPlugin.value || canPublishPlugin.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fullName = computed(() => {
|
|
||||||
if (props.plugin.fullName) {
|
|
||||||
return props.plugin.fullName;
|
|
||||||
}
|
|
||||||
if (props.plugin.author && props.plugin.name) {
|
|
||||||
return `${props.plugin.author}/${props.plugin.name}`;
|
|
||||||
}
|
|
||||||
return props.plugin.name || "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const authorName = computed(() => {
|
|
||||||
if (props.plugin.author) {
|
|
||||||
return props.plugin.author;
|
|
||||||
}
|
|
||||||
const [author] = fullName.value.split("/");
|
|
||||||
return author || "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const pluginTypeLabel = computed(() => {
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
access: t("certd.auth"),
|
|
||||||
dnsProvider: t("certd.dns"),
|
|
||||||
deploy: t("certd.deployPlugin"),
|
|
||||||
};
|
|
||||||
return labelMap[props.plugin.pluginType || ""] || props.plugin.pluginType || "-";
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceLabel = computed(() => {
|
|
||||||
if (props.source === "local" && props.plugin.type !== "builtIn") {
|
|
||||||
return t("certd.localPlugin");
|
|
||||||
}
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
builtIn: t("certd.builtIn"),
|
|
||||||
custom: t("certd.custom"),
|
|
||||||
store: t("certd.store"),
|
|
||||||
};
|
|
||||||
return labelMap[props.plugin.type || ""] || props.plugin.type || "-";
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentVersion = computed(() => {
|
|
||||||
if (props.source === "local") {
|
|
||||||
return props.plugin.version || props.plugin.latest || "-";
|
|
||||||
}
|
|
||||||
return props.plugin.installedVersion || props.plugin.latest || "-";
|
|
||||||
});
|
|
||||||
|
|
||||||
const versionTitle = computed(() => {
|
|
||||||
if (props.source === "local") {
|
|
||||||
return `${t("certd.version")} v${currentVersion.value}`;
|
|
||||||
}
|
|
||||||
if (props.plugin.installedVersion && props.plugin.upgradeAvailable && props.plugin.latest) {
|
|
||||||
return `${t("certd.dashboard.latestVersion", { version: `v${props.plugin.latest}` })},${t("certd.onlinePluginClickToUpdate")}`;
|
|
||||||
}
|
|
||||||
if (props.plugin.installedVersion) {
|
|
||||||
return t("certd.onlinePluginAlreadyLatest");
|
|
||||||
}
|
|
||||||
if (props.plugin.latest) {
|
|
||||||
return t("certd.dashboard.latestVersion", { version: `v${props.plugin.latest}` });
|
|
||||||
}
|
|
||||||
return `${t("certd.version")} -`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleTitle = computed(() => {
|
|
||||||
const disabled = props.source === "local" ? props.plugin.disabled : props.plugin.localDisabled;
|
|
||||||
return disabled ? t("certd.onlinePluginClickToEnable") : t("certd.onlinePluginClickToDisable");
|
|
||||||
});
|
|
||||||
|
|
||||||
function isActionLoading(action: PluginCardAction) {
|
|
||||||
return actionLoading.value === action;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCardClick() {
|
|
||||||
if (props.source === "local") {
|
|
||||||
if (showEditButton.value) {
|
|
||||||
void editPlugin();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit("click", props.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCardDoubleClick() {
|
|
||||||
if (props.source !== "local") {
|
|
||||||
emit("dblclick", props.plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitChanged(action: PluginCardAction) {
|
|
||||||
emit("changed", {
|
|
||||||
plugin: props.plugin,
|
|
||||||
action,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runAction(action: PluginCardAction, callback: () => Promise<void>, options?: { emitChanged?: boolean }) {
|
|
||||||
if (actionLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
actionLoading.value = action;
|
|
||||||
try {
|
|
||||||
await callback();
|
|
||||||
if (options?.emitChanged !== false) {
|
|
||||||
emitChanged(action);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
actionLoading.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function editPlugin() {
|
|
||||||
await openFormDialog({
|
|
||||||
title: `编辑插件 ${props.plugin.title || props.plugin.name}`,
|
|
||||||
columns: {},
|
|
||||||
noneForm: true,
|
|
||||||
wrapper: {
|
|
||||||
width: 1480,
|
|
||||||
destroyOnClose: true,
|
|
||||||
maskClosable: false,
|
|
||||||
okText: "保存",
|
|
||||||
cancelText: "关闭",
|
|
||||||
class: "plugin-edit-dialog",
|
|
||||||
},
|
|
||||||
body: () =>
|
|
||||||
h(PluginEditDialogBody, {
|
|
||||||
ref: editDialogBodyRef,
|
|
||||||
pluginId: editPluginId.value,
|
|
||||||
}),
|
|
||||||
async onSubmit() {
|
|
||||||
await editDialogBodyRef.value?.save?.();
|
|
||||||
emitChanged("edit");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyPlugin() {
|
|
||||||
if (!props.copyHandler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runAction(
|
|
||||||
"copy",
|
|
||||||
async () => {
|
|
||||||
await props.copyHandler?.(props.plugin);
|
|
||||||
},
|
|
||||||
{ emitChanged: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportPlugin() {
|
|
||||||
await runAction(
|
|
||||||
"export",
|
|
||||||
async () => {
|
|
||||||
const content = await api.ExportPlugin(props.plugin.id);
|
|
||||||
if (!content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = `${props.plugin.name}.yaml`;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
},
|
|
||||||
{ emitChanged: false }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function publishPlugin() {
|
|
||||||
await publishLocalPlugin(
|
|
||||||
{ ...props.plugin, id: editPluginId.value },
|
|
||||||
{
|
|
||||||
async afterPublish() {
|
|
||||||
emitChanged("publish");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openConfig() {
|
|
||||||
await openConfigDialog({
|
|
||||||
row: props.plugin,
|
|
||||||
onSuccess: async () => {
|
|
||||||
emitChanged("config");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installPlugin() {
|
|
||||||
const fullName = fullNameValue();
|
|
||||||
if (!fullName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runAction("install", async () => {
|
|
||||||
await api.OnlinePluginInstall({
|
|
||||||
fullName,
|
|
||||||
version: props.plugin.latest,
|
|
||||||
});
|
|
||||||
message.success(t("certd.onlinePluginInstallSuccess"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninstallPlugin() {
|
|
||||||
if (!props.plugin.localPluginId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: t("certd.confirm"),
|
|
||||||
content: t("certd.onlinePluginDeleteConfirm", { name: fullNameValue() || props.plugin.title || props.plugin.name }),
|
|
||||||
async onOk() {
|
|
||||||
await runAction("uninstall", async () => {
|
|
||||||
await api.DelObj(props.plugin.localPluginId);
|
|
||||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePlugin() {
|
|
||||||
if (!props.plugin.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: t("certd.confirm"),
|
|
||||||
content: "确定要删除吗?如果该插件已被使用,删除可能会导致流水线执行失败!",
|
|
||||||
async onOk() {
|
|
||||||
await runAction("remove", async () => {
|
|
||||||
await api.DelObj(props.plugin.id);
|
|
||||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePlugin() {
|
|
||||||
const id = props.source === "local" ? props.plugin.id : props.plugin.localPluginId;
|
|
||||||
const canToggleBuiltInPlugin = props.source === "local" && props.plugin.type === "builtIn" && !!props.plugin.name;
|
|
||||||
if (!id && !canToggleBuiltInPlugin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: t("certd.confirm"),
|
|
||||||
content: `${t("certd.confirmToggle")} ${isDisabled.value ? t("certd.enable") : t("certd.disable")}?`,
|
|
||||||
maskClosable: true,
|
|
||||||
async onOk() {
|
|
||||||
await runAction("toggle", async () => {
|
|
||||||
await api.SetDisabled({
|
|
||||||
id: id || undefined,
|
|
||||||
name: props.plugin.name,
|
|
||||||
type: props.source === "local" ? props.plugin.type : "store",
|
|
||||||
disabled: !isDisabled.value,
|
|
||||||
});
|
|
||||||
await pluginStore.reload();
|
|
||||||
message.success(t("certd.operationSuccess"));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullNameValue() {
|
|
||||||
return fullName.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDownloadCount(count: number) {
|
|
||||||
if (count >= 10000) {
|
|
||||||
return `${(count / 10000).toFixed(1).replace(/\.0$/, "")}w`;
|
|
||||||
}
|
|
||||||
return `${count}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleVersionClick() {
|
|
||||||
if (props.source === "local" || !props.plugin.upgradeAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: "升级插件",
|
|
||||||
content: `确认将 ${props.plugin.title || props.plugin.name} 升级到 v${props.plugin.latest} 吗?`,
|
|
||||||
okText: "确认升级",
|
|
||||||
cancelText: "取消",
|
|
||||||
async onOk() {
|
|
||||||
await installPlugin();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.plugin-item-card.plugin-card {
|
|
||||||
width: 100%;
|
|
||||||
border-color: #e5eaf1;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
||||||
transition-duration: 160ms;
|
|
||||||
transition-property: border-color, box-shadow, transform, opacity;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #7dc4ff;
|
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
border-color: #00b7ff;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 2px rgba(0, 183, 255, 0.1),
|
|
||||||
0 8px 24px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-installed {
|
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fffb 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled {
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-status-tag {
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex: none;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
display: flex;
|
|
||||||
min-height: 148px;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 14px 16px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__main {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__icon {
|
|
||||||
flex: none;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__main .plugin-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: #00b7ff;
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__title-wrap {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__title {
|
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #1f2937;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 22px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.plugin-card__title:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__author {
|
|
||||||
display: inline-flex;
|
|
||||||
max-width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
margin-left: auto;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
flex: none;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-self-authored {
|
|
||||||
color: #1677ff;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__actions {
|
|
||||||
display: flex;
|
|
||||||
min-width: max-content;
|
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__tools {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__tool {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #5f6b7a;
|
|
||||||
opacity: 0.68;
|
|
||||||
transition-duration: 160ms;
|
|
||||||
transition-property: color, background-color, opacity, transform;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
display: flex;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
opacity: 1;
|
|
||||||
background: rgba(22, 119, 255, 0.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__action-zone {
|
|
||||||
position: relative;
|
|
||||||
width: 68px;
|
|
||||||
height: 28px;
|
|
||||||
flex: none;
|
|
||||||
|
|
||||||
.plugin-status-tag,
|
|
||||||
.plugin-card__action-button {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
height: 28px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 26px;
|
|
||||||
transition-duration: 160ms;
|
|
||||||
transition-property: opacity, transform, filter;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-status-tag {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__action-button {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateY(4px) scale(0.96);
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-loading,
|
|
||||||
&:hover {
|
|
||||||
.plugin-status-tag {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-4px) scale(0.96);
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__action-button {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__local-action-zone {
|
|
||||||
width: 28px;
|
|
||||||
|
|
||||||
.plugin-card__action-button {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__install-zone {
|
|
||||||
width: 68px;
|
|
||||||
|
|
||||||
.plugin-card__action-button {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__desc {
|
|
||||||
display: -webkit-box;
|
|
||||||
min-height: 40px;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #5f6b7a;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
text-wrap: pretty;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__meta {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
|
|
||||||
.ant-tag {
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__version {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 12px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
line-height: 22px;
|
|
||||||
|
|
||||||
&.is-upgradable {
|
|
||||||
color: #1677ff;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #0958d9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__version-icon {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__download-count {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: #6b7280;
|
|
||||||
|
|
||||||
.fs-icon {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__ai-check-icon {
|
|
||||||
flex: none;
|
|
||||||
color: #52c41a;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-simple {
|
|
||||||
.plugin-card__desc {
|
|
||||||
min-height: 40px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__meta {
|
|
||||||
min-height: 24px;
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__version {
|
|
||||||
min-height: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
.plugin-item-card.plugin-card {
|
|
||||||
border-color: #303030;
|
|
||||||
background: #1f1f1f;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #1668dc;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 2px rgba(22, 119, 255, 0.18),
|
|
||||||
0 8px 24px rgba(0, 0, 0, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-installed {
|
|
||||||
background: linear-gradient(180deg, #1f1f1f 0%, #18251e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__title {
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.plugin-card__title:hover {
|
|
||||||
color: #69b1ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__author,
|
|
||||||
.plugin-card__desc,
|
|
||||||
.plugin-card__version,
|
|
||||||
.plugin-card__download-count {
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__meta {
|
|
||||||
border-top-color: #303030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__tool {
|
|
||||||
color: rgba(255, 255, 255, 0.56);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #69b1ff;
|
|
||||||
background: rgba(22, 119, 255, 0.16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card__version.is-upgradable {
|
|
||||||
color: #69b1ff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #91caff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -114,12 +114,7 @@ const { openConfigDialog } = usePluginConfig();
|
|||||||
|
|
||||||
async function doPluginConfig() {
|
async function doPluginConfig() {
|
||||||
const certApplyInfo = await GetPluginByName("CertApply");
|
const certApplyInfo = await GetPluginByName("CertApply");
|
||||||
await openConfigDialog({
|
await openConfigDialog({ row: certApplyInfo, crudExpose: null });
|
||||||
row: certApplyInfo,
|
|
||||||
onSuccess: async () => {
|
|
||||||
await loadForm();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="less"></style>
|
<style lang="less"></style>
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ import { computed, onMounted, reactive, ref } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import Rollbackable from "./rollbackable.vue";
|
import Rollbackable from "./rollbackable.vue";
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
import * as api from "./api";
|
|
||||||
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
@@ -267,35 +264,12 @@ function clearFormValue(key: string) {
|
|||||||
console.log(key, configForm);
|
console.log(key, configForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginOriginName() {
|
|
||||||
if (props.plugin.fullName) {
|
|
||||||
return props.plugin.fullName;
|
|
||||||
}
|
|
||||||
if (props.plugin.author && props.plugin.name) {
|
|
||||||
return `${props.plugin.author}/${props.plugin.name}`;
|
|
||||||
}
|
|
||||||
return props.plugin.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPluginSetting() {
|
async function loadPluginSetting() {
|
||||||
const originName = getPluginOriginName();
|
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
|
||||||
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName);
|
for (const key in currentPlugin.value.input) {
|
||||||
if (!currentPlugin.value?.input && originName !== props.plugin.name) {
|
|
||||||
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
|
|
||||||
}
|
|
||||||
if (!currentPlugin.value?.input && props.plugin.id) {
|
|
||||||
const plugin = await api.GetObj(props.plugin.localPluginId || props.plugin.id);
|
|
||||||
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
|
|
||||||
currentPlugin.value = {
|
|
||||||
...plugin,
|
|
||||||
...(metadata || {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const input = currentPlugin.value?.input || {};
|
|
||||||
for (const key in input) {
|
|
||||||
configForm[key] = {};
|
configForm[key] = {};
|
||||||
}
|
}
|
||||||
const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting;
|
const setting = props.plugin.sysSetting;
|
||||||
if (setting) {
|
if (setting) {
|
||||||
const settingJson = JSON.parse(setting);
|
const settingJson = JSON.parse(setting);
|
||||||
merge(configForm, settingJson.metadata?.input || {});
|
merge(configForm, settingJson.metadata?.input || {});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
import { Ref, ref, computed } from "vue";
|
import { Ref, ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
import { Modal, message } from "ant-design-vue";
|
import { Modal, message } from "ant-design-vue";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -10,9 +11,9 @@ import KvInput from "/@/components/plugins/common/kv-input.vue";
|
|||||||
import { usePluginConfig } from "./use-config";
|
import { usePluginConfig } from "./use-config";
|
||||||
import { useSettingStore } from "/src/store/settings/index";
|
import { useSettingStore } from "/src/store/settings/index";
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
import PluginAuthorField from "./components/plugin-author-field.vue";
|
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
let lastType = "";
|
let lastType = "";
|
||||||
@@ -49,70 +50,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
const syncLoading = ref(false);
|
|
||||||
const lastSyncTime = ref(0);
|
|
||||||
const autoSyncInterval = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
const syncButtonTitle = computed(() => {
|
|
||||||
if (!lastSyncTime.value) {
|
|
||||||
return t("certd.onlinePluginNotSynced");
|
|
||||||
}
|
|
||||||
return t("certd.onlinePluginLastSyncTime", {
|
|
||||||
time: formatSyncTime(lastSyncTime.value),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatSyncTime(time: number) {
|
|
||||||
return new Date(time).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function needAutoSync(time: number) {
|
|
||||||
if (!time) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return Date.now() - time > autoSyncInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
function canEditStorePlugin(row: any) {
|
|
||||||
if (row.type !== "store") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof row.localEditable === "boolean") {
|
|
||||||
return row.localEditable;
|
|
||||||
}
|
|
||||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
|
||||||
return !row.developerId || (!!bindUserId && Number(row.developerId) === bindUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
|
|
||||||
if (syncLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
syncLoading.value = true;
|
|
||||||
try {
|
|
||||||
await api.OnlinePluginSync();
|
|
||||||
const setting = await api.OnlinePluginSetting();
|
|
||||||
lastSyncTime.value = setting.lastSyncTime || Date.now();
|
|
||||||
await pluginStore.reload();
|
|
||||||
crudExpose.doRefresh();
|
|
||||||
if (options?.showSuccess !== false) {
|
|
||||||
message.success(t("certd.onlinePluginSyncSuccess"));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
syncLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOnlinePluginSetting() {
|
|
||||||
const setting = await api.OnlinePluginSetting();
|
|
||||||
lastSyncTime.value = setting.lastSyncTime || 0;
|
|
||||||
if (needAutoSync(lastSyncTime.value)) {
|
|
||||||
await syncOnlinePlugins({ showSuccess: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOnlinePluginSetting().catch(e => {
|
|
||||||
console.warn("load online plugin setting failed", e);
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
crudOptions: {
|
crudOptions: {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -152,17 +89,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
await openImportDialog({ crudExpose });
|
await openImportDialog({ crudExpose });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncOnline: {
|
|
||||||
show: true,
|
|
||||||
icon: "ion:sync-outline",
|
|
||||||
type: "primary",
|
|
||||||
text: t("certd.onlinePluginSync"),
|
|
||||||
tooltip: { title: syncButtonTitle },
|
|
||||||
loading: syncLoading,
|
|
||||||
async click() {
|
|
||||||
await syncOnlinePlugins();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
clearRuntimeDeps: {
|
clearRuntimeDeps: {
|
||||||
show: true,
|
show: true,
|
||||||
icon: "ion:trash-outline",
|
icon: "ion:trash-outline",
|
||||||
@@ -184,7 +110,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
show: false,
|
|
||||||
rowKey: "name",
|
rowKey: "name",
|
||||||
remove: {
|
remove: {
|
||||||
afterRemove: async context => {
|
afterRemove: async context => {
|
||||||
@@ -200,26 +125,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
buttons: {
|
buttons: {
|
||||||
edit: {
|
edit: {
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return canEditStorePlugin(row);
|
return row.type === "custom";
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return canEditStorePlugin(row);
|
return row.type === "custom";
|
||||||
}),
|
}),
|
||||||
async click({ row }) {
|
|
||||||
const copyRow = { ...row };
|
|
||||||
delete copyRow.fullName;
|
|
||||||
delete copyRow.id;
|
|
||||||
crudExpose.openCopy({
|
|
||||||
row: copyRow,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
remove: {
|
remove: {
|
||||||
order: 999,
|
order: 999,
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return row.type === "custom" || row.type === "store";
|
return row.type === "custom";
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
@@ -228,7 +145,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
title: t("certd.export"),
|
title: t("certd.export"),
|
||||||
type: "link",
|
type: "link",
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return canEditStorePlugin(row);
|
return row.type === "custom";
|
||||||
}),
|
}),
|
||||||
async click({ row }) {
|
async click({ row }) {
|
||||||
const content = await api.ExportPlugin(row.id);
|
const content = await api.ExportPlugin(row.id);
|
||||||
@@ -254,9 +171,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
async click({ row }) {
|
async click({ row }) {
|
||||||
await openConfigDialog({
|
await openConfigDialog({
|
||||||
row,
|
row,
|
||||||
onSuccess: async () => {
|
crudExpose,
|
||||||
crudExpose.doRefresh();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -271,7 +186,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
onSuccess(opts: any) {
|
onSuccess(opts: any) {
|
||||||
crudExpose.doRefresh();
|
if (opts.res?.id) {
|
||||||
|
router.push({
|
||||||
|
name: "SysPluginEdit",
|
||||||
|
query: {
|
||||||
|
id: opts.res.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
@@ -362,18 +286,19 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
title: t("certd.author"),
|
title: t("certd.author"),
|
||||||
type: "text",
|
type: "text",
|
||||||
search: {
|
search: {
|
||||||
component: {
|
|
||||||
name: "a-input",
|
|
||||||
vModel: "value",
|
|
||||||
},
|
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
show: true,
|
show: true,
|
||||||
component: {
|
helper: t("certd.authorHelper"),
|
||||||
name: PluginAuthorField,
|
rules: [
|
||||||
vModel: "modelValue",
|
{ required: true },
|
||||||
},
|
{
|
||||||
|
type: "pattern",
|
||||||
|
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||||
|
message: t("certd.authorRuleMsg"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
width: 200,
|
width: 200,
|
||||||
@@ -390,6 +315,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
column: {
|
column: {
|
||||||
width: 300,
|
width: 300,
|
||||||
cellRender({ row }) {
|
cellRender({ row }) {
|
||||||
|
if (row.type === "custom") {
|
||||||
|
return <router-link to={`/sys/plugin/edit?id=${row.id}`}>{row.title}</router-link>;
|
||||||
|
}
|
||||||
return <div>{row.title}</div>;
|
return <div>{row.title}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -410,7 +338,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
value: "store",
|
value: "custom",
|
||||||
component: {
|
component: {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
@@ -418,6 +346,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
dict: dict({
|
dict: dict({
|
||||||
data: [
|
data: [
|
||||||
{ label: t("certd.builtIn"), value: "builtIn" },
|
{ label: t("certd.builtIn"), value: "builtIn" },
|
||||||
|
{ label: t("certd.custom"), value: "custom" },
|
||||||
{ label: t("certd.store"), value: "store" },
|
{ label: t("certd.store"), value: "store" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -543,7 +472,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t("certd.confirm"),
|
title: t("certd.confirm"),
|
||||||
content: `${t("certd.confirmToggle")} ${!value ? t("certd.disable") : t("certd.enable")}?`,
|
content: `${t("certd.confirmToggle")} ${!value ? t("certd.disable") : t("certd.enable")}?`,
|
||||||
maskClosable: true,
|
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await api.SetDisabled({
|
await api.SetDisabled({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -551,7 +479,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
type: row.type,
|
type: row.type,
|
||||||
disabled: !value,
|
disabled: !value,
|
||||||
});
|
});
|
||||||
crudExpose.doRefresh();
|
await crudExpose.doRefresh();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -562,9 +490,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
group: {
|
group: {
|
||||||
title: t("certd.pluginGroup"),
|
title: t("certd.pluginGroup"),
|
||||||
type: "dict-select",
|
type: "dict-select",
|
||||||
search: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
dict: dict({
|
dict: dict({
|
||||||
url: "/pi/plugin/groupsList",
|
url: "/pi/plugin/groupsList",
|
||||||
label: "title",
|
label: "title",
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-plugin-edit">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">
|
||||||
|
插件编辑
|
||||||
|
<span class="sub">
|
||||||
|
<span class="name flex-inline"> 插件名称:<fs-copyable :model-value="pluginName"></fs-copyable> </span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<a-button class="mr-1" type="primary" :loading="saveLoading" @click="doSave">保存</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="pi-plugin-editor">
|
||||||
|
<div class="base">
|
||||||
|
<a-tabs type="card">
|
||||||
|
<a-tab-pane key="base" tab="插件信息"> </a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<div class="base-body">
|
||||||
|
<fs-form ref="baseFormRef" v-bind="formOptionsRef"></fs-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
<a-tabs type="card">
|
||||||
|
<a-tab-pane key="editor" tab="元数据"> </a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<div class="metadata-body">
|
||||||
|
<code-editor :id="`metadata_${idRef}`" v-model:model-value="plugin.metadata" language="yaml" @save="doSave"></code-editor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="script">
|
||||||
|
<a-tabs type="card">
|
||||||
|
<a-tab-pane key="script" tab="脚本"> </a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<div class="script-body">
|
||||||
|
<code-editor :id="`content_${idRef}`" v-model:model-value="plugin.content" language="javascript" @save="doSave"></code-editor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, provide, ref, Ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import * as api from "./api";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import createCrudOptions from "./crud";
|
||||||
|
import { useColumns } from "@fast-crud/fast-crud";
|
||||||
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
|
//@ts-ignore
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SysPluginEdit",
|
||||||
|
});
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const pluginStore = usePluginStore();
|
||||||
|
const plugin = ref<any>({});
|
||||||
|
const formOptionsRef: Ref = ref();
|
||||||
|
const baseFormRef: Ref = ref({});
|
||||||
|
function initFormOptions() {
|
||||||
|
const formCrudOptions = createCrudOptions({
|
||||||
|
//@ts-ignore
|
||||||
|
crudExpose: {},
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { buildFormOptions } = useColumns();
|
||||||
|
|
||||||
|
const formOptions = buildFormOptions(formCrudOptions.crudOptions, {});
|
||||||
|
|
||||||
|
formOptions.mode = "edit";
|
||||||
|
formOptions.col = {
|
||||||
|
span: 24,
|
||||||
|
};
|
||||||
|
formOptions.labelCol = {
|
||||||
|
style: {
|
||||||
|
width: "100px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
formOptionsRef.value = formOptions;
|
||||||
|
}
|
||||||
|
initFormOptions();
|
||||||
|
|
||||||
|
const idRef = ref(route.query.id);
|
||||||
|
async function getPlugin() {
|
||||||
|
const id = route.query.id;
|
||||||
|
const pluginObj = await api.GetObj(id);
|
||||||
|
plugin.value = pluginObj;
|
||||||
|
|
||||||
|
const baseFrom = {
|
||||||
|
...pluginObj,
|
||||||
|
};
|
||||||
|
if (baseFrom.extra) {
|
||||||
|
baseFrom.extra = yaml.load(baseFrom.extra);
|
||||||
|
}
|
||||||
|
delete baseFrom.metadata;
|
||||||
|
delete baseFrom.content;
|
||||||
|
baseFormRef.value.setFormData(baseFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getPlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginName = computed(() => {
|
||||||
|
if (!plugin.value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (plugin.value.author) {
|
||||||
|
return `${plugin.value.author}/${plugin.value.name}`;
|
||||||
|
}
|
||||||
|
return plugin.value.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
provide("get:plugin", () => {
|
||||||
|
return plugin;
|
||||||
|
});
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
try {
|
||||||
|
yaml.load(plugin.value.metadata);
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = `元数据校验失败:${e.message}`;
|
||||||
|
notification.error({
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
async function doSave() {
|
||||||
|
validate();
|
||||||
|
saveLoading.value = true;
|
||||||
|
const baseForm = baseFormRef.value.getFormData();
|
||||||
|
const form = {
|
||||||
|
...plugin.value,
|
||||||
|
...baseForm,
|
||||||
|
};
|
||||||
|
if (form.extra) {
|
||||||
|
form.extra = yaml.dump(form.extra);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.UpdateObj(form);
|
||||||
|
notification.success({
|
||||||
|
message: "保存成功",
|
||||||
|
});
|
||||||
|
pluginStore.clear();
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doTest() {
|
||||||
|
await doSave();
|
||||||
|
const result = await api.DoTest({
|
||||||
|
id: plugin.value.id,
|
||||||
|
input: {},
|
||||||
|
});
|
||||||
|
notification.success({
|
||||||
|
message: "测试已开始",
|
||||||
|
description: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.page-plugin-edit {
|
||||||
|
.pi-plugin-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
.fs-editor-code {
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 30%;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
.base-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
width: 600px;
|
||||||
|
max-width: 30%;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
.metadata-body {
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-editor {
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
.ant-tabs-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.metadata-source {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.script {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
.script-body {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,169 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<fs-page class="page-sys-plugin">
|
<fs-page class="page-cert">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ t("certd.pluginManagement") }}
|
{{ t("certd.pluginManagement") }}
|
||||||
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<fs-crud ref="crudRef" class="plugin-card-crud" v-bind="crudBinding">
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
<a-empty v-if="pluginList.length === 0" class="plugin-card-empty" />
|
<!-- <template #pagination-left>-->
|
||||||
<div v-else class="plugin-card-grid">
|
<!-- <a-tooltip :title="t('certd.batchDelete')">-->
|
||||||
<PluginItemCard
|
<!-- <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>-->
|
||||||
v-for="item of pluginList"
|
<!-- </a-tooltip>-->
|
||||||
:key="item.id || item.name"
|
<!-- </template>-->
|
||||||
:source="getPluginCardSource(item)"
|
|
||||||
:plugin="item"
|
|
||||||
:show-config="settingStore.isComm"
|
|
||||||
:editable="canEditPlugin(item)"
|
|
||||||
:copy-handler="copyPlugin"
|
|
||||||
@changed="handlePluginChanged"
|
|
||||||
@click="openPluginDetail"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</fs-crud>
|
</fs-crud>
|
||||||
<OnlinePluginDetailModal v-model:open="detailVisible" :plugin="detailPlugin" @installed="handleDetailInstalled" />
|
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useFs } from "@fast-crud/fast-crud";
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
import PluginItemCard from "./components/plugin-item-card.vue";
|
import { message, Modal } from "ant-design-vue";
|
||||||
import OnlinePluginDetailModal from "./components/online-plugin-detail-modal.vue";
|
import { DeleteBatch } from "./api";
|
||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
import { useMounted } from "/@/use/use-mounted";
|
import { useMounted } from "/@/use/use-mounted";
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import { useSettingStore } from "/src/store/settings";
|
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "SysPlugin",
|
name: "SysPlugin",
|
||||||
});
|
});
|
||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
||||||
const detailVisible = ref(false);
|
|
||||||
const detailPlugin = ref<any>();
|
|
||||||
|
|
||||||
const pluginList = computed(() => {
|
const selectedRowKeys = context.selectedRowKeys;
|
||||||
return crudBinding.value?.data || [];
|
const handleBatchDelete = () => {
|
||||||
});
|
if (selectedRowKeys.value?.length > 0) {
|
||||||
|
Modal.confirm({
|
||||||
function getPluginCardSource(row: any) {
|
title: t("certd.pluginManagement"),
|
||||||
return row.type === "store" && (row.appId || row.developerId) ? "market" : "local";
|
content: t("certd.batchDeleteConfirm", { count: selectedRowKeys.value.length }),
|
||||||
}
|
async onOk() {
|
||||||
|
await DeleteBatch(selectedRowKeys.value);
|
||||||
function canEditPlugin(row: any) {
|
message.info(t("certd.deleteSuccess"));
|
||||||
if (row.type === "custom") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (row.type !== "store") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof row.localEditable === "boolean") {
|
|
||||||
return row.localEditable;
|
|
||||||
}
|
|
||||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
|
||||||
return !row.developerId || (!!bindUserId && Number(row.developerId) === bindUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPluginDetail(row: any) {
|
|
||||||
if (row.type !== "store" || !(row.fullName || (row.author && row.name))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
detailPlugin.value = row;
|
|
||||||
detailVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDetailInstalled() {
|
|
||||||
crudExpose.doRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyPlugin(row: any) {
|
|
||||||
const copyRow = { ...row };
|
|
||||||
delete copyRow.fullName;
|
|
||||||
delete copyRow.id;
|
|
||||||
delete copyRow.developerId;
|
|
||||||
delete copyRow.appId;
|
|
||||||
delete copyRow.latest;
|
|
||||||
delete copyRow.status;
|
|
||||||
delete copyRow.downloadCount;
|
|
||||||
delete copyRow.score;
|
|
||||||
await crudExpose.openCopy(
|
|
||||||
{
|
|
||||||
row: copyRow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSuccess() {
|
|
||||||
crudExpose.doRefresh();
|
crudExpose.doRefresh();
|
||||||
|
selectedRowKeys.value = [];
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
} else {
|
||||||
}
|
message.error(t("certd.selectRecordFirst"));
|
||||||
|
|
||||||
async function handlePluginChanged(payload: { action: string }) {
|
|
||||||
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove" || payload.action === "copy") {
|
|
||||||
await pluginStore.reload();
|
|
||||||
}
|
}
|
||||||
crudExpose.doRefresh();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 页面打开后获取列表数据
|
// 页面打开后获取列表数据
|
||||||
useMounted(async () => {
|
useMounted(async () => {
|
||||||
await crudExpose.doRefresh();
|
await crudExpose.doRefresh();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less"></style>
|
||||||
.page-sys-plugin {
|
|
||||||
.fs-page-content {
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-crud {
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-empty {
|
|
||||||
padding: 72px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
padding: 4px 10px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 1400px) {
|
|
||||||
.page-sys-plugin {
|
|
||||||
.plugin-card-grid {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.page-sys-plugin {
|
|
||||||
.plugin-card-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.page-sys-plugin {
|
|
||||||
.plugin-card-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function usePluginConfig() {
|
|||||||
|
|
||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async function openConfigDialog({ row, onSuccess }) {
|
async function openConfigDialog({ row, crudExpose }) {
|
||||||
const configEditorRef = ref();
|
const configEditorRef = ref();
|
||||||
function createCrudOptions() {
|
function createCrudOptions() {
|
||||||
return {
|
return {
|
||||||
@@ -33,10 +33,10 @@ export function usePluginConfig() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async afterSubmit() {
|
afterSubmit() {
|
||||||
notification.success({ message: t("certd.operationSuccess") });
|
notification.success({ message: t("certd.operationSuccess") });
|
||||||
if (onSuccess) {
|
if (crudExpose) {
|
||||||
await onSuccess();
|
crudExpose.doRefresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async doSubmit({}: any) {
|
async doSubmit({}: any) {
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ export function usePluginImport() {
|
|||||||
async doSubmit({ form }: any) {
|
async doSubmit({ form }: any) {
|
||||||
return await api.ImportPlugin({
|
return await api.ImportPlugin({
|
||||||
...form,
|
...form,
|
||||||
type: "store",
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
.sys-plugin-publish {
|
|
||||||
margin: 2px;
|
|
||||||
|
|
||||||
&__hero {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 18px;
|
|
||||||
border: 1px solid #e4ecf7;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: linear-gradient(135deg, #f7fbff 0%, #eef5ff 55%, #f8fbff 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon-wrap {
|
|
||||||
display: flex;
|
|
||||||
width: 54px;
|
|
||||||
height: 54px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid rgba(64, 120, 192, 0.16);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 8px 22px rgba(31, 77, 132, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
color: #2f6fbb;
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__hero-main {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title-line,
|
|
||||||
&__version-line,
|
|
||||||
&__author-register,
|
|
||||||
&__review-icons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title-line {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: #172033;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__desc {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: #526172;
|
|
||||||
line-height: 22px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sections,
|
|
||||||
&__latest-version {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sections {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__latest-version {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__meta-full {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__version {
|
|
||||||
color: #1f2937;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__review-icons {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__review-icon {
|
|
||||||
display: block;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&.is-ai {
|
|
||||||
&.is-passed {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-pending {
|
|
||||||
color: #faad14;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-rejected {
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-admin {
|
|
||||||
color: #1677ff;
|
|
||||||
|
|
||||||
&.is-rejected {
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__rejected-reasons {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-left: 3px solid #ff7875;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff2f0;
|
|
||||||
color: #a61d24;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 19px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__prompt {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
import { reactive, ref } from "vue";
|
|
||||||
import { message } from "ant-design-vue";
|
|
||||||
import { useFormDialog } from "/@/use/use-dialog";
|
|
||||||
import { useI18n } from "/src/locales";
|
|
||||||
import { usePluginStore } from "/@/store/plugin";
|
|
||||||
import * as api from "./api";
|
|
||||||
import "./use-publish.less";
|
|
||||||
|
|
||||||
type PublishManagerResult =
|
|
||||||
| {
|
|
||||||
action: "publish";
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
action: "registered";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
action: "cancel";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function usePluginPublish() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { openFormDialog } = useFormDialog();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
const publishingPluginId = ref<number | string>("");
|
|
||||||
|
|
||||||
const authorNamePattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
||||||
|
|
||||||
function getPluginTypeLabel(pluginType: string) {
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
access: t("certd.auth"),
|
|
||||||
dnsProvider: t("certd.dns"),
|
|
||||||
deploy: t("certd.deployPlugin"),
|
|
||||||
};
|
|
||||||
return labelMap[pluginType] || pluginType || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPublishingPlugin(row: any) {
|
|
||||||
return !!row?.id && publishingPluginId.value === row.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerPluginAuthor() {
|
|
||||||
return new Promise<api.OnlinePluginAuthorBean | undefined>(resolve => {
|
|
||||||
let resolved = false;
|
|
||||||
const resolveAuthor = (author?: api.OnlinePluginAuthorBean) => {
|
|
||||||
if (resolved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolved = true;
|
|
||||||
resolve(author);
|
|
||||||
};
|
|
||||||
|
|
||||||
void openFormDialog({
|
|
||||||
title: t("certd.onlinePluginAuthorRegister"),
|
|
||||||
wrapper: {
|
|
||||||
width: 560,
|
|
||||||
onClosed() {
|
|
||||||
resolveAuthor();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initialForm: {
|
|
||||||
name: "",
|
|
||||||
nameConfirm: "",
|
|
||||||
desc: "",
|
|
||||||
},
|
|
||||||
async onSubmit(form: any) {
|
|
||||||
if (form.name?.trim() !== form.nameConfirm?.trim()) {
|
|
||||||
throw new Error("两次输入的作者名称不一致");
|
|
||||||
}
|
|
||||||
const author = await api.OnlinePluginAuthorAdd({
|
|
||||||
name: form.name,
|
|
||||||
displayName: form.displayName,
|
|
||||||
desc: form.desc,
|
|
||||||
});
|
|
||||||
resolveAuthor(author);
|
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
name: {
|
|
||||||
title: t("certd.onlinePluginAuthorName"),
|
|
||||||
type: "text",
|
|
||||||
form: {
|
|
||||||
col: { span: 24 },
|
|
||||||
helper: t("certd.onlinePluginAuthorNameHelper"),
|
|
||||||
rules: [
|
|
||||||
{ required: true, message: t("certd.onlinePluginAuthorNameRequired") },
|
|
||||||
{
|
|
||||||
pattern: authorNamePattern,
|
|
||||||
message: t("certd.onlinePluginAuthorNameRuleMsg"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nameConfirm: {
|
|
||||||
title: "确认作者名称",
|
|
||||||
type: "text",
|
|
||||||
form: {
|
|
||||||
col: { span: 24 },
|
|
||||||
helper: "请再次输入相同名称。作者名称注册后不允许修改。",
|
|
||||||
rules: [{ required: true, message: "请再次输入作者名称" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusLabel(status?: string) {
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
draft: t("certd.onlinePluginStatusDraft"),
|
|
||||||
reviewing: t("certd.onlinePluginStatusReviewing"),
|
|
||||||
published: t("certd.onlinePluginStatusPublished"),
|
|
||||||
rejected: t("certd.onlinePluginStatusRejected"),
|
|
||||||
offline: t("certd.onlinePluginStatusOffline"),
|
|
||||||
};
|
|
||||||
return labelMap[status || ""] || status || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeVersion(version?: string) {
|
|
||||||
return (version || "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isReviewingVersion(version: api.OnlinePluginVersionBean) {
|
|
||||||
return version.status === "reviewing" || version.reviewStatus === "ai_pending" || version.reviewStatus === "pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareVersion(current: string, last: string) {
|
|
||||||
const currentParts = normalizeVersion(current)
|
|
||||||
.replace(/^v/i, "")
|
|
||||||
.split(".")
|
|
||||||
.map(item => Number.parseInt(item, 10));
|
|
||||||
const lastParts = normalizeVersion(last)
|
|
||||||
.replace(/^v/i, "")
|
|
||||||
.split(".")
|
|
||||||
.map(item => Number.parseInt(item, 10));
|
|
||||||
const maxLength = Math.max(currentParts.length, lastParts.length);
|
|
||||||
for (let index = 0; index < maxLength; index++) {
|
|
||||||
const currentPart = currentParts[index] || 0;
|
|
||||||
const lastPart = lastParts[index] || 0;
|
|
||||||
if (currentPart > lastPart) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (currentPart < lastPart) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLatestPublishedVersion(versions: api.OnlinePluginVersionBean[], latest?: string) {
|
|
||||||
const publishedVersions = versions.filter(item => item.status === "published");
|
|
||||||
const latestText = normalizeVersion(latest);
|
|
||||||
if (latestText) {
|
|
||||||
const matched = publishedVersions.find(item => normalizeVersion(item.version) === latestText);
|
|
||||||
if (matched) {
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return publishedVersions[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultPublishVersion(row: any, localPlugin: any, reviewingVersion?: api.OnlinePluginVersionBean) {
|
|
||||||
const localVersion = normalizeVersion(row.version || localPlugin.version);
|
|
||||||
if (reviewingVersion) {
|
|
||||||
return localVersion || normalizeVersion(reviewingVersion.version);
|
|
||||||
}
|
|
||||||
return localVersion || "1.0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionError(version: string, baselineVersion?: api.OnlinePluginVersionBean) {
|
|
||||||
const value = normalizeVersion(version);
|
|
||||||
if (!value) {
|
|
||||||
return t("certd.onlinePluginPublishVersionRequired");
|
|
||||||
}
|
|
||||||
if (!/^v?\d+(\.\d+)*$/i.test(value)) {
|
|
||||||
return t("certd.onlinePluginVersionFormatError");
|
|
||||||
}
|
|
||||||
if (baselineVersion?.version && compareVersion(value, baselineVersion.version) <= 0) {
|
|
||||||
return t("certd.onlinePluginVersionBaselineError", { version: baselineVersion.version });
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusColor(status?: string) {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
draft: "default",
|
|
||||||
reviewing: "processing",
|
|
||||||
published: "success",
|
|
||||||
rejected: "error",
|
|
||||||
offline: "warning",
|
|
||||||
};
|
|
||||||
return colorMap[status || ""] || "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAiCheckIcon(version: api.OnlinePluginVersionBean) {
|
|
||||||
if (version.aiCheckStatus === "rejected") {
|
|
||||||
return "lucide:shield-x";
|
|
||||||
}
|
|
||||||
if (version.aiCheckStatus === "pending") {
|
|
||||||
return "lucide:shield-alert";
|
|
||||||
}
|
|
||||||
return "lucide:shield-check";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAiCheckTooltip(version: api.OnlinePluginVersionBean) {
|
|
||||||
const result = `${version.aiCheckResult || ""}`.trim();
|
|
||||||
if (version.aiCheckStatus === "rejected") {
|
|
||||||
return result ? `AI 安全审查未通过:${result}` : "AI 安全审查未通过";
|
|
||||||
}
|
|
||||||
if (version.aiCheckStatus === "pending" || version.reviewStatus === "ai_pending") {
|
|
||||||
return result ? `AI 安全审查中:${result}` : "AI 安全审查中";
|
|
||||||
}
|
|
||||||
return result ? `AI 安全审查已通过:${result}` : "AI 安全审查已通过";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAdminRejected(version: api.OnlinePluginVersionBean) {
|
|
||||||
return version.reviewStatus === "rejected" && !!`${version.reviewReason || ""}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasAdminReview(version: api.OnlinePluginVersionBean) {
|
|
||||||
return version.reviewStatus === "passed" || isAdminRejected(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAdminReviewTooltip(version: api.OnlinePluginVersionBean) {
|
|
||||||
if (isAdminRejected(version)) {
|
|
||||||
return `管理员审核已拒绝:${version.reviewReason}`;
|
|
||||||
}
|
|
||||||
return "管理员审核已通过";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderReviewIndicators(version: api.OnlinePluginVersionBean) {
|
|
||||||
const aiCheckStatus = version.aiCheckStatus || (version.reviewStatus === "ai_pending" ? "pending" : "");
|
|
||||||
return (
|
|
||||||
<span class="sys-plugin-publish__review-icons">
|
|
||||||
{aiCheckStatus ? (
|
|
||||||
<a-tooltip title={getAiCheckTooltip(version)}>
|
|
||||||
<fs-icon class={["sys-plugin-publish__review-icon", "is-ai", `is-${aiCheckStatus}`]} icon={getAiCheckIcon({ ...version, aiCheckStatus })} />
|
|
||||||
</a-tooltip>
|
|
||||||
) : null}
|
|
||||||
{hasAdminReview(version) ? (
|
|
||||||
<a-tooltip title={getAdminReviewTooltip(version)}>
|
|
||||||
<fs-icon class={["sys-plugin-publish__review-icon", "is-admin", { "is-rejected": isAdminRejected(version) }]} icon={isAdminRejected(version) ? "lucide:user-x" : "lucide:user-check"} />
|
|
||||||
</a-tooltip>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRejectedReasons(version: api.OnlinePluginVersionBean) {
|
|
||||||
const aiReason = version.aiCheckStatus === "rejected" ? `${version.aiCheckResult || ""}`.trim() : "";
|
|
||||||
if (!aiReason) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div class="sys-plugin-publish__rejected-reasons">
|
|
||||||
<div class="sys-plugin-publish__rejected-reason is-ai">AI 审查未通过:{aiReason}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPublishManager(row: any, info: Awaited<ReturnType<typeof api.OnlinePluginPublishInfo>>) {
|
|
||||||
const author = info.author;
|
|
||||||
const marketPlugin = info.marketPlugin;
|
|
||||||
const versions = info.versions || [];
|
|
||||||
const localPlugin = info.localPlugin || row;
|
|
||||||
const reviewingVersion = versions.find(isReviewingVersion);
|
|
||||||
const rejectedVersion = versions.find(item => item.status === "rejected" || item.reviewStatus === "rejected");
|
|
||||||
const replaceableVersion = reviewingVersion || rejectedVersion;
|
|
||||||
const latestPublished = findLatestPublishedVersion(versions, marketPlugin?.latest);
|
|
||||||
const latestVersion = versions[0];
|
|
||||||
const isFirstPublish = !marketPlugin && versions.length === 0;
|
|
||||||
const publishMode = isFirstPublish ? "first" : replaceableVersion ? "cover" : "new";
|
|
||||||
const defaultVersion = getDefaultPublishVersion(row, localPlugin, replaceableVersion);
|
|
||||||
const versionBaseline = publishMode === "cover" ? latestPublished : latestVersion;
|
|
||||||
const state = reactive({
|
|
||||||
version: defaultVersion,
|
|
||||||
error: getVersionError(defaultVersion, versionBaseline),
|
|
||||||
});
|
|
||||||
|
|
||||||
let formWrapper: any;
|
|
||||||
let resolved = false;
|
|
||||||
let resolveResult: (result: PublishManagerResult) => void;
|
|
||||||
|
|
||||||
function getPublishModeTitle() {
|
|
||||||
if (publishMode === "first") {
|
|
||||||
return "首次发布";
|
|
||||||
}
|
|
||||||
if (publishMode === "cover") {
|
|
||||||
return "覆盖待审核版本";
|
|
||||||
}
|
|
||||||
return "发布新版本";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPublishModeTip() {
|
|
||||||
if (publishMode === "first") {
|
|
||||||
return "插件还没有提交到在线市场,确认后会创建插件并提交发布审核。";
|
|
||||||
}
|
|
||||||
if (publishMode === "cover") {
|
|
||||||
if (rejectedVersion && rejectedVersion === replaceableVersion) {
|
|
||||||
return `当前 v${rejectedVersion.version || "-"} 已被拒绝,本次提交可覆盖该版本并重新提交审核。`;
|
|
||||||
}
|
|
||||||
return `当前已有 v${reviewingVersion?.version || "-"} 正在审核中,本次提交会撤回并覆盖该审核版本。`;
|
|
||||||
}
|
|
||||||
return `当前最新版本为 v${latestVersion?.version || latestPublished?.version || marketPlugin?.latest || "-"},新提交的版本号必须更高。`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMetaItem(label: string, value: any) {
|
|
||||||
return (
|
|
||||||
<div class="cd-meta-item">
|
|
||||||
<span class="cd-meta-label">{label}</span>
|
|
||||||
<span class="cd-meta-value">{value || "-"}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAuthor() {
|
|
||||||
if (info.authorRegistered) {
|
|
||||||
return author?.displayName || author?.name || "-";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span class="sys-plugin-publish__author-register">
|
|
||||||
<span class="cd-text-danger">{t("certd.onlinePluginAuthorNotRegistered")}</span>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
onClick={async (event: any) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
const createdAuthor = await registerPluginAuthor();
|
|
||||||
if (!createdAuthor?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolveResult({ action: "registered" });
|
|
||||||
formWrapper?.close?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("certd.onlinePluginAuthorRegister")}
|
|
||||||
</a-button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLatestVersion() {
|
|
||||||
if (!latestVersion) {
|
|
||||||
return <div class="cd-text-muted">{t("certd.onlinePluginNoSubmittedVersion")}</div>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div class="sys-plugin-publish__latest-version">
|
|
||||||
<div class="sys-plugin-publish__version-line">
|
|
||||||
<strong class="sys-plugin-publish__version">v{latestVersion.version || "-"}</strong>
|
|
||||||
<a-tag color={getStatusColor(latestVersion.status)}>{getStatusLabel(latestVersion.status)}</a-tag>
|
|
||||||
{renderReviewIndicators(latestVersion)}
|
|
||||||
</div>
|
|
||||||
{renderRejectedReasons(latestVersion)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContent() {
|
|
||||||
return (
|
|
||||||
<div class="sys-plugin-publish">
|
|
||||||
<div class="sys-plugin-publish__hero">
|
|
||||||
<div class="sys-plugin-publish__icon-wrap">
|
|
||||||
<fs-icon class="sys-plugin-publish__icon" icon={localPlugin.icon || row.icon || "clarity:plugin-line"} />
|
|
||||||
</div>
|
|
||||||
<div class="sys-plugin-publish__hero-main">
|
|
||||||
<div class="sys-plugin-publish__title-line">
|
|
||||||
<div class="sys-plugin-publish__title">{localPlugin.title || row.title || localPlugin.name || row.name || "-"}</div>
|
|
||||||
<a-tag color="blue">{getPublishModeTitle()}</a-tag>
|
|
||||||
</div>
|
|
||||||
<div class="sys-plugin-publish__desc">{localPlugin.desc || row.desc || "暂无描述"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sys-plugin-publish__sections">
|
|
||||||
<div class="cd-card-section">
|
|
||||||
<div class="cd-card-section-title">插件信息</div>
|
|
||||||
<div class="cd-meta-grid">
|
|
||||||
{renderMetaItem(t("certd.pluginName"), localPlugin.name || row.name)}
|
|
||||||
{renderMetaItem(t("certd.pluginType"), getPluginTypeLabel(localPlugin.pluginType || row.pluginType))}
|
|
||||||
{renderMetaItem(t("certd.pluginGroup"), localPlugin.group || row.group)}
|
|
||||||
{renderMetaItem(t("certd.onlinePluginPublishStatus"), marketPlugin ? getStatusLabel(marketPlugin.status) : t("certd.onlinePluginNotSubmitted"))}
|
|
||||||
<div class="sys-plugin-publish__meta-full">{renderMetaItem(t("certd.author"), renderAuthor())}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cd-card-section">
|
|
||||||
<div class="cd-card-section-title">{t("certd.onlinePluginLatestVersion")}</div>
|
|
||||||
{renderLatestVersion()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cd-card-section">
|
|
||||||
<div class="cd-card-section-title cd-card-section-title--compact">{t("certd.onlinePluginCurrentRelease")}</div>
|
|
||||||
<div class={["cd-tip-box sys-plugin-publish__prompt", { "cd-tip-box-warning": publishMode === "cover" }]}>
|
|
||||||
<div class="cd-tip-title">{t("certd.onlinePluginPublishPrompt")}</div>
|
|
||||||
<div>{getPublishModeTip()}</div>
|
|
||||||
</div>
|
|
||||||
<div class="cd-form-row">
|
|
||||||
<label class="cd-form-label">{t("certd.version")}</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={state.version}
|
|
||||||
placeholder="请输入版本号,例如 1.0.1"
|
|
||||||
class={["cd-text-input", { "cd-text-input-error": state.error }]}
|
|
||||||
onInput={(event: any) => {
|
|
||||||
state.version = event?.target?.value || "";
|
|
||||||
state.error = getVersionError(state.version, versionBaseline);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{state.error ? <div class="cd-field-error">{state.error}</div> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<PublishManagerResult>(resolve => {
|
|
||||||
resolveResult = (result: PublishManagerResult) => {
|
|
||||||
if (resolved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolved = true;
|
|
||||||
resolve(result);
|
|
||||||
};
|
|
||||||
void openFormDialog({
|
|
||||||
title: marketPlugin ? t("certd.onlinePluginPublishManage") : t("certd.onlinePluginPublish"),
|
|
||||||
columns: {},
|
|
||||||
body: () => renderContent(),
|
|
||||||
wrapper: {
|
|
||||||
width: 920,
|
|
||||||
buttons: {
|
|
||||||
reset: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
ok: {
|
|
||||||
show: true,
|
|
||||||
text: t("certd.onlinePluginPublishSubmit"),
|
|
||||||
disabled: !info.authorRegistered,
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
show: true,
|
|
||||||
text: "取消",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onClosed() {
|
|
||||||
resolveResult({ action: "cancel" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async onSubmit() {
|
|
||||||
if (!info.authorRegistered) {
|
|
||||||
throw new Error(t("certd.onlinePluginAuthorNotRegistered"));
|
|
||||||
}
|
|
||||||
state.error = getVersionError(state.version, versionBaseline);
|
|
||||||
if (state.error) {
|
|
||||||
throw new Error(state.error);
|
|
||||||
}
|
|
||||||
resolveResult({ action: "publish", version: normalizeVersion(state.version) });
|
|
||||||
},
|
|
||||||
}).then(wrapper => {
|
|
||||||
formWrapper = wrapper;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function publishLocalPlugin(row: any, options?: { beforePublish?: () => Promise<any>; afterPublish?: () => Promise<void> }) {
|
|
||||||
if (!row?.id || (row.type !== "store" && row.type !== "custom")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const info = await api.OnlinePluginPublishInfo({
|
|
||||||
id: row.id,
|
|
||||||
});
|
|
||||||
const action = await openPublishManager(row, info);
|
|
||||||
if (action.action === "cancel") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action.action === "registered") {
|
|
||||||
await publishLocalPlugin(row, options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action.action !== "publish") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
publishingPluginId.value = row.id;
|
|
||||||
try {
|
|
||||||
const publishRow = (await options?.beforePublish?.()) || row;
|
|
||||||
await api.OnlinePluginPublish({
|
|
||||||
id: publishRow.id,
|
|
||||||
version: action.version,
|
|
||||||
});
|
|
||||||
if (normalizeVersion(publishRow.version) !== action.version) {
|
|
||||||
await api.UpdateObj({
|
|
||||||
...publishRow,
|
|
||||||
version: action.version,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await pluginStore.reload();
|
|
||||||
await options?.afterPublish?.();
|
|
||||||
message.success(t("certd.onlinePluginPublishSuccess"));
|
|
||||||
} finally {
|
|
||||||
publishingPluginId.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isPublishingPlugin,
|
|
||||||
publishLocalPlugin,
|
|
||||||
registerPluginAuthor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
LEGO_VERSION=4.30.1
|
LEGO_VERSION=4.30.1
|
||||||
JKS_GO_VERSION=1.0.3
|
JKS_GO_VERSION=1.0.3
|
||||||
certd_plugin_loadmode=dev
|
certd_plugin_loadmode=dev
|
||||||
# certd_release_mode=stable
|
|
||||||
+1
-1
@@ -30,4 +30,4 @@ typeorm:
|
|||||||
|
|
||||||
account:
|
account:
|
||||||
server:
|
server:
|
||||||
baseUrl: 'http://localhost:1017/subject'
|
baseUrl: 'http://127.0.0.1:1017/subject'
|
||||||
@@ -26,9 +26,8 @@ typeorm:
|
|||||||
#
|
#
|
||||||
#plus:
|
#plus:
|
||||||
# server:
|
# server:
|
||||||
# baseUrls: ['http://localhost:11007']
|
# baseUrls: ['http://127.0.0.1:11007']
|
||||||
#
|
#
|
||||||
#account:
|
#account:
|
||||||
# server:
|
# server:
|
||||||
# baseUrl: 'http://localhost:1017/subject'
|
# baseUrl: 'http://127.0.0.1:1017/subject'
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ typeorm:
|
|||||||
#
|
#
|
||||||
#account:
|
#account:
|
||||||
# server:
|
# server:
|
||||||
# baseUrl: 'http://localhost:1017/subject'
|
# baseUrl: 'http://127.0.0.1:1017/subject'
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
ALTER TABLE `cd_audit_log` ADD COLUMN `scope` varchar(32) NOT NULL DEFAULT 'user';
|
|
||||||
CREATE INDEX `index_audit_log_scope` ON `cd_audit_log` (`scope`);
|
|
||||||
|
|
||||||
ALTER TABLE `cd_audit_log` ADD COLUMN `success` tinyint(1) NOT NULL DEFAULT 1;
|
|
||||||
|
|
||||||
-- 审计日志表索引
|
|
||||||
CREATE INDEX `index_audit_log_type` ON `cd_audit_log` (`type`);
|
|
||||||
CREATE INDEX `index_audit_log_create_time` ON `cd_audit_log` (`create_time`);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
|
|
||||||
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
|
||||||
|
|
||||||
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT true;
|
|
||||||
|
|
||||||
-- 审计日志表索引
|
|
||||||
CREATE INDEX "index_audit_log_type" ON "cd_audit_log" ("type");
|
|
||||||
CREATE INDEX "index_audit_log_create_time" ON "cd_audit_log" ("create_time");
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
|
|
||||||
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
|
||||||
|
|
||||||
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT (1);
|
|
||||||
-- 删除project_name字段
|
|
||||||
ALTER TABLE "cd_audit_log" DROP COLUMN "project_name";
|
|
||||||
|
|
||||||
-- 审计日志表索引
|
|
||||||
CREATE INDEX "index_audit_log_type" ON "cd_audit_log" ("type");
|
|
||||||
CREATE INDEX "index_audit_log_create_time" ON "cd_audit_log" ("create_time");
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
ALTER TABLE "pi_plugin" ADD COLUMN "app_id" integer;
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "developer_id" integer;
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "full_name" varchar(200);
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "latest" varchar(100);
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "status" varchar(100);
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "download_count" integer;
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "score" real;
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "sync_time" integer;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX "index_plugin_full_name" ON "pi_plugin" ("full_name");
|
|
||||||
CREATE INDEX "index_plugin_plugin_type" ON "pi_plugin" ("pluginType");
|
|
||||||
CREATE INDEX "index_plugin_group" ON "pi_plugin" ("group");
|
|
||||||
CREATE INDEX "index_plugin_developer_id" ON "pi_plugin" ("developer_id");
|
|
||||||
CREATE INDEX "index_plugin_sync_time" ON "pi_plugin" ("sync_time");
|
|
||||||
|
|
||||||
UPDATE "pi_plugin" SET "type" = 'store' WHERE "type" = 'custom';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE "pi_plugin" ADD COLUMN "installed" boolean NOT NULL DEFAULT true;
|
|
||||||
|
|
||||||
UPDATE "pi_plugin" SET "installed" = true;
|
|
||||||
|
|
||||||
ALTER TABLE "pi_plugin" ADD COLUMN "ai_check_status" varchar(32) NOT NULL DEFAULT '';
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"dev-start": "cross-env NODE_ENV=dev node --optimize-for-size ./bootstrap.js",
|
"dev-start": "cross-env NODE_ENV=dev node --optimize-for-size ./bootstrap.js",
|
||||||
"dc": "cd ../../../ && pnpm run dev",
|
"dc": "cd ../../../ && pnpm run dev",
|
||||||
"dev": "cross-env NODE_ENV=dev mwtsc --watch --run @midwayjs/mock/app",
|
"dev": "cross-env NODE_ENV=dev mwtsc --watch --run @midwayjs/mock/app",
|
||||||
"dev-localcomm": "cross-env NODE_ENV=dev-localcomm mwtsc --watch --run @midwayjs/mock/app",
|
"dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app",
|
||||||
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
|
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
|
||||||
"dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
|
"dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
|
||||||
"dev-mysql": "cross-env NODE_ENV=dev-mysql mwtsc --watch --run @midwayjs/mock/app",
|
"dev-mysql": "cross-env NODE_ENV=dev-mysql mwtsc --watch --run @midwayjs/mock/app",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import cors from "@koa/cors";
|
|||||||
import { GlobalExceptionMiddleware } from "./middleware/global-exception.js";
|
import { GlobalExceptionMiddleware } from "./middleware/global-exception.js";
|
||||||
import { PreviewMiddleware } from "./middleware/preview.js";
|
import { PreviewMiddleware } from "./middleware/preview.js";
|
||||||
import { AuthorityMiddleware } from "./middleware/authority.js";
|
import { AuthorityMiddleware } from "./middleware/authority.js";
|
||||||
import { AuditLogMiddleware } from "./middleware/audit-log.js";
|
|
||||||
import { logger } from "@certd/basic";
|
import { logger } from "@certd/basic";
|
||||||
import { ResetPasswdMiddleware } from "./middleware/reset-passwd/middleware.js";
|
import { ResetPasswdMiddleware } from "./middleware/reset-passwd/middleware.js";
|
||||||
import DefaultConfig from "./config/config.default.js";
|
import DefaultConfig from "./config/config.default.js";
|
||||||
@@ -42,10 +41,10 @@ process.on("uncaughtException", error => {
|
|||||||
// }
|
// }
|
||||||
// setInterval(log, 200);
|
// setInterval(log, 200);
|
||||||
// log()
|
// log()
|
||||||
// }127.0.0.1
|
// }
|
||||||
|
|
||||||
|
|
||||||
// startHeapLog();
|
// startHeapLog();
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
detectorOptions: {
|
detectorOptions: {
|
||||||
ignore: ["**/plugins/**"],
|
ignore: ["**/plugins/**"],
|
||||||
@@ -114,7 +113,6 @@ export class MainConfiguration {
|
|||||||
PreviewMiddleware,
|
PreviewMiddleware,
|
||||||
//授权处理
|
//授权处理
|
||||||
AuthorityMiddleware,
|
AuthorityMiddleware,
|
||||||
AuditLogMiddleware,
|
|
||||||
|
|
||||||
//resetPasswd,重置密码模式下不提供服务
|
//resetPasswd,重置密码模式下不提供服务
|
||||||
ResetPasswdMiddleware,
|
ResetPasswdMiddleware,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import { getReleaseMode, normalizeReleaseVersion } from "./app-controller.js";
|
import { normalizeReleaseVersion } from "./app-controller.js";
|
||||||
|
|
||||||
describe("AppController.normalizeReleaseVersion", () => {
|
describe("AppController.normalizeReleaseVersion", () => {
|
||||||
it("normalizes AtomGit release tag names", () => {
|
it("normalizes AtomGit release tag names", () => {
|
||||||
@@ -15,29 +15,3 @@ describe("AppController.normalizeReleaseVersion", () => {
|
|||||||
assert.equal(normalizeReleaseVersion({ name: "v1.40.0" }), "1.40.0");
|
assert.equal(normalizeReleaseVersion({ name: "v1.40.0" }), "1.40.0");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("AppController.getReleaseMode", () => {
|
|
||||||
it("returns 'latest' when env var is not set", () => {
|
|
||||||
delete process.env.certd_release_mode;
|
|
||||||
assert.equal(getReleaseMode(), "latest");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 'latest' when env var is empty", () => {
|
|
||||||
process.env.certd_release_mode = "";
|
|
||||||
assert.equal(getReleaseMode(), "latest");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 'stable' when env var is 'stable'", () => {
|
|
||||||
process.env.certd_release_mode = "stable";
|
|
||||||
assert.equal(getReleaseMode(), "stable");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 'latest' when env var is an unknown value", () => {
|
|
||||||
process.env.certd_release_mode = "unknown";
|
|
||||||
assert.equal(getReleaseMode(), "latest");
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
delete process.env.certd_release_mode;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user