diff --git a/.vscode/launch.json b/.vscode/launch.json index 188c03cd7..d3d7ccf3f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -87,6 +87,20 @@ "plus_use_prod": "false", "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": [ diff --git a/AGENTS.md b/AGENTS.md index 081b24094..38cb0b50d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,6 +139,9 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web - 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。 - 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。 - 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。 +- 前端组件样式统一写在 ` diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-author-field.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-author-field.vue new file mode 100644 index 000000000..d17843d5e --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-author-field.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue new file mode 100644 index 000000000..87de39aef --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-input.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-input.vue index 0dda49acd..afc1c8396 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-input.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-input.vue @@ -41,8 +41,6 @@ @@ -304,7 +574,7 @@ function handleVersionClick() { .plugin-card__head { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 12px; } @@ -313,17 +583,17 @@ function handleVersionClick() { display: flex; min-width: 0; flex: 1; - align-items: flex-start; + align-items: center; } .plugin-card__icon { flex: none; - margin-top: 1px; + margin-top: 0; } .plugin-card__main .plugin-icon { display: flex; - align-items: flex-end; + align-items: center; justify-content: center; width: 22px; height: 22px; @@ -334,14 +604,19 @@ function handleVersionClick() { } .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; @@ -378,6 +653,14 @@ function handleVersionClick() { text-overflow: ellipsis; white-space: nowrap; } + + &.is-self-authored { + color: #1677ff; + + .fs-icon { + color: #1677ff; + } + } } .plugin-card__actions { @@ -392,14 +675,9 @@ function handleVersionClick() { .plugin-card__tools { display: flex; - min-width: max-content; flex: none; align-items: center; gap: 1px; - opacity: 0.76; - transition-duration: 160ms; - transition-property: opacity; - transition-timing-function: ease-out; } .plugin-card__tool { @@ -417,7 +695,14 @@ function handleVersionClick() { 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 { @@ -482,7 +767,14 @@ function handleVersionClick() { } .plugin-card__local-action-zone { - width: 76px; + width: 28px; + + .plugin-card__action-button { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); + filter: blur(0); + } } .plugin-card__install-zone { @@ -496,12 +788,6 @@ function handleVersionClick() { } } - &:hover { - .plugin-card__tools { - opacity: 1; - } - } - .plugin-card__desc { display: -webkit-box; min-height: 40px; @@ -570,6 +856,13 @@ function handleVersionClick() { } } + .plugin-card__ai-check-icon { + flex: none; + color: #52c41a; + font-size: 16px; + line-height: 1; + } + &.is-simple { .plugin-card__desc { min-height: 40px; diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-market-panel.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-market-panel.vue deleted file mode 100644 index 221c7c48e..000000000 --- a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-market-panel.vue +++ /dev/null @@ -1,334 +0,0 @@ - - - - - diff --git a/packages/ui/certd-client/src/views/sys/plugin/config-common.vue b/packages/ui/certd-client/src/views/sys/plugin/config-common.vue index 0d3879b96..cdea0e28c 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/config-common.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/config-common.vue @@ -114,7 +114,12 @@ const { openConfigDialog } = usePluginConfig(); async function doPluginConfig() { const certApplyInfo = await GetPluginByName("CertApply"); - await openConfigDialog({ row: certApplyInfo, crudExpose: null }); + await openConfigDialog({ + row: certApplyInfo, + onSuccess: async () => { + await loadForm(); + }, + }); } diff --git a/packages/ui/certd-client/src/views/sys/plugin/config-editor.vue b/packages/ui/certd-client/src/views/sys/plugin/config-editor.vue index 570e89893..5a577e0d1 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/config-editor.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/config-editor.vue @@ -49,6 +49,9 @@ import { computed, onMounted, reactive, ref } from "vue"; import { useRoute, useRouter } from "vue-router"; import Rollbackable from "./rollbackable.vue"; import { usePluginStore } from "/@/store/plugin"; +import * as api from "./api"; +// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。 +import yaml from "js-yaml"; const route = useRoute(); const router = useRouter(); const pluginStore = usePluginStore(); @@ -264,12 +267,35 @@ function clearFormValue(key: string) { 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() { - currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name); - for (const key in currentPlugin.value.input) { + const originName = getPluginOriginName(); + currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName); + 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] = {}; } - const setting = props.plugin.sysSetting; + const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting; if (setting) { const settingJson = JSON.parse(setting); merge(configForm, settingJson.metadata?.input || {}); diff --git a/packages/ui/certd-client/src/views/sys/plugin/crud.tsx b/packages/ui/certd-client/src/views/sys/plugin/crud.tsx index 7285057db..a8ef643f5 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/crud.tsx +++ b/packages/ui/certd-client/src/views/sys/plugin/crud.tsx @@ -1,7 +1,6 @@ import * as api from "./api"; import { useI18n } from "/src/locales"; 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 { Modal, message } from "ant-design-vue"; //@ts-ignore @@ -11,9 +10,9 @@ import KvInput from "/@/components/plugins/common/kv-input.vue"; import { usePluginConfig } from "./use-config"; import { useSettingStore } from "/src/store/settings/index"; import { usePluginStore } from "/@/store/plugin"; +import PluginAuthorField from "./components/plugin-author-field.vue"; export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { - const router = useRouter(); const { t } = useI18n(); let lastType = ""; @@ -50,6 +49,70 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat const settingStore = useSettingStore(); 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 { crudOptions: { settings: { @@ -89,6 +152,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat 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: { show: true, icon: "ion:trash-outline", @@ -126,13 +200,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat buttons: { edit: { show: compute(({ row }) => { - return row.type === "custom"; + return canEditStorePlugin(row); }), }, copy: { show: compute(({ row }) => { - return row.type === "custom"; + return canEditStorePlugin(row); }), + async click({ row }) { + const copyRow = { ...row }; + delete copyRow.fullName; + delete copyRow.id; + crudExpose.openCopy({ + row: copyRow, + }); + }, }, remove: { order: 999, @@ -146,7 +228,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat title: t("certd.export"), type: "link", show: compute(({ row }) => { - return row.type === "custom"; + return canEditStorePlugin(row); }), async click({ row }) { const content = await api.ExportPlugin(row.id); @@ -172,7 +254,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat async click({ row }) { await openConfigDialog({ row, - crudExpose, + onSuccess: async () => { + crudExpose.doRefresh(); + }, }); }, }, @@ -187,16 +271,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }, form: { onSuccess(opts: any) { - if (opts.res?.id) { - router.push({ - name: "SysPluginEdit", - query: { - id: opts.res.id, - }, - }); - } else { - crudExpose.doRefresh(); - } + crudExpose.doRefresh(); }, }, columns: { @@ -287,19 +362,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat title: t("certd.author"), type: "text", search: { + component: { + name: "a-input", + vModel: "value", + }, show: true, }, form: { show: true, - helper: t("certd.authorHelper"), - rules: [ - { required: true }, - { - type: "pattern", - pattern: /^[a-zA-Z][a-zA-Z0-9]+$/, - message: t("certd.authorRuleMsg"), - }, - ], + component: { + name: PluginAuthorField, + vModel: "modelValue", + }, }, column: { width: 200, @@ -316,9 +390,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat column: { width: 300, cellRender({ row }) { - if (row.type === "custom") { - return {row.title}; - } return
{row.title}
; }, }, @@ -339,7 +410,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat show: true, }, form: { - value: "custom", + value: "store", component: { disabled: true, }, @@ -347,8 +418,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat dict: dict({ data: [ { label: t("certd.builtIn"), value: "builtIn" }, - { label: t("certd.custom"), value: "custom" }, - { label: t("certd.installedStorePlugin"), value: "store" }, + { label: t("certd.store"), value: "store" }, ], }), column: { @@ -473,6 +543,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat Modal.confirm({ title: t("certd.confirm"), content: `${t("certd.confirmToggle")} ${!value ? t("certd.disable") : t("certd.enable")}?`, + maskClosable: true, onOk: async () => { await api.SetDisabled({ id: row.id, @@ -480,7 +551,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat type: row.type, disabled: !value, }); - await crudExpose.doRefresh(); + crudExpose.doRefresh(); }, }); }, @@ -491,6 +562,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat group: { title: t("certd.pluginGroup"), type: "dict-select", + search: { + show: true, + }, dict: dict({ url: "/pi/plugin/groupsList", label: "title", diff --git a/packages/ui/certd-client/src/views/sys/plugin/edit.vue b/packages/ui/certd-client/src/views/sys/plugin/edit.vue deleted file mode 100644 index 958447aab..000000000 --- a/packages/ui/certd-client/src/views/sys/plugin/edit.vue +++ /dev/null @@ -1,230 +0,0 @@ - - - - diff --git a/packages/ui/certd-client/src/views/sys/plugin/index.vue b/packages/ui/certd-client/src/views/sys/plugin/index.vue index 8ece10ef8..bd16cbdc2 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/index.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/index.vue @@ -6,134 +6,108 @@ {{ t("certd.pluginBetaWarning") }} - - - - - -
- -
-
-
- - - - -
+ + +
+ +
+
+