perf(domain): 添加域名过期时间同步进度显示功能

添加同步进度对话框,展示同步任务的实时进度和状态
更新中英文翻译文件,添加相关文案
修改AGENTS.md文档中的格式化命令说明
This commit is contained in:
xiaojunnuo
2026-05-07 00:22:41 +08:00
parent a7e281e278
commit 9d2937dd4b
5 changed files with 112 additions and 15 deletions
+1 -1
View File
@@ -208,5 +208,5 @@ Get-ChildItem packages\ui\certd-client\src\views\certd
- 后补单元测试时,应先基于对正确行为的实际预期编写测试,而不是为了迎合现有实现改写预期;如果运行后出现红灯,且通过测试需要修改已有实现,应先向用户确认这是确实的 bug,还是原本需求/既有行为就是如此;确认后再修改原始实现,避免把测试补充变成未经确认的行为改动。
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
- 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit``corepack pnpm --dir packages\core\basic test:unit``corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复,例如 `corepack pnpm --dir packages\ui\certd-client exec prettier --write <files>``corepack pnpm --dir packages\ui\certd-client exec eslint --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建。
@@ -20,6 +20,7 @@ export default {
importFromProvider: "Import from Domain Provider",
syncExpirationDate: "Sync Domain Expiration Time",
syncTaskSubmitted: "Sync task submitted",
syncExpirationProgress: "Sync Domain Expiration Progress",
expirationMonitorSetting: "Domain Expiration Monitor Settings",
subdomainDnsHelper: "Note: In DNS validation mode, subdomains do not need to be maintained here, otherwise certificate application may be affected (except delegated subdomains or free second-level domains).",
path: "Path",
@@ -28,6 +29,9 @@ export default {
progress: "Progress",
operation: "Operation",
total: "Total",
current: "Current",
running: "Running",
done: "Done",
skipped: "Skipped",
failed: "Failed",
notExecuted: "Not executed",
@@ -20,6 +20,7 @@ export default {
importFromProvider: "从域名提供商导入",
syncExpirationDate: "同步域名过期时间",
syncTaskSubmitted: "同步任务已提交",
syncExpirationProgress: "同步域名过期时间进度",
expirationMonitorSetting: "域名过期监控设置",
subdomainDnsHelper: "注意:DNS校验方式下,子域名不需要在此处维护,否则会影响证书申请(子域名托管或免费二级域名除外)",
path: "路径",
@@ -28,6 +29,9 @@ export default {
progress: "进度",
operation: "操作",
total: "总数",
current: "当前",
running: "运行中",
done: "已完成",
skipped: "跳过",
failed: "失败",
notExecuted: "未执行",
@@ -3,7 +3,7 @@ import { Modal, notification } from "ant-design-vue";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useDomainImportManage } from "./use";
import { useDomainImportManage, useSyncExpirationProcess } from "./use";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
@@ -52,6 +52,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
const openDomainImportManageDialog = useDomainImportManage();
const openSyncExpirationProcessDialog = useSyncExpirationProcess({ crudExpose });
const subdomainConfirmed = ref(false);
return {
@@ -140,13 +141,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
icon: "ion:refresh-outline",
text: t("certd.domain.syncExpirationDate"),
click: async () => {
await api.SyncExpirationStart();
notification.success({
message: t("certd.domain.syncTaskSubmitted"),
});
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
try {
await api.SyncExpirationStart();
} finally {
await openSyncExpirationProcessDialog();
}
},
},
monitorSettingSave: {
@@ -361,7 +360,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.httpUploaderType} dict={httpUploaderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.httpUploaderAccess} dict={accessDict} color={"auto"}></fs-values-format>
<a-tag class={"ml-5 flex items-center"}>{t("certd.domain.path")}: {row.httpUploadRootDir}</a-tag>
<a-tag class={"ml-5 flex items-center"}>
{t("certd.domain.path")}: {row.httpUploadRootDir}
</a-tag>
</div>
);
}
@@ -1,11 +1,10 @@
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
import { compute } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { useSettingStore } from "/@/store/settings";
import { Ref, ref } from "vue";
import * as api from "./api";
import DomainImportTaskStatus from "./import.vue";
import { useI18n } from "/@/locales";
import { useSettingStore } from "/@/store/settings";
import { useFormDialog } from "/@/use/use-dialog";
export function useDomainImport() {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
@@ -92,3 +91,92 @@ export function useDomainImportManage() {
});
};
}
export function useSyncExpirationProcess(opts: { crudExpose: any }) {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
return async function openSyncExpirationProcessDialog() {
const taskStatus: Ref<any> = ref({});
const errors: Ref<string[]> = ref([]);
const timerRef: Ref<any> = ref(null);
function stop() {
if (timerRef.value) {
clearTimeout(timerRef.value);
timerRef.value = null;
}
}
async function loadStatus() {
const status = await api.SyncExpirationStatus();
taskStatus.value = status || {};
errors.value = taskStatus.value.errors || [];
if (taskStatus.value.status === "running") {
stop();
timerRef.value = setTimeout(async () => {
await loadStatus();
}, 3000);
} else {
stop();
await opts.crudExpose.doRefresh();
}
}
await loadStatus();
await openFormDialog({
title: t("certd.domain.syncExpirationProgress"),
body: () => {
const progress = Math.min(Math.round(taskStatus.value.progress || 0), 100);
const isRunning = taskStatus.value.status === "running";
const errorList = errors.value.map(item => {
return <div>{item}</div>;
});
return (
<div class={"w-full"}>
<div class={"mt-4 flex flex-wrap gap-2"}>
<a-tag color={isRunning ? "processing" : "success"}>{isRunning ? t("certd.domain.running") : t("certd.domain.done")}</a-tag>
<a-tag class={"m-0"} color={"blue"}>
{t("certd.domain.total")}{taskStatus.value.total || 0}
</a-tag>
<a-tag class={"m-0"} color={"green"}>
{t("certd.success")}{taskStatus.value.successCount || 0}
</a-tag>
<a-tag class={"m-0"} color={"red"}>
{t("certd.domain.failed")}{taskStatus.value.errorCount || 0}
</a-tag>
<a-tag class={"m-0"} color={"cyan"}>
{t("certd.domain.current")}{taskStatus.value.current || 0}
</a-tag>
</div>
<div class={"mt-4 pr-4"}>
<a-progress percent={progress} status={errors.value.length > 0 ? "exception" : isRunning ? "active" : "success"} />
</div>
{errors.value.length > 0 && <div class={"mt-2 break-words text-red-500 mb-4"}>{errorList}</div>}
</div>
);
},
wrapper: {
width: 600,
footer: false,
buttons: {
cancel: {
show: false,
},
reset: {
show: false,
},
ok: {
show: true,
},
},
onClosed() {
stop();
},
},
});
};
}