Merge branch 'v2-dev' into v2-dev-buy

This commit is contained in:
xiaojunnuo
2025-08-29 16:54:11 +08:00
78 changed files with 1559 additions and 225 deletions
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes
* 修复cron选择组件星期显示错误的bug ([eb75e52](https://github.com/certd/certd/commit/eb75e52278f94a72643f7317e6740fb42666c68a))
### Performance Improvements
* 短信验证码支持腾讯云 ([9108459](https://github.com/certd/certd/commit/9108459ae42bcd95a59acba164a64e82e5f2cfe6))
* 商业版支持自定义插件的参数配置 ([17f23f3](https://github.com/certd/certd/commit/17f23f37516af925d5049291d67d41e4271f81f8))
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* openapi返回证书时挑选匹配范围最小的那一个;增加format参数,增加返回值p7b格式,增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/ui-client
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.36.17",
"version": "1.36.18",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -30,6 +30,7 @@
"@ant-design/icons-vue": "^7.0.1",
"@aws-sdk/client-s3": "^3.535.0",
"@aws-sdk/s3-request-presigner": "^3.535.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0",
"@fast-crud/fast-crud": "^1.25.13",
"@fast-crud/fast-extends": "^1.25.13",
@@ -43,7 +44,6 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/vue-store": "^0.7.0",
"@vee-validate/zod": "^4.15.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@vue/shared": "^3.5.13",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.6",
@@ -103,8 +103,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@certd/lib-iframe": "^1.36.18",
"@certd/pipeline": "^1.36.18",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -564,7 +564,7 @@ export default {
ipv6Priority: "IPv6 Priority",
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
enableCommonCnameService: "Enable Public CNAME Service",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <a href='#/sys/cname/provider'>custom CNAME service</a> is set, CNAME proxy certificate application will not work.",
enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery",
saveButton: "Save",
stopSuccess: "Stopped successfully",
@@ -587,6 +587,7 @@ export default {
commFeature: "Commercial feature",
smsProvider: "SMS provider",
aliyunSms: "Aliyun SMS",
tencentSms: "Tencent SMS",
yfySms: "YFY SMS",
smsTest: "SMS test",
testMobilePlaceholder: "Enter test mobile number",
@@ -570,7 +570,7 @@ export default {
ipv6Priority: "IPV6优先",
dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6",
enableCommonCnameService: "启用公共CNAME服务",
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>,则无法使用CNAME代理方式申请证书",
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<a href='#/sys/cname/provider'>自定义CNAME服务</a>,则无法使用CNAME代理方式申请证书",
enableCommonSelfServicePasswordRetrieval: "启用自助找回密码",
saveButton: "保存",
stopSuccess: "停止成功",
@@ -593,6 +593,7 @@ export default {
commFeature: "商业版功能",
smsProvider: "短信提供商",
aliyunSms: "阿里云短信",
tencentSms: "腾讯云短信",
yfySms: "易发云短信",
smsTest: "短信测试",
testMobilePlaceholder: "输入测试手机号",
@@ -133,7 +133,7 @@ export const sysResources = [
title: "certd.sysResources.sysPluginConfig",
name: "SysPluginConfig",
path: "/sys/plugin/config",
component: "/sys/plugin/config.vue",
component: "/sys/plugin/config-common.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
@@ -1,9 +1,11 @@
import { defineStore } from "pinia";
import * as api from "./api.plugin";
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud";
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState {
group?: PluginGroups;
originGroup?: PluginGroups;
}
export type PluginGroup = {
@@ -32,14 +34,17 @@ export class PluginGroups {
groups!: { [key: string]: PluginGroup };
map!: { [key: string]: PluginDefine };
t: any;
constructor(groups: { [key: string]: PluginGroup }) {
mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
this.groups = groups;
this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups);
this.initMap();
}
private initGroup(groups: { [p: string]: PluginGroup }) {
const { merge } = useMerge();
const all: PluginGroup = {
key: "all",
title: this.t("certd.all"),
@@ -48,6 +53,14 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded",
};
for (const key in groups) {
if (this.mergeSetting) {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
}
}
}
all.plugins.push(...groups[key].plugins);
}
this.groups = {
@@ -132,11 +145,15 @@ export const usePluginStore = defineStore({
id: "app.plugin",
state: (): PluginState => ({
group: null,
originGroup: null,
}),
actions: {
async reload() {
const groups = await api.GetGroups({});
this.group = new PluginGroups(groups);
this.group = new PluginGroups(groups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(groups));
console.log("group", this.group);
console.log("originGroup", this.originGroup);
},
async init() {
if (!this.group) {
@@ -150,6 +167,7 @@ export const usePluginStore = defineStore({
},
async clear() {
this.group = null;
this.originGroup = null
},
async getList(): Promise<PluginDefine[]> {
await this.init();
@@ -159,6 +177,10 @@ export const usePluginStore = defineStore({
await this.init();
return this.group.get(name);
},
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) {
return await api.GetPluginConfig(query);
},
@@ -304,3 +304,11 @@ h6 {
padding: 10px;
color: #6e6e6e;
}
.ant-modal-body{
.fs-form-body{
max-height: 66vh;
overflow-y: auto;
}
}
@@ -84,6 +84,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
component: {
color: "auto",
},
order: -1,
},
form: {
component: {
@@ -82,6 +82,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 300,
order: -11,
},
},
from: {
@@ -138,6 +138,7 @@ export function useCertPipelineCreator() {
form: {
doSubmit,
wrapper: {
wrapClassName: "cert_pipeline_create_form",
width: 1350,
saveRemind: false,
title: t("certd.pipelineForm.createTitle"),
@@ -115,4 +115,13 @@ function batchRerun() {
padding-left: 10px;
}
}
.cert_pipeline_create_form {
.ant-collapse {
margin: 10px;
}
.ant-collapse-header {
text-align: right;
}
}
</style>
@@ -43,7 +43,7 @@
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-button type="primary" size="large" html-type="submit" :loading="loading" class="login-button">
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
{{ t("authentication.loginButton") }}
</a-button>
@@ -217,7 +217,6 @@ export default defineComponent({
</script>
<style lang="less">
.login-page.main {
//margin: 20px !important;
margin-bottom: 100px;
@@ -97,6 +97,7 @@ export type CertApplyPluginSysInput = {
export type PluginSysSetting<T> = {
sysSetting: {
input?: T;
metadata?: Record<string, any>;
};
};
export type CommPluginConfig = {
@@ -118,6 +119,14 @@ export async function SaveCommPluginConfigs(data: CommPluginConfig): Promise<voi
});
}
export async function savePluginSetting(req: { name: string; sysSetting: any }): Promise<void> {
return await request({
url: apiPrefix + "/saveSetting",
method: "post",
data: req,
});
}
export async function DoTest(req: { id: number; input: any }): Promise<void> {
return await request({
url: apiPrefix + "/doTest",
@@ -0,0 +1,184 @@
<template>
<div class="plugin-config">
<div class="origin-metadata w-100%">
<div class="block-title">
自定义插件参数配置
<div class="helper">可以设置插件选项的配置设置配置默认值修改帮助说明设置是否显示该字段等</div>
</div>
<div class="p-10">
<div ref="formRef" class="config-form w-full" :label-col="labelCol" :wrapper-col="wrapperCol">
<table class="table-fixed w-full">
<thead>
<tr>
<th class="text-left p-5" width="200px">插件参数</th>
<th class="text-left p-5" width="100px">参数配置</th>
<th class="text-left flex-1 p-5">自定义</th>
</tr>
</thead>
<tbody>
<template v-for="item in originInputs" :key="item.key">
<template v-for="prop in editableKeys" :key="prop.key">
<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 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' }">
<rollbackable :value="configForm[item.key][prop.key]" @set="prop.onSet(item)" @clear="delete configForm[item.key][prop.key]">
<template #default>
<fs-render :render-func="prop.defaultRender(item)"></fs-render>
</template>
<template #edit>
<fs-render :render-func="prop.editRender(item)"></fs-render>
</template>
</rollbackable>
</td>
</tr>
</template>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { computed, nextTick, onMounted, reactive, ref, Ref, unref } from "vue";
import { useRoute, useRouter } from "vue-router";
import * as api from "./api";
import { usePluginStore } from "/@/store/plugin";
import { cloneDeep, get, merge, set, unset } from "lodash-es";
import Rollbackable from "./rollbackable.vue";
import { FsRender } from "@fast-crud/fast-crud";
const route = useRoute();
const router = useRouter();
const pluginStore = usePluginStore();
const props = defineProps<{
plugin: any;
}>();
const pluginMetadata = ref<any>("");
const currentPlugin = ref();
const labelCol = ref({
span: null,
style: {
width: "145px",
},
});
const wrapperCol = ref({ span: 16 });
const configForm: any = reactive({});
function getScope() {
return {
form: configForm,
};
}
function getForm() {
return configForm;
}
const editableKeys = ref([
{
key: "value",
label: "默认值",
onSet(item: any) {
configForm[item.key]["value"] = item.value ?? null;
},
defaultRender(item: any) {
return () => {
return item["value"] ?? "";
};
},
editRender(item: any) {
return () => {
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key]["value"]} scope={getScope()} />;
};
},
},
{
key: "show",
label: "是否显示",
onSet(item: any) {
configForm[item.key]["show"] = item.show ?? true;
},
defaultRender(item: any) {
return () => {
const value = item["show"];
return value === false ? "不显示" : "显示";
};
},
editRender(item: any) {
return () => {
return <a-switch vModel:checked={configForm[item.key]["show"]} />;
};
},
},
{
key: "helper",
label: "帮助说明",
onSet(item: any) {
configForm[item.key]["helper"] = item.helper ?? "";
},
defaultRender(item: any) {
return () => {
return <pre class={"helper"}>{item["helper"]}</pre>;
};
},
editRender(item: any) {
return () => {
return <a-textarea rows={5} vModel:value={configForm[item.key]["helper"]} />;
};
},
},
]);
const originInputs = computed(() => {
if (!currentPlugin.value) {
return;
}
const input = cloneDeep(currentPlugin.value.input);
const newInputs: any = {};
for (const key in input) {
const value = input[key];
value.key = key;
const newInput: any = cloneDeep(value);
newInputs[key] = newInput;
}
return newInputs;
});
function clearFormValue(key: string) {
unset(configForm, key);
console.log(key, configForm);
}
async function loadPluginSetting() {
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
for (const key in currentPlugin.value.input) {
configForm[key] = {};
}
const setting = props.plugin.sysSetting;
if (setting) {
const settingJson = JSON.parse(setting);
merge(configForm, settingJson.metadata?.input || {});
}
}
onMounted(async () => {
await loadPluginSetting();
});
defineExpose({
getForm,
});
</script>
<style lang="less">
.plugin-config {
pre {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
}
</style>
@@ -1,11 +1,14 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { Ref, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
//@ts-ignore
import yaml from "js-yaml";
import { usePluginImport } from "./use-import";
import { usePluginConfig } from "./use-config";
import { useSettingStore } from "/src/store/settings/index";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -35,75 +38,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const { openCrudFormDialog } = useFormWrapper();
async function openImportDialog() {
function createCrudOptions() {
return {
crudOptions: {
columns: {
content: {
title: t("certd.pluginFile"),
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 8,
},
},
col: {
span: 24,
},
helper: t("certd.selectPluginFile"),
},
},
override: {
title: t("certd.overrideSameName"),
type: "dict-switch",
dict: dict({
data: [
{
value: true,
label: t("certd.override"),
},
{
value: false,
label: t("certd.noOverride"),
},
],
}),
form: {
value: false,
col: {
span: 24,
},
helper: t("certd.overrideHelper"),
},
},
},
form: {
wrapper: {
title: t("certd.importPlugin"),
saveRemind: false,
},
afterSubmit() {
notification.success({ message: t("certd.operationSuccess") });
crudExpose.doRefresh();
},
async doSubmit({ form }: any) {
return await api.ImportPlugin({
...form,
});
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
const { openImportDialog } = usePluginImport();
const { openConfigDialog } = usePluginConfig();
const settingStore = useSettingStore();
return {
crudOptions: {
settings: {
@@ -139,7 +78,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
text: t("certd.import"),
type: "primary",
async click() {
await openImportDialog();
await openImportDialog({ crudExpose });
},
},
},
@@ -186,6 +125,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
},
},
config: {
show: computed(() => {
return settingStore.isComm;
}),
text: null,
icon: "ion:settings-outline",
title: t("certd.config"),
type: "link",
async click({ row }) {
await openConfigDialog({
row,
crudExpose,
});
},
},
},
},
table: {
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { defineProps } from "vue";
const props = defineProps<{ value: any }>();
const emits = defineEmits(["set", "clear"]);
function setValue() {
emits("set");
}
function clearValue() {
emits("clear");
}
</script>
<template>
<div class="rollbackable">
<div class="flex">
<div style="width: 100px">
<a-tag v-if="value === undefined" color="green" size="small" class="pointer flex-inline items-center" @click.stop="setValue">
<fs-icon icon="material-symbols:edit" class="mr-5"></fs-icon>
自定义
</a-tag>
<a-tag v-else color="red" size="small" class="pointer flex-inline items-center" @click.stop="clearValue">
<fs-icon icon="material-symbols:undo" class="mr-5"></fs-icon>
还原
</a-tag>
</div>
<div class="flex-1 overflow-hidden value-render">
<slot v-if="value === undefined" name="default"></slot>
<slot v-else name="edit"></slot>
</div>
</div>
</div>
</template>
<style lang="less">
.rollbackable {
.value-render {
.ant-select,
.ant-input {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,79 @@
import * as api from "/@/views/sys/plugin/api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { useI18n } from "/@/locales";
import { notification } from "ant-design-vue";
import ConfigEditor from "./config-editor.vue";
import { ref } from "vue";
import { usePluginStore } from "/@/store/plugin";
export function usePluginConfig() {
const { openCrudFormDialog } = useFormWrapper();
const { t } = useI18n();
const pluginStore = usePluginStore();
// @ts-ignore
async function openConfigDialog({ row, crudExpose }) {
const configEditorRef = ref();
function createCrudOptions() {
return {
crudOptions: {
columns: {},
form: {
wrapper: {
width: "80%",
title: "插件参数自定义",
saveRemind: false,
slots: {
"form-body-top": () => {
return (
<div>
<ConfigEditor ref={configEditorRef} plugin={row}></ConfigEditor>
</div>
);
},
},
},
afterSubmit() {
notification.success({ message: t("certd.operationSuccess") });
crudExpose.doRefresh();
},
async doSubmit({}: any) {
const form = configEditorRef.value.getForm();
const newForm: any = {};
for (const key in form) {
const value = form[key];
if (value && Object.keys(value).length > 0) {
newForm[key] = value;
}
}
const res = await api.savePluginSetting({
name: row.name,
sysSetting: {
metadata: {
input: newForm,
},
},
});
await pluginStore.clear();
return res;
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
// modal.confirm({
// title: "插件元数据配置",
// width: "80%",
// content: () => {
// return <ConfigEditor plugin={row}></ConfigEditor>;
// },
// });
}
return {
openConfigDialog,
};
}
@@ -0,0 +1,80 @@
import * as api from "/@/views/sys/plugin/api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { useI18n } from "/@/locales";
import { Modal, notification } from "ant-design-vue";
export function usePluginImport() {
const { openCrudFormDialog } = useFormWrapper();
const { t } = useI18n();
async function openImportDialog({ crudExpose }) {
function createCrudOptions() {
return {
crudOptions: {
columns: {
content: {
title: t("certd.pluginFile"),
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 8,
},
},
col: {
span: 24,
},
helper: t("certd.selectPluginFile"),
},
},
override: {
title: t("certd.overrideSameName"),
type: "dict-switch",
dict: dict({
data: [
{
value: true,
label: t("certd.override"),
},
{
value: false,
label: t("certd.noOverride"),
},
],
}),
form: {
value: false,
col: {
span: 24,
},
helper: t("certd.overrideHelper"),
},
},
},
form: {
wrapper: {
title: t("certd.importPlugin"),
saveRemind: false,
},
afterSubmit() {
notification.success({ message: t("certd.operationSuccess") });
crudExpose.doRefresh();
},
async doSubmit({ form }: any) {
return await api.ImportPlugin({
...form,
});
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
return {
openImportDialog,
};
}
@@ -45,6 +45,7 @@
<a-form-item :label="t('certd.smsProvider')" :name="['private', 'sms', 'type']">
<a-select v-model:value="formState.private.sms.type" @change="smsTypeChange">
<a-select-option value="aliyun">{{ t("certd.aliyunSms") }}</a-select-option>
<a-select-option value="tencent">{{ t("certd.tencentSms") }}</a-select-option>
<a-select-option value="yfysms">{{ t("certd.yfySms") }}</a-select-option>
</a-select>
</a-form-item>