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 @@
+
+
+
{{ authorName }}
+
注册作者
+
+
+
+
+
+
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 @@
-
-
-
-
- {{ t("certd.deployPlugin") }}
- {{ t("certd.auth") }}
- {{ t("certd.dns") }}
-
-
-
-
-
-
-
- {{ t("certd.onlinePluginSync") }}
-
-
-
-
-
正在查询插件市场...
-
-
-
{{ t("certd.onlinePluginSyncFirst") }}
-
-
- {{ t("certd.onlinePluginSync") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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") }}
-
-
-
-
-
- {{ t("certd.localPlugin") }}
-
-
-
-
-
-
-
-
-
-
-
- {{ t("certd.pluginMarket") }}
-
-
-
-
-
+
+
+
+
+