mirror of
https://github.com/certd/certd.git
synced 2026-07-08 13:47:36 +08:00
Merge branch 'v2-dev' into v2-invite
This commit is contained in:
@@ -135,6 +135,7 @@ export default defineConfig({
|
|||||||
{text: "支付宝配置", link: "/guide/use/comm/payments/alipay.md"},
|
{text: "支付宝配置", link: "/guide/use/comm/payments/alipay.md"},
|
||||||
{text: "微信支付配置", link: "/guide/use/comm/payments/wxpay.md"},
|
{text: "微信支付配置", link: "/guide/use/comm/payments/wxpay.md"},
|
||||||
{text: "彩虹易支付配置", link: "/guide/use/comm/payments/yizhifu.md"},
|
{text: "彩虹易支付配置", link: "/guide/use/comm/payments/yizhifu.md"},
|
||||||
|
{text: "插件选项映射", link: "/guide/use/comm/plugin/"},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
|
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
|
||||||
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
|
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
|
||||||
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
|
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
|
||||||
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
|
| 通知 | 邮件通知、自定义webhook | 企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
|
||||||
| 站点监控 | 限制1条 | 无限制 | 无限制 |
|
| 站点监控 | 限制1条 | 无限制 | 无限制 |
|
||||||
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
|
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
|
||||||
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
|
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
* [支付宝支付配置](./payments/alipay.md)
|
* [支付宝支付配置](./payments/alipay.md)
|
||||||
* [微信支付配置](./payments/wxpay.md)
|
* [微信支付配置](./payments/wxpay.md)
|
||||||
* [彩虹易支付配置](./payments/yizhifu.md)
|
* [彩虹易支付配置](./payments/yizhifu.md)
|
||||||
|
* [插件选项映射](./plugin/)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
# 插件选项映射
|
||||||
|
|
||||||
|
商业版可以通过插件配置,自定义插件中下拉选择框的选项显示内容。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
插件中部分下拉选择框的选项可能带有"免费"、"测试"等字眼,商业版运营场景下需要隐藏或改写这些文字。
|
||||||
|
|
||||||
|
## 配置方式
|
||||||
|
|
||||||
|
1. 进入"系统管理" → "插件管理"
|
||||||
|
2. 找到需要配置的插件(如 CertApply 证书申请),点击"配置"按钮
|
||||||
|
3. 在"插件参数自定义"对话框中,找到带有下拉选项的参数(如"证书颁发机构")
|
||||||
|
4. 该参数的配置行会多出一项"选项映射",点击"自定义"
|
||||||
|
|
||||||
|
### 填写映射关系
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
系统会列出该下拉框的所有**选项值**和**原始显示内容**:
|
||||||
|
|
||||||
|
| 选项值 | 原始显示 | 自定义显示 |
|
||||||
|
|---|---|---|
|
||||||
|
| letsencrypt | Let's Encrypt(免费,新手推荐,支持IP证书) | [输入框] |
|
||||||
|
| google | Google(免费) | [输入框] |
|
||||||
|
|
||||||
|
- "自定义显示"一列为输入框,默认 placeholder 显示原始内容
|
||||||
|
- 只需填写**需要改写**的选项,留空的选项将保持原始显示
|
||||||
|
- 例如:将 Let's Encrypt(免费,新手推荐,支持IP证书) 改写为 Let's Encrypt
|
||||||
|
|
||||||
|
### 保存生效
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
填写完成后保存配置,用户在创建证书流水线时看到的选项文字即会变更为自定义内容。
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +57,18 @@ export class PluginGroups {
|
|||||||
for (const plugin of groups[key].plugins) {
|
for (const plugin of groups[key].plugins) {
|
||||||
if (plugin.sysSetting) {
|
if (plugin.sysSetting) {
|
||||||
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
|
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
|
||||||
|
// 应用选项映射
|
||||||
|
for (const key of Object.keys(plugin.input)) {
|
||||||
|
const inputDef = plugin.input[key];
|
||||||
|
if (inputDef.optionsMapping && inputDef.component?.options) {
|
||||||
|
const mapping = inputDef.optionsMapping;
|
||||||
|
for (const opt of inputDef.component.options) {
|
||||||
|
if (mapping[opt.value] !== undefined) {
|
||||||
|
opt.label = mapping[opt.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template v-for="item in originInputs" :key="item.key">
|
<template v-for="item in originInputs" :key="item.key">
|
||||||
<template v-for="prop in editableKeys" :key="prop.key">
|
<template v-for="prop in getEditableKeys(item)" :key="prop.key">
|
||||||
<tr>
|
<tr>
|
||||||
<td v-if="prop.key === 'value'" class="border-t-2 p-5" rowspan="3" :class="{ 'border-t-2': prop.key === 'value' }">{{ item.title }}</td>
|
<td v-if="prop.key === 'value'" class="border-t-2 p-5" :rowspan="getEditableKeys(item).length" :class="{ 'border-t-2': prop.key === 'value' }">{{ item.title }}</td>
|
||||||
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">{{ prop.label }}</td>
|
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">{{ prop.label }}</td>
|
||||||
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">
|
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">
|
||||||
<rollbackable :value="configForm[item.key][prop.key]" @set="prop.onSet(item)" @clear="delete configForm[item.key][prop.key]">
|
<rollbackable :value="configForm[item.key][prop.key]" @set="prop.onSet(item)" @clear="delete configForm[item.key][prop.key]">
|
||||||
@@ -157,6 +157,92 @@ const editableKeys = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const optionsMappingKey = {
|
||||||
|
key: "optionsMapping",
|
||||||
|
label: "选项映射",
|
||||||
|
onSet(item: any) {
|
||||||
|
configForm[item.key]["optionsMapping"] = item.optionsMapping ?? null;
|
||||||
|
},
|
||||||
|
defaultRender(item: any) {
|
||||||
|
return () => {
|
||||||
|
const mapping = item["optionsMapping"];
|
||||||
|
if (!mapping || Object.keys(mapping).length === 0) {
|
||||||
|
return <span class="text-gray-400">未设置</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class="options-mapping-tags">
|
||||||
|
{Object.entries(mapping).map(([key, label]: any) => (
|
||||||
|
<a-tag color="blue" size="small" class="mb-2 mr-2">
|
||||||
|
{key} → {label}
|
||||||
|
</a-tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
editRender(item: any) {
|
||||||
|
return () => {
|
||||||
|
const options = item.component?.options || [];
|
||||||
|
if (options.length === 0) {
|
||||||
|
return <span class="text-gray-400">该组件没有预设选项</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLabelChange = (optValue: string, newLabel: string) => {
|
||||||
|
const mapping = configForm[item.key]["optionsMapping"] || {};
|
||||||
|
if (newLabel) {
|
||||||
|
mapping[optValue] = newLabel;
|
||||||
|
configForm[item.key]["optionsMapping"] = { ...mapping };
|
||||||
|
} else {
|
||||||
|
delete mapping[optValue];
|
||||||
|
if (Object.keys(mapping).length > 0) {
|
||||||
|
configForm[item.key]["optionsMapping"] = { ...mapping };
|
||||||
|
} else {
|
||||||
|
delete configForm[item.key]["optionsMapping"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabel = (optValue: string) => {
|
||||||
|
return configForm[item.key]["optionsMapping"]?.[optValue] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="options-mapping-editor">
|
||||||
|
<table class="w-full table-auto border-collapse border border-gray-300 text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">选项值</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">原始显示</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">自定义显示</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{options.map((opt: any) => (
|
||||||
|
<tr>
|
||||||
|
<td class="border border-gray-300 px-2 py-1">
|
||||||
|
<code class="text-xs">{opt.value}</code>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 text-gray-500">{opt.label}</td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1">
|
||||||
|
<a-input size="small" placeholder={opt.label} value={getLabel(opt.value)} onUpdate:value={(val: string) => onLabelChange(opt.value, val)} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="helper mt-1">只需填写需要自定义的选项,留空则使用原始显示内容</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEditableKeys(item: any) {
|
||||||
|
if (item.component?.name === "a-select" || item.component?.name === "icon-select") {
|
||||||
|
return [...editableKeys.value, optionsMappingKey];
|
||||||
|
}
|
||||||
|
return editableKeys.value;
|
||||||
|
}
|
||||||
const originInputs = computed(() => {
|
const originInputs = computed(() => {
|
||||||
if (!currentPlugin.value) {
|
if (!currentPlugin.value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<a-form-item :label="t('certd.smtpDomain')" name="host" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpDomain') }]">
|
<a-form-item :label="t('certd.smtpDomain')" name="host" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpDomain') }]">
|
||||||
<a-input v-model:value="formState.host" />
|
<a-input v-model:value="formState.host" />
|
||||||
|
<div class="helper">
|
||||||
|
{{ t("certd.sendFailHelpDoc") }}<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{ t("certd.emailConfigHelpDoc") }}</a>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item :label="t('certd.smtpPort')" name="port" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpPort') }]">
|
<a-form-item :label="t('certd.smtpPort')" name="port" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpPort') }]">
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="plus" class="plus" :disabled="!settingStore.isPlus">
|
<a-tab-pane key="plus" class="plus" :disabled="!settingStore.isPlus" v-if="formState.usePlus">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
{{ t("certd.useOfficialEmailServer") }}
|
{{ t("certd.useOfficialEmailServer") }}
|
||||||
@@ -113,7 +116,6 @@
|
|||||||
<div class="helper">
|
<div class="helper">
|
||||||
{{ t("certd.sendFailHelpDoc") }}<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{ t("certd.emailConfigHelpDoc") }}</a>
|
{{ t("certd.sendFailHelpDoc") }}<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{ t("certd.emailConfigHelpDoc") }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="helper">{{ t("certd.tryOfficialEmailServer") }}</div>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||||
<a-button type="primary" :loading="testFormState.loading" html-type="submit">{{ t("certd.test") }}</a-button>
|
<a-button type="primary" :loading="testFormState.loading" html-type="submit">{{ t("certd.test") }}</a-button>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
|
|||||||
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
|
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
|
||||||
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
|
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { set } from "lodash-es";
|
import { cloneDeep, set } from "lodash-es";
|
||||||
import { executorQueue } from "@certd/lib-server";
|
import { executorQueue } from "@certd/lib-server";
|
||||||
import parser from "cron-parser";
|
import parser from "cron-parser";
|
||||||
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
||||||
@@ -1106,39 +1106,41 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
const pipeline = JSON.parse(item.content);
|
const pipeline = JSON.parse(item.content);
|
||||||
if (trigger.props === false) {
|
if (trigger.props === false) {
|
||||||
//清除trigger
|
//清除trigger
|
||||||
pipeline.triggers = [];
|
pipeline.triggers = [];
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
const start = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[0];
|
||||||
|
let end = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[1];
|
||||||
|
if (trigger.randomRange[1] < trigger.randomRange[0]) {
|
||||||
|
//跨天
|
||||||
|
end = dayjs().add(1, "day").format("YYYY-MM-DD") + " " + trigger.randomRange[1];
|
||||||
|
}
|
||||||
|
const startTime = dayjs(start).valueOf();
|
||||||
|
const endTime = dayjs(end).valueOf();
|
||||||
|
|
||||||
|
const triggerConf = cloneDeep(trigger);
|
||||||
if (trigger.random === true) {
|
if (trigger.random === true) {
|
||||||
//随机时间
|
//随机时间
|
||||||
const start = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[0];
|
|
||||||
let end = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[1];
|
|
||||||
if (trigger.randomRange[1] < trigger.randomRange[0]) {
|
|
||||||
//跨天
|
|
||||||
end = dayjs().add(1, "day").format("YYYY-MM-DD") + " " + trigger.randomRange[1];
|
|
||||||
}
|
|
||||||
const startTime = dayjs(start).valueOf();
|
|
||||||
const endTime = dayjs(end).valueOf();
|
|
||||||
const randomTime = Math.floor(Math.random() * (endTime - startTime)) + startTime;
|
const randomTime = Math.floor(Math.random() * (endTime - startTime)) + startTime;
|
||||||
const time = dayjs(randomTime).format(" ss:mm:HH").replaceAll(":", " ").replaceAll(" 0", " ").trim();
|
const time = dayjs(randomTime).format(" ss:mm:HH").replaceAll(":", " ").replaceAll(" 0", " ").trim();
|
||||||
set(trigger, "props.cron", `${time} * * *`);
|
set(triggerConf, "props.cron", `${time} * * *`)
|
||||||
}
|
}
|
||||||
delete trigger.random;
|
delete triggerConf.random
|
||||||
delete trigger.randomRange;
|
delete triggerConf.randomRange;
|
||||||
pipeline.triggers = [
|
pipeline.triggers = [{
|
||||||
{
|
id: nanoid(),
|
||||||
id: nanoid(),
|
title: "定时触发",
|
||||||
title: "定时触发",
|
...triggerConf
|
||||||
...trigger,
|
}];
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.doUpdatePipelineJson(item, pipeline);
|
await this.doUpdatePipelineJson(item, pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any, projectId?: number) {
|
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any, projectId?: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user