refactor: organize certd client i18n translations

This commit is contained in:
xiaojunnuo
2026-04-30 23:48:48 +08:00
parent 267243e71b
commit 028932c04a
35 changed files with 503 additions and 150 deletions
@@ -1,17 +1,18 @@
<template>
<div class="flex">
<a-input :value="valueRef" placeholder="请输入图片验证码" autocomplete="off" @update:value="onChange">
<a-input :value="valueRef" :placeholder="t('certd.captcha.inputImageCode')" autocomplete="off" @update:value="onChange">
<template #prefix>
<fs-icon icon="ion:image-outline"></fs-icon>
</template>
</a-input>
<div class="input-right pointer" title="点击刷新">
<div class="input-right pointer" :title="t('certd.captcha.refresh')">
<img class="image-code" :src="imageCodeSrc" @click="resetImageCode" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
modelValue: any;
@@ -20,6 +21,7 @@ const props = defineProps<{
defineOptions({
name: "ImageCaptcha",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const valueRef = ref("");
@@ -4,8 +4,8 @@
<div class="sweep-animation"></div>
<div class="box-content">
<div class="box-icon"></div>
<span v-if="modelValue == null" class="status-text">点击进行验证</span>
<span v-else class="status-text">验证成功</span>
<span v-if="modelValue == null" class="status-text">{{ t("certd.captcha.clickToVerify") }}</span>
<span v-else class="status-text">{{ t("certd.captcha.verifySuccess") }}</span>
</div>
</div>
</div>
@@ -13,8 +13,10 @@
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { ref, Ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { loadScript } from "vue-plugin-load-script";
const { t } = useI18n();
const loaded = ref(false);
async function loadCaptchaScript() {
// 加载验证码js
@@ -56,7 +58,7 @@ function callback(res: { ret: number; ticket: string; randstr: string; errorCode
if (res.errorCode && res.errorCode > 0) {
notification.error({
message: `验证码验证失败:${res.errorMessage || res.errorCode}`,
message: t("certd.captcha.verifyFailed", { message: res.errorMessage || res.errorCode }),
});
}
@@ -83,13 +85,13 @@ function loadErrorCallback(error: any) {
// errorMessage: "jsload_error",
// });
notification.error({
message: `验证码加载失败:${error?.message || error}`,
message: t("certd.captcha.loadFailed", { message: error?.message || error }),
});
}
async function triggerCaptcha() {
if (!loaded.value) {
notification.error({
message: "验证码还未加载完成,请稍后再试",
message: t("certd.captcha.notLoaded"),
});
return;
}
@@ -4,7 +4,7 @@
<!-- <td class="domain">-->
<!-- {{ props.domain }}-->
<!-- </td>-->
<td class="host-record" :title="'域名:' + props.domain">
<td class="host-record" :title="t('certd.verifyPlan.domainTitle', { domain: props.domain })">
<fs-copyable v-model="cnameRecord.hostRecord"></fs-copyable>
</td>
<td style="text-align: center">CNAME</td>
@@ -16,17 +16,17 @@
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" title="重置校验状态,重新校验">
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</td>
<td class="center">
<template v-if="cnameRecord.status !== 'valid'">
<a-button type="primary" size="small" :loading="loading" @click="doVerify">点击验证</a-button>
<a-button type="primary" size="small" :loading="loading" @click="doVerify">{{ t("certd.verifyPlan.clickToValidate") }}</a-button>
<cname-tip :record="cnameRecord"></cname-tip>
</template>
<div v-else class="helper" title="后续自动申请证书需要">不要删除CNAME</div>
<div v-else class="helper" :title="t('certd.verifyPlan.keepCnameTitle')">{{ t("certd.verifyPlan.keepCname") }}</div>
</td>
</tr>
</tbody>
@@ -35,18 +35,20 @@
<script lang="ts" setup>
import { CnameRecord, GetByDomain } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { dict } from "@fast-crud/fast-crud";
import * as api from "./api.js";
import CnameTip from "./cname-tip.vue";
import { Modal } from "ant-design-vue";
import { utils } from "/@/utils/index.js";
const { t } = useI18n();
const statusDict = dict({
data: [
{ label: "待设置CNAME", value: "cname", color: "warning" },
{ label: "验证中", value: "validating", color: "blue" },
{ label: "验证成功", value: "valid", color: "green" },
{ label: "验证失败", value: "failed", color: "red" },
{ label: "验证超时", value: "timeout", color: "red" },
{ label: t("certd.verifyPlan.status.pendingCname"), value: "cname", color: "warning" },
{ label: t("certd.verifyPlan.status.validating"), value: "validating", color: "blue" },
{ label: t("certd.verifyPlan.status.valid"), value: "valid", color: "green" },
{ label: t("certd.verifyPlan.status.failed"), value: "failed", color: "red" },
{ label: t("certd.verifyPlan.status.timeout"), value: "timeout", color: "red" },
],
});
@@ -125,8 +127,8 @@ async function doVerify() {
async function resetStatus() {
Modal.confirm({
title: "重置状态",
content: "确定要重置校验状态吗?",
title: t("certd.verifyPlan.resetStatus"),
content: t("certd.verifyPlan.confirmResetStatus"),
onOk: async () => {
await api.ResetStatus(cnameRecord.value.id);
await loadRecord();
@@ -2,17 +2,17 @@
<a-tooltip :overlay-style="{ maxWidth: '400px' }">
<template #title>
<div>
<div>多试几次如果仍然无法验证通过请按如下步骤排查问题</div>
<div>1. 解析记录应该添加在{{ record.domain }}域名下</div>
<div>2. 要添加的是CNAME类型的记录不是TXT</div>
<div>3. 核对记录值是否是:{{ record.recordValue }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.intro") }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step1", { domain: record.domain }) }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step2") }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step3", { value: record.recordValue }) }}</div>
<div>
4. 在验证中状态下运行下面的命令,查看cname和txt解析是否正确
{{ t("certd.verifyPlan.cnameTip.step4") }}
<fs-copyable :style="{ color: '#52c41a' }" :model-value="nslookupCmd"></fs-copyable>
或者
{{ t("certd.verifyPlan.cnameTip.or") }}
<fs-copyable :style="{ color: '#52c41a' }" :model-value="digCmd"></fs-copyable>
</div>
<div>5. 如果以上检查都没有问题则可能是DNS解析生效时间比较慢某些提供商延迟可能高达几个小时</div>
<div>{{ t("certd.verifyPlan.cnameTip.step5") }}</div>
</div>
</template>
<fs-icon class="ml-5 pointer" icon="mingcute:question-line"></fs-icon>
@@ -21,9 +21,11 @@
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
record: any;
}>();
const { t } = useI18n();
const nslookupCmd = computed(() => {
return `nslookup -q=txt _acme-challenge.${props.record.domain}`;
@@ -2,11 +2,11 @@
<table class="cname-verify-plan">
<thead>
<tr>
<td style="width: 160px">主机记录</td>
<td style="width: 100px; text-align: center">记录类型</td>
<td style="width: 250px">请设置CNAME记录验证成功以后不要删除</td>
<td style="width: 120px" class="center">状态</td>
<td style="width: 90px" class="center">操作</td>
<td style="width: 160px">{{ t("certd.verifyPlan.hostRecord") }}</td>
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.recordType") }}</td>
<td style="width: 250px">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
<td style="width: 120px" class="center">{{ t("certd.status") }}</td>
<td style="width: 90px" class="center">{{ t("certd.verifyPlan.operation") }}</td>
</tr>
</thead>
<template v-for="key in domains" :key="key">
@@ -19,11 +19,14 @@
import { CnameRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import CnameRecordInfo from "/@/components/plugins/cert/domains-verify-plan-editor/cname-record-info.vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
defineOptions({
name: "CnameVerifyPlan",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
@@ -2,10 +2,10 @@
<table class="http-verify-plan">
<thead>
<tr>
<td style="width: 160px">网站域名</td>
<td style="width: 100px; text-align: center">上传方式</td>
<td style="width: 150px">上传授权</td>
<td style="width: 200px">网站根目录路径</td>
<td style="width: 160px">{{ t("certd.verifyPlan.websiteDomain") }}</td>
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.uploadMethod") }}</td>
<td style="width: 150px">{{ t("certd.verifyPlan.uploadAccess") }}</td>
<td style="width: 200px">{{ t("certd.verifyPlan.websiteRootPath") }}</td>
</tr>
</thead>
<tbody v-if="records" class="http-record-body">
@@ -21,7 +21,7 @@
<access-selector v-model="item.httpUploaderAccess" :type="item.httpUploaderType" @change="onRecordChange"></access-selector>
</td>
<td>
<a-input v-model:value="item.httpUploadRootDir" placeholder="网站根目录,如:/www/wwwroot" @change="onRecordChange"></a-input>
<a-input v-model:value="item.httpUploadRootDir" :placeholder="t('certd.verifyPlan.websiteRootPlaceholder')" @change="onRecordChange"></a-input>
</td>
</tr>
</template>
@@ -31,6 +31,7 @@
<script lang="ts" setup>
import { Ref, ref, watch, nextTick } from "vue";
import { useI18n } from "vue-i18n";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
@@ -39,6 +40,8 @@ defineOptions({
name: "HttpVerifyPlan",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
@@ -5,7 +5,7 @@
<div class="plan-box bg-white dark:bg-neutral-700">
<div class="fullscreen-button pointer flex-center" @click="fullscreen = !fullscreen">
<span v-if="!fullscreen" style="font-size: 10px" class="flex-center">
这里可以放大
{{ t("certd.verifyPlan.expandTip") }}
<fs-icon icon="ion:arrow-forward-outline"></fs-icon>
</span>
<fs-icon :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'"></fs-icon>
@@ -13,9 +13,9 @@
<table class="plan-table">
<thead>
<tr>
<th style="min-width: 100px">主域名</th>
<th>验证方式</th>
<th>验证计划</th>
<th style="min-width: 100px">{{ t("certd.verifyPlan.mainDomain") }}</th>
<th>{{ t("certd.verifyPlan.challengeType") }}</th>
<th>{{ t("certd.verifyPlan.challengePlan") }}</th>
</tr>
</thead>
<tbody>
@@ -30,13 +30,13 @@
<div class="plan">
<div v-if="item.type === 'dns'" class="plan-dns">
<div class="form-item">
<span class="label">DNS类型</span>
<span class="label">{{ t("certd.verifyPlan.dnsType") }}:</span>
<span class="input">
<fs-dict-select
v-model:value="item.dnsProviderType"
size="small"
:dict="dnsProviderTypeDict"
placeholder="DNS提供商"
:placeholder="t('certd.verifyPlan.dnsProvider')"
@change="onPlanChanged"
@selected-change="onDnsProviderChange(item, $event)"
></fs-dict-select>
@@ -44,9 +44,9 @@
</div>
<a-divider type="vertical" />
<div class="form-item">
<span class="label">DNS授权</span>
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
<span class="input">
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" placeholder="请选择" @change="onPlanChanged"></access-selector>
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
</span>
</div>
</div>
@@ -55,7 +55,7 @@
</div>
<div v-if="item.type === 'http'" class="plan-http">
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
<div class="helper">证书颁发机构将请求 https://yourdomain/.well-known/acme-challenge/xxxxxx 来验证域名所有权。</div>
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
</div>
</div>
</td>
@@ -72,6 +72,7 @@
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
@@ -84,17 +85,19 @@ defineOptions({
name: "DomainsVerifyPlanEditor",
});
const { t } = useI18n();
const challengeTypeOptions = ref<any[]>([
{
label: "DNS验证",
label: t("certd.verifyPlan.dnsChallenge"),
value: "dns",
},
{
label: "CNAME验证",
label: t("certd.verifyPlan.cnameChallenge"),
value: "cname",
},
{
label: "HTTP验证",
label: t("certd.verifyPlan.httpChallenge"),
value: "http",
},
]);
@@ -1,5 +1,6 @@
import Validator from "async-validator";
import { DomainsVerifyPlanInput } from "./type";
import { $t } from "/@/locales";
function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
if (value == null) {
@@ -13,7 +14,7 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const subDomain of subDomains) {
const plan = value[domain].cnameVerifyPlan[subDomain];
if (plan.status !== "valid") {
throw new Error(`域名${subDomain}的CNAME未验证通过,请先设置CNAME记录,点击验证按钮`);
throw new Error($t("certd.verifyPlan.errors.cnameNotValid", { domain: subDomain }));
}
}
}
@@ -22,7 +23,7 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const item of domains) {
//如果有通配符域名则不允许使用http校验
if (item.startsWith("*.")) {
throw new Error(`域名${item}为通配符域名,不支持HTTP校验`);
throw new Error($t("certd.verifyPlan.errors.wildcardNotSupportHttp", { domain: item }));
}
}
@@ -31,19 +32,19 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const subDomain of subDomains) {
const plan = value[domain].httpVerifyPlan[subDomain];
if (!plan.httpUploaderType) {
throw new Error(`域名${subDomain}的上传方式必须填写`);
throw new Error($t("certd.verifyPlan.errors.uploadMethodRequired", { domain: subDomain }));
}
if (!plan.httpUploaderAccess) {
throw new Error(`域名${subDomain}的上传授权信息必须填写`);
throw new Error($t("certd.verifyPlan.errors.uploadAccessRequired", { domain: subDomain }));
}
if (!plan.httpUploadRootDir) {
throw new Error(`域名${subDomain}的网站根路径必须填写`);
throw new Error($t("certd.verifyPlan.errors.websiteRootRequired", { domain: subDomain }));
}
}
}
} else if (type === "dns") {
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
throw new Error(`DNS模式下,域名${domain}的DNS类型和授权信息必须填写`);
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
}
}
}
@@ -1,7 +1,7 @@
<template>
<div class="api-test">
<div>
<fs-button :loading="loading" type="primary" text="测试" icon="ion:refresh-outline" @click="doTest"></fs-button>
<fs-button :loading="loading" type="primary" :text="t('certd.pluginCommon.test')" icon="ion:refresh-outline" @click="doTest"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -12,6 +12,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { ref, inject } from "vue";
import { useI18n } from "vue-i18n";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
@@ -19,6 +20,8 @@ defineOptions({
name: "ApiTest",
});
const { t } = useI18n();
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
@@ -61,14 +64,14 @@ const doTest = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `错误:${err.message}`;
message.value = t("certd.pluginCommon.errorWithMessage", { message: err.message });
},
showErrorNotify: false,
}
);
message.value = "测试请求成功";
message.value = t("certd.pluginCommon.testRequestSuccess");
if (res) {
message.value += `,返回:${JSON.stringify(res)}`;
message.value += t("certd.pluginCommon.responseSuffix", { response: JSON.stringify(res) });
}
} finally {
loading.value = false;
@@ -9,6 +9,7 @@
<script setup lang="ts">
import { inject, ref, watch } from "vue";
import { useI18n } from "/@/locales";
defineOptions({
name: "CertDomainsGetter",
@@ -24,6 +25,7 @@ const emit = defineEmits<{
}>();
const pipeline: any = inject("pipeline");
const { t } = useI18n();
function findStepFromPipeline(targetStepId: string) {
for (const stage of pipeline.value.stages) {
@@ -40,7 +42,7 @@ function findStepFromPipeline(targetStepId: string) {
const errorRef = ref("");
function getStepIdFromInputKey(inputKey: string) {
if (!inputKey) {
errorRef.value = "请先选择域名证书";
errorRef.value = t("certd.pluginCommon.selectCertFirst");
return;
}
return inputKey.split(".")[1];
@@ -49,7 +51,7 @@ function getDomainFromPipeline(inputKey: string) {
let targetStepId = getStepIdFromInputKey(inputKey);
let certStep = findStepFromPipeline(targetStepId);
if (!certStep) {
errorRef.value = "找不到目标步骤,请先选择域名证书";
errorRef.value = t("certd.pluginCommon.targetStepNotFound");
return;
}
@@ -58,7 +60,7 @@ function getDomainFromPipeline(inputKey: string) {
targetStepId = getStepIdFromInputKey(firstLevelValue);
certStep = findStepFromPipeline(targetStepId);
if (!certStep) {
errorRef.value = "找不到目标步骤,请先选择域名证书";
errorRef.value = t("certd.pluginCommon.targetStepNotFound");
return;
}
}
@@ -17,12 +17,12 @@
<template v-if="search">
<div class="flex w-full items-center justify-between flex-wrap" style="padding: 4px 8px">
<div class="flex-1 flex flex-row items-center">
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear placeholder="这里可以搜索域名(数据来自“设置->域名管理”),您也可以直接在上面输入框输入" @keydown.enter="doSearch" />
<fs-button type="primary" class="m-1" :loading="loading" icon="mingcute:search-2-line" @click="doSearch"> 查询 </fs-button>
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear :placeholder="t('certd.pluginCommon.domainSearchPlaceholder')" @keydown.enter="doSearch" />
<fs-button type="primary" class="m-1" :loading="loading" icon="mingcute:search-2-line" @click="doSearch">{{ t("certd.pluginCommon.search") }}</fs-button>
</div>
<div class="manager flex flex-row items-center">
<fs-button type="primary" class="m-1" icon="mingcute:vip-1-line" @click="openDomainImportDialog">导入域名</fs-button>
<fs-button class="m-1" type="primary" icon="carbon:gui-management" @click="openDomainManager">管理域名</fs-button>
<fs-button type="primary" class="m-1" icon="mingcute:vip-1-line" @click="openDomainImportDialog">{{ t("certd.pluginCommon.importDomain") }}</fs-button>
<fs-button class="m-1" type="primary" icon="carbon:gui-management" @click="openDomainManager">{{ t("certd.pluginCommon.manageDomain") }}</fs-button>
</div>
</div>
<div v-if="hasError" class="helper p-2" :class="{ error: hasError }">
@@ -47,7 +47,7 @@
</template>
</a-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新我的域名列表" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshMyDomains')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -57,6 +57,7 @@
</template>
<script setup lang="ts">
import { computed, defineComponent, onMounted, ref, Ref, useAttrs } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { Dicts } from "../lib/dicts";
import { request } from "/@/api/service";
@@ -67,6 +68,8 @@ defineOptions({
name: "DomainSelector",
});
const { t } = useI18n();
const VNodes = defineComponent({
props: {
vnodes: {
@@ -4,7 +4,7 @@
<a-auto-complete class="remote-auto-complete-input" :filter-option="filterOption" :options="optionsRef" :value="value" v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)">
</a-auto-complete>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -15,6 +15,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
@@ -22,6 +23,8 @@ defineOptions({
name: "RemoteAutoComplete",
});
const { t } = useI18n();
const props = defineProps<
{
watches?: string[];
@@ -93,16 +96,16 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
optionsRef.value = list;
@@ -7,6 +7,7 @@
<script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib";
import { inject, ref, useAttrs } from "vue";
import { useI18n } from "vue-i18n";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { getInputFromForm } from "./utils";
@@ -15,6 +16,7 @@ defineOptions({
name: "RemoteInput",
});
const { openCrudFormDialog } = useFormWrapper();
const { t } = useI18n();
const props = defineProps<{
modelValue: string;
title: string;
@@ -53,7 +55,7 @@ async function openDialog() {
saveRemind: false,
},
afterSubmit() {
notification.success({ message: "操作成功" });
notification.success({ message: t("certd.operationSuccess") });
},
async doSubmit({ form }: any) {
return await doPluginFormSubmit(form);
@@ -5,12 +5,12 @@
<template #dropdownRender="{ menuNode: menu }">
<template v-if="search">
<div class="flex w-full" style="padding: 4px 8px">
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear placeholder="查询关键字" @keydown.enter="doSearch" />
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear :placeholder="t('certd.pluginCommon.searchKeyword')" @keydown.enter="doSearch" />
<a-button class="ml-2" :loading="loading" type="text" @click="doSearch">
<template #icon>
<search-outlined />
</template>
查询
{{ t("certd.pluginCommon.search") }}
</a-button>
</div>
<div v-if="hasError" class="helper p-2" :class="{ error: hasError }">
@@ -26,7 +26,7 @@
</template>
</a-select>
<div class="ml-5 flex flex-row no-wrap">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<UploadCert v-if="uploadCert" class="ml-5" v-bind="uploadCert" @submit="refreshOptions"></UploadCert>
</div>
</div>
@@ -38,6 +38,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
import UploadCert from "./upload-cert.vue";
@@ -47,6 +48,8 @@ defineOptions({
name: "RemoteSelect",
});
const { t } = useI18n();
const selectRef = ref(null);
const VNodes = defineComponent({
@@ -161,7 +164,7 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
optionsRef.value = [];
},
showErrorNotify: false,
@@ -169,9 +172,9 @@ const getOptions = async () => {
);
let list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
list = list.map((item: any) => {
return {
@@ -3,7 +3,7 @@
<div class="flex flex-row">
<a-tree-select class="remote-tree-select-input" :tree-data="optionsRef" :value="value" tree-checkable allow-clear v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)"> </a-tree-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -14,6 +14,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
@@ -21,6 +22,8 @@ defineOptions({
name: "RemoteTreeSelect",
});
const { t } = useI18n();
const props = defineProps<
{
watches: string[];
@@ -104,16 +107,16 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
optionsRef.value = list;
pagerRef.value.total = list.length;
@@ -1,12 +1,13 @@
<template>
<div class="upload-cert">
<fs-button v-model:loading="loading" type="primary" text="上传" v-bind="props.button" @click="openUploadCertDialog"></fs-button>
<fs-button v-model:loading="loading" type="primary" :text="t('certd.pluginCommon.upload')" v-bind="props.button" @click="openUploadCertDialog"></fs-button>
</div>
</template>
<script lang="ts" setup>
import { message } from "ant-design-vue";
import { useFormDialog } from "../../../use/use-dialog";
import { computed, inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { doRequest } from "../lib";
import { getInputFromForm } from "./utils";
import { UploadCertProps } from "./types";
@@ -14,6 +15,7 @@ import { merge } from "lodash-es";
const props = defineProps<UploadCertProps>();
const loading = ref(false);
const { t } = useI18n();
const emit = defineEmits(["submit"]);
const { openFormDialog } = useFormDialog();
@@ -28,18 +30,18 @@ const getScope: any = inject("get:scope", () => {
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const title = computed(() => props.title || "上传证书");
const title = computed(() => props.title || t("certd.pluginCommon.uploadCert"));
function openUploadCertDialog() {
const columns = merge(
{
certName: {
title: "证书名称",
title: t("certd.pluginCommon.certName"),
form: {
component: {
name: "a-input",
vModel: "value",
},
helper: "上传后证书显示名称",
helper: t("certd.pluginCommon.certNameHelper"),
},
},
},
@@ -49,7 +51,7 @@ function openUploadCertDialog() {
title: title.value,
columns: {
certName: {
title: "证书名称",
title: t("certd.pluginCommon.certName"),
form: {
component: {
name: "a-input",
@@ -84,7 +86,7 @@ function openUploadCertDialog() {
showErrorNotify: true,
}
);
message.success("上传成功");
message.success(t("certd.pluginCommon.uploadSuccess"));
emit("submit");
} finally {
loading.value = false;
@@ -1,17 +1,18 @@
import { dict } from "@fast-crud/fast-crud";
import { $t } from "/@/locales";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({
data: [
{ value: "dns", label: "DNS校验", color: "green" },
{ value: "cname", label: "CNAME代理校验", color: "blue" },
{ value: "http", label: "HTTP校验", color: "yellow" },
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
],
}),
dnsProviderTypeDict: dict({
@@ -22,17 +23,17 @@ export const Dicts = {
{ label: "SFTP", value: "sftp" },
{ label: "SCP", value: "scp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: $t("certd.verifyPlan.uploader.aliyunOss"), value: "alioss" },
{ label: $t("certd.verifyPlan.uploader.tencentCos"), value: "tencentcos" },
{ label: $t("certd.verifyPlan.uploader.qiniuOss"), value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
{ label: $t("certd.verifyPlan.uploader.sshDeprecated"), value: "ssh", disabled: true },
],
}),
domainFromTypeDict: dict({
data: [
{ value: "manual", label: "手动" },
{ value: "auto", label: "自动" },
{ value: "manual", label: $t("certd.verifyPlan.domainFrom.manual") },
{ value: "auto", label: $t("certd.verifyPlan.domainFrom.auto") },
],
}),
};
@@ -14,5 +14,93 @@ export default {
gotoCnameTip: "Please go to CNAME Record Page",
fromType: "From Type",
expirationDate: "Expiration Date",
cnameManagedInCnamePage: "For CNAME mode, please manage records on the CNAME Records page.",
subdomainConfirmTitle: "Subdomain Confirmation",
subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?",
importFromProvider: "Import from Domain Provider",
syncExpirationDate: "Sync Domain Expiration Time",
syncTaskSubmitted: "Sync task submitted",
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",
addImportTask: "Add Import Task",
refresh: "Refresh",
progress: "Progress",
operation: "Operation",
total: "Total",
skipped: "Skipped",
failed: "Failed",
notExecuted: "Not executed",
execute: "Run",
delete: "Delete",
confirmDelete: "Confirm deletion?",
domainProvider: "Domain Provider",
domainProviderAccessType: "Domain Provider Access Type",
domainProviderAccess: "Domain Provider Access",
},
verifyPlan: {
expandTip: "Click to enlarge",
mainDomain: "Main Domain",
challengeType: "Challenge Method",
challengePlan: "Challenge Plan",
dnsType: "DNS Type",
dnsProvider: "DNS Provider",
dnsAccess: "DNS Access",
pleaseSelect: "Please select",
httpHelper: "The CA will request https://yourdomain/.well-known/acme-challenge/xxxxxx to verify domain ownership.",
dnsChallenge: "DNS Challenge",
cnameChallenge: "CNAME Challenge",
cnameProxyChallenge: "CNAME Proxy Challenge",
httpChallenge: "HTTP Challenge",
domainTitle: "Domain: {domain}",
resetStatusTooltip: "Reset validation status and validate again",
clickToValidate: "Validate",
keepCnameTitle: "Required for future automatic certificate applications",
keepCname: "Do not delete CNAME",
resetStatus: "Reset Status",
confirmResetStatus: "Are you sure you want to reset the validation status?",
hostRecord: "Host Record",
recordType: "Record Type",
setCnameRecord: "Set the CNAME record. Do not delete it after validation succeeds.",
operation: "Operation",
websiteDomain: "Website Domain",
uploadMethod: "Upload Method",
uploadAccess: "Upload Access",
websiteRootPath: "Website Root Path",
websiteRootPlaceholder: "Website root path, e.g. /www/wwwroot",
status: {
pendingCname: "Pending CNAME Setup",
validating: "Validating",
valid: "Validation Successful",
failed: "Validation Failed",
timeout: "Validation Timed Out",
},
cnameTip: {
intro: "Try a few more times. If validation still fails, troubleshoot with these steps:",
step1: "1. The DNS record should be added under {domain}.",
step2: "2. Add a CNAME record, not a TXT record.",
step3: "3. Check whether the record value is: {value}",
step4: "4. While validating, run the command below to check whether CNAME and TXT resolution are correct.",
or: "or",
step5: "5. If all checks pass, DNS propagation may be slow. Some providers can take several hours.",
},
errors: {
cnameNotValid: "The CNAME for domain {domain} has not been validated. Set the CNAME record first, then click Validate.",
wildcardNotSupportHttp: "Domain {domain} is a wildcard domain and does not support HTTP validation.",
uploadMethodRequired: "The upload method for domain {domain} is required.",
uploadAccessRequired: "The upload access for domain {domain} is required.",
websiteRootRequired: "The website root path for domain {domain} is required.",
dnsProviderRequired: "In DNS mode, the DNS type and access information for domain {domain} are required.",
},
uploader: {
aliyunOss: "Alibaba Cloud OSS",
tencentCos: "Tencent Cloud COS",
qiniuOss: "Qiniu OSS",
sshDeprecated: "SSH (deprecated, use SFTP instead)",
},
domainFrom: {
manual: "Manual",
auto: "Automatic",
},
},
};
@@ -18,4 +18,24 @@ export default {
cname_feature_guide: "CNAME feature principle and usage guide",
mainDomain: "Main Domain",
cnameRecord: "CNAME Record Management",
importRecords: "Import CNAME Records",
batchImport: "Batch Import",
exportRecordsTip: "After exporting CNAME records, you can batch import CNAME DNS records to the domain registrar.",
batchExport: "Batch Export",
resetStatus: "Reset Status",
confirmResetStatus: "Are you sure you want to reset the validation status?",
resetStatusTooltip: "Reset validation status and validate again",
cname: {
importRecords: "Import CNAME Records",
batchImport: "Batch Import",
exportRecordsTip: "After exporting CNAME records, you can batch import CNAME DNS records to the domain registrar.",
batchExport: "Batch Export",
resetStatus: "Reset Status",
confirmResetStatus: "Are you sure you want to reset the validation status?",
resetStatusTooltip: "Reset validation status and validate again",
domainList: "Domain List",
domainListHelper: "One domain per line for batch import\nRemove *. from wildcard domains\nExisting records will be skipped automatically",
cnameService: "CNAME Service",
importTaskSubmitted: "Import task submitted",
},
};
@@ -113,4 +113,36 @@ export default {
helpDocLink: "Help Docs",
suite: "Suite",
helpDoc: "Help Document",
pluginCommon: {
test: "Test",
errorWithMessage: "Error: {message}",
testRequestSuccess: "Test request succeeded",
responseSuffix: ", response: {response}",
searchKeyword: "Search keyword",
search: "Search",
refreshOptions: "Refresh options",
getOptionsError: "Failed to get options: {message}",
getDataSuccessSelect: "Data loaded. Please select from the dropdown.",
getDataSuccessEmpty: "Data loaded. No data found.",
domainSearchPlaceholder: "Search domains here (from Settings -> Domain Management), or type directly in the input above",
importDomain: "Import Domains",
manageDomain: "Manage Domains",
refreshMyDomains: "Refresh my domain list",
upload: "Upload",
uploadCert: "Upload Certificate",
certName: "Certificate Name",
certNameHelper: "Display name after upload",
uploadSuccess: "Upload successful",
selectCertFirst: "Please select a domain certificate first",
targetStepNotFound: "Target step not found. Please select a domain certificate first.",
},
captcha: {
inputImageCode: "Please enter the image captcha",
refresh: "Refresh captcha",
clickToVerify: "Click to verify",
verifySuccess: "Verification successful",
verifyFailed: "Verification failed. Please try again.",
loadFailed: "Failed to load captcha",
notLoaded: "Captcha is not loaded yet",
},
};
@@ -97,6 +97,7 @@ export default {
pipeline: "Pipeline",
},
editSchedule: "Edit Schedule",
webhook: "Webhook",
timerTrigger: "Timer Trigger",
schedule: "Schedule",
selectCron: "Please select a schedule Cron",
@@ -18,6 +18,7 @@ export default {
project: {
noProjectJoined: "You haven't joined any projects yet",
applyToJoin: "Please apply to join a project to start using",
projectList: "Project List",
systemProjects: "System Project List",
createdAt: "Created At",
applyJoin: "Apply to Join",
@@ -7,8 +7,13 @@ export default {
settingLink: "Site Monitoring Settings",
limitInfo: "Basic edition limited to 1, professional and above unlimited, current",
checkAll: "Check All",
checkNow: "Check Now",
batchDelete: "Batch Delete",
confirmTitle: "Confirm",
confirmContent: "Confirm to trigger check for all site certificates?",
batchDeleteConfirm: "Are you sure you want to batch delete these {count} records?",
deleteSuccess: "Delete successful",
selectRecordsFirst: "Please select records first",
checkSubmitted: "Check task submitted",
pleaseRefresh: "Please refresh the page later to see the results",
siteName: "Site Name",
@@ -39,6 +44,10 @@ export default {
ipCheckHelper: "Enable to check certificate expiration time on each IP (or source site domain) ",
ipSyncAuto: "Enable IP Sync Auto",
ipSyncMode: "IP Sync Mode",
ipSyncModeHelper: "Choose whether to check only IPv4, only IPv6, or all IPs.",
ipSyncModeAll: "Check all IPs",
ipSyncModeIPV4Only: "Check IPv4 only",
ipSyncModeIPV6Only: "Check IPv6 only",
ipIgnoreCoherence: "Ignore Certificate Coherence",
ipIgnoreCoherenceHelper: "Enable to ignore certificate coherence check, only check certificate expiration time",
selectRequired: "Please select",
@@ -49,6 +58,10 @@ export default {
certInfoId: "Certificate ID",
checkSubmittedRefresh: "Check task submitted. Please refresh later to view the result.",
ipManagement: "IP Management",
siteIpMonitor: "Site IP Monitor",
ipList: "IP List",
ipListHelper: "One IP or CNAME domain per line",
enterImportIpOrDomain: "Please enter the IPs or domains to import",
bulkImport: "Bulk Import",
basicLimitError: "Basic version allows only one monitoring site. Please upgrade to the Pro version.",
limitExceeded: "Sorry, you can only create up to {max} monitoring records. Please purchase or upgrade your plan.",
@@ -14,5 +14,93 @@ export default {
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
fromType: "来源类型",
expirationDate: "到期时间",
cnameManagedInCnamePage: "CNAME方式请前往CNAME记录页面进行管理",
subdomainConfirmTitle: "子域名确认",
subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?",
importFromProvider: "从域名提供商导入",
syncExpirationDate: "同步域名过期时间",
syncTaskSubmitted: "同步任务已提交",
expirationMonitorSetting: "域名过期监控设置",
subdomainDnsHelper: "注意:DNS校验方式下,子域名不需要在此处维护,否则会影响证书申请(子域名托管或免费二级域名除外)",
path: "路径",
addImportTask: "添加导入任务",
refresh: "刷新",
progress: "进度",
operation: "操作",
total: "总数",
skipped: "跳过",
failed: "失败",
notExecuted: "未执行",
execute: "执行",
delete: "删除",
confirmDelete: "确认删除吗?",
domainProvider: "域名提供商",
domainProviderAccessType: "域名提供商访问类型",
domainProviderAccess: "域名提供商授权",
},
verifyPlan: {
expandTip: "这里可以放大",
mainDomain: "主域名",
challengeType: "验证方式",
challengePlan: "验证计划",
dnsType: "DNS类型",
dnsProvider: "DNS提供商",
dnsAccess: "DNS授权",
pleaseSelect: "请选择",
httpHelper: "证书颁发机构将请求 https://yourdomain/.well-known/acme-challenge/xxxxxx 来验证域名所有权。",
dnsChallenge: "DNS验证",
cnameChallenge: "CNAME验证",
cnameProxyChallenge: "CNAME代理验证",
httpChallenge: "HTTP验证",
domainTitle: "域名:{domain}",
resetStatusTooltip: "重置校验状态,重新校验",
clickToValidate: "点击验证",
keepCnameTitle: "后续自动申请证书需要",
keepCname: "不要删除CNAME",
resetStatus: "重置状态",
confirmResetStatus: "确定要重置校验状态吗?",
hostRecord: "主机记录",
recordType: "记录类型",
setCnameRecord: "请设置CNAME记录(验证成功以后不要删除)",
operation: "操作",
websiteDomain: "网站域名",
uploadMethod: "上传方式",
uploadAccess: "上传授权",
websiteRootPath: "网站根目录路径",
websiteRootPlaceholder: "网站根目录,如:/www/wwwroot",
status: {
pendingCname: "待设置CNAME",
validating: "验证中",
valid: "验证成功",
failed: "验证失败",
timeout: "验证超时",
},
cnameTip: {
intro: "多试几次,如果仍然无法验证通过,请按如下步骤排查问题:",
step1: "1. 解析记录应该添加在{domain}域名下",
step2: "2. 要添加的是CNAME类型的记录,不是TXT",
step3: "3. 核对记录值是否是:{value}",
step4: "4. 在验证中状态下,运行下面的命令,查看cname和txt解析是否正确",
or: "或者",
step5: "5. 如果以上检查都没有问题,则可能是DNS解析生效时间比较慢,某些提供商延迟可能高达几个小时",
},
errors: {
cnameNotValid: "域名{domain}的CNAME未验证通过,请先设置CNAME记录,点击验证按钮",
wildcardNotSupportHttp: "域名{domain}为通配符域名,不支持HTTP校验",
uploadMethodRequired: "域名{domain}的上传方式必须填写",
uploadAccessRequired: "域名{domain}的上传授权信息必须填写",
websiteRootRequired: "域名{domain}的网站根路径必须填写",
dnsProviderRequired: "DNS模式下,域名{domain}的DNS类型和授权信息必须填写",
},
uploader: {
aliyunOss: "阿里云OSS",
tencentCos: "腾讯云COS",
qiniuOss: "七牛OSS",
sshDeprecated: "SSH(已废弃,请选择SFTP方式)",
},
domainFrom: {
manual: "手动",
auto: "自动",
},
},
};
@@ -18,4 +18,24 @@ export default {
cname_feature_guide: "CNAME功能原理及使用说明",
mainDomain: "主域名",
cnameRecord: "CNAME记录管理",
importRecords: "导入CNAME记录",
batchImport: "批量导入",
exportRecordsTip: "导出CNAME记录之后,可用于批量导入cname解析到域名注册商",
batchExport: "批量导出",
resetStatus: "重置状态",
confirmResetStatus: "确定要重置校验状态吗?",
resetStatusTooltip: "重置校验状态,重新校验",
cname: {
importRecords: "导入CNAME记录",
batchImport: "批量导入",
exportRecordsTip: "导出CNAME记录之后,可用于批量导入cname解析到域名注册商",
batchExport: "批量导出",
resetStatus: "重置状态",
confirmResetStatus: "确定要重置校验状态吗?",
resetStatusTooltip: "重置校验状态,重新校验",
domainList: "域名列表",
domainListHelper: "每个域名一行,批量导入\n泛域名请去掉*.\n已经存在的会自动跳过",
cnameService: "CNAME服务",
importTaskSubmitted: "导入任务已提交",
},
};
@@ -117,4 +117,36 @@ export default {
helpDocLink: "帮助文档",
suite: "套餐",
helpDoc: "帮助说明",
pluginCommon: {
test: "测试",
errorWithMessage: "错误:{message}",
testRequestSuccess: "测试请求成功",
responseSuffix: ",返回:{response}",
searchKeyword: "查询关键字",
search: "查询",
refreshOptions: "刷新选项",
getOptionsError: "获取选项出错:{message}",
getDataSuccessSelect: "获取数据成功,请从下拉框中选择",
getDataSuccessEmpty: "获取数据成功,没有数据",
domainSearchPlaceholder: "这里可以搜索域名(数据来自“设置->域名管理”),您也可以直接在上面输入框输入",
importDomain: "导入域名",
manageDomain: "管理域名",
refreshMyDomains: "刷新我的域名列表",
upload: "上传",
uploadCert: "上传证书",
certName: "证书名称",
certNameHelper: "上传后证书显示名称",
uploadSuccess: "上传成功",
selectCertFirst: "请先选择域名证书",
targetStepNotFound: "找不到目标步骤,请先选择域名证书",
},
captcha: {
inputImageCode: "请输入图形验证码",
refresh: "刷新验证码",
clickToVerify: "点击验证",
verifySuccess: "验证成功",
verifyFailed: "验证失败,请重试",
loadFailed: "加载验证码失败",
notLoaded: "验证码未加载完成",
},
};
@@ -7,8 +7,13 @@ export default {
settingLink: "站点监控设置",
limitInfo: "基础版限制1条,专业版以上无限制,当前",
checkAll: "检查全部",
checkNow: "立即检查",
batchDelete: "批量删除",
confirmTitle: "确认",
confirmContent: "确认触发检查全部站点证书吗?",
batchDeleteConfirm: "确定要批量删除这{count}条记录吗",
deleteSuccess: "删除成功",
selectRecordsFirst: "请先勾选记录",
checkSubmitted: "检查任务已提交",
pleaseRefresh: "请稍后刷新页面查看结果",
siteName: "站点名称",
@@ -53,6 +58,10 @@ export default {
certInfoId: "证书ID",
checkSubmittedRefresh: "检查任务已提交,请稍后刷新查看结果",
ipManagement: "IP管理",
siteIpMonitor: "站点IP监控",
ipList: "IP列表",
ipListHelper: "IP或者CNAME域名,一行一个",
enterImportIpOrDomain: "请输入要导入的IP或域名",
bulkImport: "批量导入",
basicLimitError: "基础版只能添加一个监控站点,请赞助升级专业版",
limitExceeded: "对不起,您最多只能创建条{max}监控记录,请购买或升级套餐",
@@ -89,15 +89,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: {
async beforeSubmit({ form }) {
if (form.challengeType === "cname") {
throw new Error("CNAME方式请前往CNAME记录页面进行管理");
throw new Error(t("certd.domain.cnameManagedInCnamePage"));
}
if (form.challengeType === "dns") {
const isSubdomain = await api.IsSubdomain({ domain: form.domain });
if (isSubdomain && !subdomainConfirmed.value) {
Modal.confirm({
title: "子域名确认",
content: `检测到${form.domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?`,
okText: "确认",
title: t("certd.domain.subdomainConfirmTitle"),
content: t("certd.domain.subdomainConfirmContent", { domain: form.domain }),
okText: t("common.confirm"),
okType: "danger",
onOk: () => {
subdomainConfirmed.value = true;
@@ -119,9 +119,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
import: {
show: hasActionPermission("write"),
title: "从域名提供商导入域名",
title: t("certd.domain.importFromProvider"),
type: "primary",
text: "从域名提供商导入",
text: t("certd.domain.importFromProvider"),
needPlus: true,
color: "gold",
icon: "mingcute:vip-1-line",
@@ -135,14 +135,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
syncExpirationDate: {
show: hasActionPermission("write"),
title: "同步域名过期时间",
title: t("certd.domain.syncExpirationDate"),
type: "primary",
icon: "ion:refresh-outline",
text: "同步域名过期时间",
text: t("certd.domain.syncExpirationDate"),
click: async () => {
await api.SyncExpirationStart();
notification.success({
message: "同步任务已提交",
message: t("certd.domain.syncTaskSubmitted"),
});
setTimeout(() => {
crudExpose.doRefresh();
@@ -151,10 +151,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
monitorSettingSave: {
show: hasActionPermission("write"),
title: "域名过期监控设置",
title: t("certd.domain.expirationMonitorSetting"),
type: "primary",
icon: "ion:save-outline",
text: "域名过期监控设置",
text: t("certd.domain.expirationMonitorSetting"),
click: async () => {
router.push({
path: "/certd/cert/domain/setting",
@@ -184,7 +184,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
form: {
required: true,
helper: "注意:DNS校验方式下,子域名不需要在此处维护,否则会影响证书申请(子域名托管或免费二级域名除外)",
helper: t("certd.domain.subdomainDnsHelper"),
},
editForm: {
component: {
@@ -361,7 +361,7 @@ 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"}>{row.httpUploadRootDir}</a-tag>
<a-tag class={"ml-5 flex items-center"}>{t("certd.domain.path")}: {row.httpUploadRootDir}</a-tag>
</div>
);
}
@@ -1,16 +1,16 @@
<template>
<div class="domain-import-task-status min-h-[300px]">
<div class="action mb-5">
<fs-button type="primary" icon="mingcute:vip-1-line" @click="addTask">添加导入任务</fs-button>
<fs-button type="primary" icon="ion:refresh-outline" class="ml-2" @click="loadImportTaskStatus">刷新</fs-button>
<fs-button type="primary" icon="mingcute:vip-1-line" @click="addTask">{{ t("certd.domain.addImportTask") }}</fs-button>
<fs-button type="primary" icon="ion:refresh-outline" class="ml-2" @click="loadImportTaskStatus">{{ t("certd.domain.refresh") }}</fs-button>
</div>
<div class="table-container overflow-auto mb-10">
<table class="cd-table border-gray-300 w-full">
<thead>
<tr>
<th class="w-[220px]">来源</th>
<th class="">进度</th>
<th class="w-[220px]">操作</th>
<th class="w-[220px]">{{ t("certd.sourcee") }}</th>
<th class="">{{ t("certd.domain.progress") }}</th>
<th class="w-[220px]">{{ t("certd.domain.operation") }}</th>
</tr>
</thead>
<tbody>
@@ -27,23 +27,23 @@
<td>
<div v-if="item.task">
<div>
<a-tag color="blue">总数{{ item.task?.total }}</a-tag>
<a-tag color="success" class="ml-2">成功{{ item.task?.successCount }}</a-tag>
<a-tag type="info" class="ml-2">跳过{{ item.task?.skipCount }}</a-tag>
<a-tag color="blue">{{ t("certd.domain.total") }}{{ item.task?.total }}</a-tag>
<a-tag color="success" class="ml-2">{{ t("certd.success") }}{{ item.task?.successCount }}</a-tag>
<a-tag type="info" class="ml-2">{{ t("certd.domain.skipped") }}{{ item.task?.skipCount }}</a-tag>
<a-tooltip v-if="item.task?.errors.length > 0">
<template #title>
<div v-for="error in item.task?.errors" :key="error">{{ error }}</div>
</template>
<a-tag color="red" class="ml-2">失败{{ item.task?.errors.length }}</a-tag>
<a-tag color="red" class="ml-2">{{ t("certd.domain.failed") }}{{ item.task?.errors.length }}</a-tag>
</a-tooltip>
</div>
<a-progress :percent="item.task?.progress" size="small" status="active" />
</div>
<div v-else>未执行</div>
<div v-else>{{ t("certd.domain.notExecuted") }}</div>
</td>
<td>
<fs-button type="primary" icon="ion:play-outline" :disabled="item.task?.status === 'running'" @click="startTask(item)">执行</fs-button>
<fs-button type="primary" class="ml-2" danger icon="ion:trash-outline" @click="deleteTask(item)">删除</fs-button>
<fs-button type="primary" icon="ion:play-outline" :disabled="item.task?.status === 'running'" @click="startTask(item)">{{ t("certd.domain.execute") }}</fs-button>
<fs-button type="primary" class="ml-2" danger icon="ion:trash-outline" @click="deleteTask(item)">{{ t("certd.domain.delete") }}</fs-button>
</td>
</tr>
</tbody>
@@ -58,11 +58,13 @@ import { onMounted, onUnmounted, ref } from "vue";
import * as api from "./api";
import { useDomainImport } from "./use";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "/@/locales";
defineOptions({
name: "DomainImportTaskStatus",
});
const list = ref([]);
const { t } = useI18n();
async function loadImportTaskStatus() {
const res = await api.ImportTaskStatus();
@@ -77,8 +79,8 @@ async function startTask(item: any) {
async function deleteTask(item: any) {
Modal.confirm({
title: "确认删除吗?",
okText: "确认",
title: t("certd.domain.confirmDelete"),
okText: t("common.confirm"),
okType: "danger",
onOk: async () => {
await api.ImportTaskDelete(item.key);
@@ -5,12 +5,14 @@ import { compute } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { useSettingStore } from "/@/store/settings";
import DomainImportTaskStatus from "./import.vue";
import { useI18n } from "/@/locales";
export function useDomainImport() {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
const columns = {
dnsProviderType: {
title: "域名提供商",
title: t("certd.domain.domainProvider"),
type: "text",
form: {
component: {
@@ -29,14 +31,14 @@ export function useDomainImport() {
},
},
dnsProviderAccessType: {
title: "域名提供商访问类型",
title: t("certd.domain.domainProviderAccessType"),
type: "text",
form: {
show: false,
},
},
dnsProviderAccessId: {
title: "域名提供商授权",
title: t("certd.domain.domainProviderAccess"),
type: "text",
form: {
component: {
@@ -52,7 +54,7 @@ export function useDomainImport() {
return function openDomainImportDialog(req: { afterSubmit?: (res?: any) => void; form?: any }) {
openFormDialog({
title: "从域名提供商导入域名",
title: t("certd.domain.importFromProvider"),
columns: columns,
initialForm: {
...req.form,
@@ -73,10 +75,11 @@ export function useDomainImport() {
export function useDomainImportManage() {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
const settingStore = useSettingStore();
return async function openDomainImportManageDialog(req: { afterSubmit?: (res?: any) => void; form?: any; zIndex?: number }) {
await openFormDialog({
title: "从域名提供商导入域名",
title: t("certd.domain.importFromProvider"),
body: () => {
return <DomainImportTaskStatus />;
},
@@ -79,9 +79,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
import: {
show: hasActionPermission("write"),
title: "导入CNAME记录",
title: t("certd.cname.importRecords"),
type: "primary",
text: "批量导入",
text: t("certd.cname.batchImport"),
icon: "mingcute:vip-1-line",
click: () => {
settingStore.checkPlus();
@@ -95,9 +95,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
export: {
title: "导出CNAME记录之后,可用于批量导入cname解析到域名注册商",
title: t("certd.cname.exportRecordsTip"),
type: "primary",
text: "批量导出",
text: t("certd.cname.batchExport"),
icon: "mingcute:vip-1-line",
click: () => {
settingStore.checkPlus();
@@ -235,8 +235,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
cellRender({ value, row }) {
async function resetStatus() {
Modal.confirm({
title: "重置状态",
content: "确定要重置校验状态吗?",
title: t("certd.cname.resetStatus"),
content: t("certd.cname.confirmResetStatus"),
onOk: async () => {
await api.ResetStatus(row.id);
await crudExpose.doRefresh();
@@ -253,7 +253,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
)}
{row.status === "valid" && (
<a-tooltip title={"重置校验状态,重新校验"}>
<a-tooltip title={t("certd.cname.resetStatusTooltip")}>
<fs-icon class={"ml-5 pointer "} icon="solar:undo-left-square-bold" onClick={resetStatus}></fs-icon>
</a-tooltip>
)}
@@ -2,6 +2,7 @@ import { dict } from "@fast-crud/fast-crud";
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
import { useI18n } from "/@/locales";
export const cnameProviderDict = dict({
url: "/cname/provider/list",
@@ -10,10 +11,11 @@ export const cnameProviderDict = dict({
});
export function useCnameImport() {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
const columns = {
domainList: {
title: "域名列表",
title: t("certd.cname.domainList"),
type: "text",
form: {
component: {
@@ -24,11 +26,11 @@ export function useCnameImport() {
span: 24,
},
required: true,
helper: "每个域名一行,批量导入\n泛域名请去掉*.\n已经存在的会自动跳过",
helper: t("certd.cname.domainListHelper"),
},
},
cnameProviderId: {
title: "CNAME服务",
title: t("certd.cname.cnameService"),
type: "dict-select",
dict: cnameProviderDict,
form: {
@@ -39,14 +41,14 @@ export function useCnameImport() {
return function openCnameImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "导入CNAME记录",
title: t("certd.cname.importRecords"),
columns: columns,
onSubmit: async (form: any) => {
await api.Import({
domainList: form.domainList,
cnameProviderId: form.cnameProviderId,
});
message.success("导入任务已提交");
message.success(t("certd.cname.importTaskSubmitted"));
if (req.afterSubmit) {
req.afterSubmit();
}
@@ -69,17 +69,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
title: t("monitor.confirmTitle"),
content: t("monitor.batchDeleteConfirm", { count: selectedRowKeys.value.length }),
async onOk() {
await api.BatchDelObj(selectedRowKeys.value);
message.info("删除成功");
message.info(t("monitor.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
message.error(t("monitor.selectRecordsFirst"));
}
};
@@ -265,7 +265,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "link",
text: null,
tooltip: {
title: "立即检查",
title: t("monitor.checkNow"),
},
icon: "ion:play-sharp",
click: async ({ row }) => {
@@ -17,7 +17,7 @@
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<a-tooltip :title="t('monitor.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
@@ -2,14 +2,16 @@ import { useFormWrapper } from "@fast-crud/fast-crud";
import SiteIpCertMonitor from "./index.vue";
import { siteIpApi } from "/@/views/certd/monitor/site/ip/api";
import { useI18n } from "/@/locales";
export function useSiteIpMonitor() {
const { openDialog, openCrudFormDialog } = useFormWrapper();
const { t } = useI18n();
async function openSiteIpMonitorDialog(opts: { siteId: number }) {
await openDialog({
wrapper: {
title: "站点IP监控",
title: t("monitor.siteIpMonitor"),
width: "80%",
is: "a-modal",
footer: false,
@@ -40,10 +42,10 @@ export function useSiteIpMonitor() {
columns: {
text: {
type: "textarea",
title: "IP列表",
title: t("monitor.ipList"),
form: {
helper: "IP或者CNAME域名,一行一个",
rules: [{ required: true, message: "请输入要导入的IP或域名" }],
helper: t("monitor.ipListHelper"),
rules: [{ required: true, message: t("monitor.enterImportIpOrDomain") }],
component: {
placeholder: "192.168.1.2\ncname.foo.com",
rows: 8,