chore: suite first

pull/330/head
xiaojunnuo 2024-12-17 10:27:35 +08:00
parent 64319937a1
commit 8057586dc1
20 changed files with 1785 additions and 17 deletions

View File

@ -0,0 +1,10 @@
# 证书申请失败情况
## DNS记录问题
1. DNS 不要设置CAA记录删除即可
2. DNSSEC相关报错DNSSEC管理中删除即可
3. DNS 有其他平台申请过的_acme-challenge记录删除即可

View File

@ -1,5 +1,4 @@
import { SysSettingsEntity } from './system/index.js'; import { SysSettingsEntity } from './system/index.js';
export * from './basic/index.js'; export * from './basic/index.js';
export * from './system/index.js'; export * from './system/index.js';
export { LibServerConfiguration as Configuration } from './configuration.js'; export { LibServerConfiguration as Configuration } from './configuration.js';

View File

@ -169,6 +169,48 @@ export const sysResources = [
icon: "ion:person-outline", icon: "ion:person-outline",
permission: "sys:auth:user:view" permission: "sys:auth:user:view"
} }
},
{
title: "套餐支付",
name: "SuiteManager",
path: "/sys/suite",
redirect: "/sys/suite/product",
meta: {
icon: "ion:golf-outline",
permission: "sys:settings:view"
},
children: [
{
title: "套餐管理",
name: "ProductManager",
path: "/sys/suite/product",
component: "/sys/suite/product/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
},
{
title: "支付管理",
name: "PaymentManager",
path: "/sys/suite/payment",
component: "/sys/suite/payment/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
},
{
title: "订单管理",
name: "OrderManager",
path: "/sys/suite/order",
component: "/sys/suite/order/index.vue",
meta: {
icon: "ion:person-outline",
permission: "sys:settings:edit"
}
}
]
} }
// { // {

View File

@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/suite/order";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id }
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled }
});
}

View File

@ -0,0 +1,254 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
minWidth: 200,
fixed: "right"
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
domain: {
title: "CNAME域名",
type: "text",
editForm: {
component: {
disabled: true
}
},
search: {
show: true
},
form: {
component: {
placeholder: "cname.handsfree.work"
},
helper: "需要一个右边DNS提供商注册的域名也可以将其他域名的dns服务器转移到这几家来。\nCNAME域名一旦确定不可修改建议使用一级子域名",
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 200
}
},
dnsProviderType: {
title: "DNS提供商",
type: "dict-select",
search: {
show: true
},
dict: dict({
url: "pi/dnsProvider/list",
value: "key",
label: "title"
}),
form: {
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 150,
component: {
color: "auto"
}
}
},
accessId: {
title: "DNS提供商授权",
type: "dict-select",
dict: dict({
url: "/pi/access/list",
value: "id",
label: "name"
}),
form: {
component: {
name: "access-selector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderType;
})
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 150,
component: {
color: "auto"
}
}
},
isDefault: {
title: "是否默认",
type: "dict-switch",
dict: dict({
data: [
{ label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "default" }
]
}),
form: {
value: false,
rules: [{ required: true, message: "请选择是否默认" }]
},
column: {
align: "center",
width: 100
}
},
setDefault: {
title: "设置默认",
type: "text",
form: {
show: false
},
column: {
width: 100,
align: "center",
conditionalRenderDisabled: true,
cellRender: ({ row }) => {
if (row.isDefault) {
return;
}
const onClick = async () => {
Modal.confirm({
title: "提示",
content: `确定要设置为默认吗?`,
onOk: async () => {
await api.SetDefault(row.id);
await crudExpose.doRefresh();
}
});
};
return (
<a-button type={"link"} size={"small"} onClick={onClick}>
</a-button>
);
}
}
},
disabled: {
title: "禁用/启用",
type: "dict-switch",
dict: dict({
data: [
{ label: "启用", value: false, color: "success" },
{ label: "禁用", value: true, color: "error" }
]
}),
form: {
value: false
},
column: {
width: 100,
component: {
title: "点击可禁用/启用",
on: {
async click({ value, row }) {
Modal.confirm({
title: "提示",
content: `确定要${!value ? "禁用" : "启用"}吗?`,
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
}
});
}
}
}
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 160,
align: "center"
}
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true,
width: 160
}
}
}
}
};
}

View File

@ -0,0 +1,57 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
CNAME服务配置
<span class="sub">
此处配置的域名作为其他域名校验的代理当别的域名需要申请证书时通过CNAME映射到此域名上来验证所有权好处是任何域名都可以通过此方式申请证书也无需填写AccessSecret
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">CNAME功能原理及使用说明</a>
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "CnameProvider"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@ -0,0 +1,76 @@
import { request } from "/src/api/service";
export function createPaymentApi() {
const apiPrefix = "/sys/suite/payment";
return {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
},
async AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
},
async UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
},
async GetOptions(id: number) {
return await request({
url: apiPrefix + "/options",
method: "post"
});
},
async GetSimpleInfo(id: number) {
return await request({
url: apiPrefix + "/simpleInfo",
method: "post",
params: { id }
});
},
async GetDefineTypes() {
return await request({
url: apiPrefix + "/getTypeDict",
method: "post"
});
},
async GetProviderDefine(type: string) {
return await request({
url: apiPrefix + "/define",
method: "post",
params: { type }
});
}
};
}

View File

@ -0,0 +1,194 @@
import { ColumnCompositionProps, compute, dict } from "@fast-crud/fast-crud";
import { computed, provide, ref, toRef } from "vue";
import { useReference } from "/@/use/use-refrence";
import { forEach, get, merge, set } from "lodash-es";
import { Modal } from "ant-design-vue";
import * as api from "/@/views/sys/cname/provider/api";
import { mitter } from "/@/utils/util.mitt";
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
const notificationTypeDictRef = dict({
url: "/sys/suite/payment/getList"
});
const defaultPluginConfig = {
component: {
name: "a-input",
vModel: "value"
}
};
function buildDefineFields(define: any, form: any, mode: string) {
const formWrapperRef = crudExpose.getFormWrapperRef();
const columnsRef = toRef(formWrapperRef.formOptions, "columns");
for (const key in columnsRef.value) {
if (key.indexOf(".") >= 0) {
delete columnsRef.value[key];
}
}
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
forEach(define.input, (value: any, mapKey: any) => {
const key = "body." + mapKey;
const field = {
...value,
key
};
const column = merge({ title: key }, defaultPluginConfig, field);
//eval
useReference(column);
if (column.required) {
if (!column.rules) {
column.rules = [];
}
column.rules.push({ required: true, message: "此项必填" });
}
//设置默认值
if (column.value != null && get(form, key) == null) {
set(form, key, column.value);
}
//字段配置赋值
columnsRef.value[key] = column;
console.log("form", columnsRef.value, form);
});
}
const currentDefine = ref();
return {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
type: {
title: "支付类型",
type: "dict-select",
dict: notificationTypeDictRef,
search: {
show: false
},
column: {
width: 200,
component: {
color: "auto"
}
},
editForm: {
component: {
disabled: false
}
},
form: {
component: {
disabled: false,
showSearch: true,
filterOption: (input: string, option: any) => {
input = input?.toLowerCase();
return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0;
},
renderLabel(item: any) {
return (
<span class={"flex-o flex-between"}>
{item.label}
{item.needPlus && <fs-icon icon={"mingcute:vip-1-line"} className={"color-plus"}></fs-icon>}
</span>
);
}
},
rules: [{ required: true, message: "请选择通知类型" }],
valueChange: {
immediate: true,
async handle({ value, mode, form, immediate }) {
if (value == null) {
return;
}
const lastTitle = currentDefine.value?.title;
const define = await api.GetProviderDefine(value);
currentDefine.value = define;
console.log("define", define);
if (!immediate) {
form.body = {};
if (define.needPlus) {
mitter.emit("openVipModal");
}
}
if (!form.name || form.name === lastTitle) {
form.name = define.title;
}
buildDefineFields(define, form, mode);
}
},
helper: computed(() => {
const define = currentDefine.value;
if (define == null) {
return "";
}
return define.desc;
})
}
} as ColumnCompositionProps,
name: {
title: "通知名称",
search: {
show: true
},
type: ["text"],
form: {
rules: [{ required: true, message: "请填写名称" }],
helper: "随便填,当多个相同类型的通知时,便于区分"
},
column: {
width: 200
}
},
test: {
title: "测试",
form: {
show: compute(({ form }) => {
return !!form.type;
}),
component: {
name: "api-test",
action: "TestRequest"
},
order: 990,
col: {
span: 24
}
},
column: {
show: false
}
},
setting: {
column: { show: false },
form: {
show: false,
valueBuilder({ value, form }) {
form.body = {};
if (!value) {
return;
}
const setting = JSON.parse(value);
for (const key in setting) {
form.body[key] = setting[key];
}
},
valueResolve({ form }) {
const setting = form.body;
form.setting = JSON.stringify(setting);
}
}
} as ColumnCompositionProps
};
}

View File

@ -0,0 +1,54 @@
import { ref } from "vue";
import { getCommonColumnDefine } from "./common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { createNotificationApi } from "/@/views/certd/notification/api";
const api = createNotificationApi();
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const addRequest = async (req: AddReq) => {
const { form } = req;
const res = await api.AddObj(form);
return res;
};
const typeRef = ref();
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "145px"
}
}
},
rowHandle: {
width: 200
},
columns: {
...commonColumnsDefine
}
}
};
}

View File

@ -0,0 +1,41 @@
<template>
<fs-page>
<template #header>
<div class="title">
支付方式管理
<span class="sub">管理支付方式</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createPaymentApi } from "./api";
import { notificationProvide } from "/@/views/certd/notification/common";
export default defineComponent({
name: "PaymentManager",
setup() {
const api = createPaymentApi();
notificationProvide(api);
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef
};
}
});
</script>

View File

@ -0,0 +1,163 @@
<template>
<div class="notification-selector">
<div class="flex-o w-100">
<fs-dict-select
class="flex-1"
:value="modelValue"
:dict="optionsDictRef"
:disabled="disabled"
:render-label="renderLabel"
:slots="selectSlots"
:allow-clear="true"
@update:value="onChange"
/>
<fs-table-select
ref="tableSelectRef"
class="flex-0"
:model-value="modelValue"
:dict="optionsDictRef"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false },
table: {
scroll: {
x: 540
}
}
}"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
@update:model-value="onChange"
@dialog-closed="doRefresh"
>
<template #default="scope">
<fs-button class="ml-5" :disabled="disabled" :size="size" type="primary" icon="ant-design:edit-outlined" @click="scope.open"></fs-button>
</template>
</fs-table-select>
</div>
</div>
</template>
<script lang="tsx" setup>
import { inject, ref, Ref, watch } from "vue";
import { createNotificationApi } from "../api";
import { message } from "ant-design-vue";
import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
import { notificationProvide } from "/@/views/certd/notification/common";
defineOptions({
name: "NotificationSelector"
});
const props = defineProps<{
modelValue?: number | string;
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
const onChange = async (value: number) => {
await emitValue(value);
};
const emit = defineEmits(["update:modelValue", "selectedChange", "change"]);
const api = createNotificationApi();
notificationProvide(api);
// const types = ref({});
// async function loadNotificationTypes() {
// const types = await api.GetDefineTypes();
// const map: any = {};
// for (const item of types) {
// map[item.type] = item;
// }
// types.value = map;
// }
// loadNotificationTypes();
const tableSelectRef = ref();
const optionsDictRef = dict({
url: "/pi/notification/options",
value: "id",
label: "name",
onReady: ({ dict }) => {
const data = [
{
id: 0,
name: "使用默认通知",
icon: "ion:notifications"
},
...dict.data
];
dict.setData(data);
}
});
const renderLabel = (option: any) => {
return <span>{option.name}</span>;
};
async function openTableSelectDialog(e: any) {
e.preventDefault();
await tableSelectRef.value.open();
await tableSelectRef.value.crudExpose.openAdd({});
}
const selectSlots = ref({
dropdownRender({ menuNode }: any) {
const res = [];
res.push(menuNode);
res.push(<a-divider style="margin: 4px 0" />);
res.push(<a-space style="padding: 4px 8px" />);
res.push(<fs-button class="w-100" type="text" icon="plus-outlined" text="新建通知渠道" onClick={openTableSelectDialog}></fs-button>);
return res;
}
});
const target: Ref<any> = ref({});
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
async function emitValue(value: any) {
target.value = optionsDictRef.dataMap[value];
if (value !== 0 && pipeline?.value && target && pipeline.value.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的通知");
return;
}
emit("change", value);
emit("update:modelValue", value);
}
watch(
() => {
return props.modelValue;
},
async (value) => {
await optionsDictRef.loadDict();
target.value = optionsDictRef.dataMap[value];
emit("selectedChange", target.value);
},
{
immediate: true
}
);
//pipeline
const pipeline = inject("pipeline", null);
async function doRefresh() {
await optionsDictRef.reloadDict();
}
</script>
<style lang="less">
.notification-selector {
width: 100%;
}
</style>

View File

@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/suite/product";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id }
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled }
});
}

View File

@ -0,0 +1,195 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
minWidth: 200,
fixed: "right"
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
title: {
title: "套餐名称",
type: "text",
editForm: {
component: {
disabled: true
}
},
search: {
show: true
},
column: {
width: 200
}
},
type: {
title: "类型",
type: "dict-select",
dict: dict({
data: [
{ label: "套餐", value: "suite" },
{ label: "加量包", value: "addon" }
]
})
},
maxDomainCount: {
title: "域名数量",
type: "number"
},
maxPipelineCount: {
title: "流水线数量",
type: "number"
},
maxDeployCount: {
title: "部署次数",
type: "number"
},
price: {
title: "单价",
type: "number"
},
originPrice: {
title: "原价",
type: "number"
},
duration: {
title: "有效时长",
type: "number"
},
intro: {
title: "说明",
type: "textarea"
},
order: {
title: "排序",
type: "number",
form: {
show: false
}
},
disabled: {
title: "禁用/启用",
type: "dict-switch",
dict: dict({
data: [
{ label: "启用", value: false, color: "success" },
{ label: "禁用", value: true, color: "error" }
]
}),
form: {
value: false
},
column: {
width: 100,
component: {
title: "点击可禁用/启用",
on: {
async click({ value, row }) {
Modal.confirm({
title: "提示",
content: `确定要${!value ? "禁用" : "启用"}吗?`,
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
}
});
}
}
}
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 160,
align: "center"
}
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true,
width: 160
}
}
}
}
};
}

View File

@ -0,0 +1,57 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
CNAME服务配置
<span class="sub">
此处配置的域名作为其他域名校验的代理当别的域名需要申请证书时通过CNAME映射到此域名上来验证所有权好处是任何域名都可以通过此方式申请证书也无需填写AccessSecret
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">CNAME功能原理及使用说明</a>
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "CnameProvider"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/suite/userSuite";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id }
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled }
});
}

View File

@ -0,0 +1,254 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
minWidth: 200,
fixed: "right"
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
domain: {
title: "CNAME域名",
type: "text",
editForm: {
component: {
disabled: true
}
},
search: {
show: true
},
form: {
component: {
placeholder: "cname.handsfree.work"
},
helper: "需要一个右边DNS提供商注册的域名也可以将其他域名的dns服务器转移到这几家来。\nCNAME域名一旦确定不可修改建议使用一级子域名",
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 200
}
},
dnsProviderType: {
title: "DNS提供商",
type: "dict-select",
search: {
show: true
},
dict: dict({
url: "pi/dnsProvider/list",
value: "key",
label: "title"
}),
form: {
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 150,
component: {
color: "auto"
}
}
},
accessId: {
title: "DNS提供商授权",
type: "dict-select",
dict: dict({
url: "/pi/access/list",
value: "id",
label: "name"
}),
form: {
component: {
name: "access-selector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderType;
})
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 150,
component: {
color: "auto"
}
}
},
isDefault: {
title: "是否默认",
type: "dict-switch",
dict: dict({
data: [
{ label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "default" }
]
}),
form: {
value: false,
rules: [{ required: true, message: "请选择是否默认" }]
},
column: {
align: "center",
width: 100
}
},
setDefault: {
title: "设置默认",
type: "text",
form: {
show: false
},
column: {
width: 100,
align: "center",
conditionalRenderDisabled: true,
cellRender: ({ row }) => {
if (row.isDefault) {
return;
}
const onClick = async () => {
Modal.confirm({
title: "提示",
content: `确定要设置为默认吗?`,
onOk: async () => {
await api.SetDefault(row.id);
await crudExpose.doRefresh();
}
});
};
return (
<a-button type={"link"} size={"small"} onClick={onClick}>
</a-button>
);
}
}
},
disabled: {
title: "禁用/启用",
type: "dict-switch",
dict: dict({
data: [
{ label: "启用", value: false, color: "success" },
{ label: "禁用", value: true, color: "error" }
]
}),
form: {
value: false
},
column: {
width: 100,
component: {
title: "点击可禁用/启用",
on: {
async click({ value, row }) {
Modal.confirm({
title: "提示",
content: `确定要${!value ? "禁用" : "启用"}吗?`,
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
}
});
}
}
}
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 160,
align: "center"
}
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true,
width: 160
}
}
}
}
};
}

View File

@ -0,0 +1,57 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
CNAME服务配置
<span class="sub">
此处配置的域名作为其他域名校验的代理当别的域名需要申请证书时通过CNAME映射到此域名上来验证所有权好处是任何域名都可以通过此方式申请证书也无需填写AccessSecret
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">CNAME功能原理及使用说明</a>
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "CnameProvider"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@ -0,0 +1,77 @@
CREATE TABLE "cd_product"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"icon" varchar(100),
"title" varchar(100),
"type" varchar(100),
"max_domain_count" integer,
"max_pipeline_count" integer,
"max_deploy_count" integer,
"deploy_count_period" varchar(100),
"site_monitor" boolean,
"expires_time" integer,
"price" integer,
"origin_price" integer,
"intro" varchar(2048),
"order" integer,
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE TABLE "cd_payment"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"type" varchar(100),
"title" varchar(100),
"setting" text,
"order" integer,
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE TABLE "cd_order"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer,
"product_id" integer,
"title" varchar(100),
"count" integer,
"price" integer,
"amount" integer,
"remark" varchar(100),
"status" varchar(100),
"pay_id" integer,
"pay_time" integer,
"pay_type" varchar(100),
"pay_no" varchar(100),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE TABLE "cd_user_suite"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer,
"product_id" integer,
"icon" varchar(100),
"title" varchar(100),
"max_domain_count" integer,
"max_pipeline_count" integer,
"max_deploy_count" integer,
"used_deploy_count" integer,
"site_monitor" boolean,
"expires_time" integer,
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);

View File

@ -208,31 +208,37 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath, tmpIcPath, tmpJksPath }) => { const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath, tmpIcPath, tmpJksPath }) => {
this.logger.info('复制到目标路径'); this.logger.info('复制到目标路径');
if (crtPath) { if (crtPath) {
crtPath = crtPath.trim();
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath); crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
this.copyFile(tmpCrtPath, crtPath); this.copyFile(tmpCrtPath, crtPath);
this.hostCrtPath = crtPath; this.hostCrtPath = crtPath;
} }
if (keyPath) { if (keyPath) {
keyPath = keyPath.trim();
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath); keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
this.copyFile(tmpKeyPath, keyPath); this.copyFile(tmpKeyPath, keyPath);
this.hostKeyPath = keyPath; this.hostKeyPath = keyPath;
} }
if (icPath) { if (icPath) {
icPath = icPath.trim();
icPath = icPath.startsWith('/') ? icPath : path.join(Constants.dataDir, icPath); icPath = icPath.startsWith('/') ? icPath : path.join(Constants.dataDir, icPath);
this.copyFile(tmpIcPath, icPath); this.copyFile(tmpIcPath, icPath);
this.hostIcPath = icPath; this.hostIcPath = icPath;
} }
if (pfxPath) { if (pfxPath) {
pfxPath = icPath.trim();
pfxPath = pfxPath.startsWith('/') ? pfxPath : path.join(Constants.dataDir, pfxPath); pfxPath = pfxPath.startsWith('/') ? pfxPath : path.join(Constants.dataDir, pfxPath);
this.copyFile(tmpPfxPath, pfxPath); this.copyFile(tmpPfxPath, pfxPath);
this.hostPfxPath = pfxPath; this.hostPfxPath = pfxPath;
} }
if (derPath) { if (derPath) {
derPath = derPath.trim();
derPath = derPath.startsWith('/') ? derPath : path.join(Constants.dataDir, derPath); derPath = derPath.startsWith('/') ? derPath : path.join(Constants.dataDir, derPath);
this.copyFile(tmpDerPath, derPath); this.copyFile(tmpDerPath, derPath);
this.hostDerPath = derPath; this.hostDerPath = derPath;
} }
if (jksPath) { if (jksPath) {
jksPath = jksPath.trim();
jksPath = jksPath.startsWith('/') ? jksPath : path.join(Constants.dataDir, jksPath); jksPath = jksPath.startsWith('/') ? jksPath : path.join(Constants.dataDir, jksPath);
this.copyFile(tmpJksPath, jksPath); this.copyFile(tmpJksPath, jksPath);
this.hostJksPath = jksPath; this.hostJksPath = jksPath;

View File

@ -239,7 +239,8 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
this.logger.info(`复制文件:${srcFile} => ${destFile}`); this.logger.info(`复制文件:${srcFile} => ${destFile}`);
} }
async execute(): Promise<void> { async execute(): Promise<void> {
const { crtPath, keyPath, cert, accessId } = this; const { cert, accessId } = this;
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath } = this;
const certReader = new CertReader(cert); const certReader = new CertReader(cert);
const handle = async (opts: CertReaderHandleContext) => { const handle = async (opts: CertReaderHandleContext) => {
@ -269,6 +270,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
const transports: any = []; const transports: any = [];
if (crtPath) { if (crtPath) {
crtPath = crtPath.trim();
transports.push({ transports.push({
localPath: tmpCrtPath, localPath: tmpCrtPath,
remotePath: crtPath, remotePath: crtPath,
@ -276,39 +278,44 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
this.logger.info(`上传证书到主机:${crtPath}`); this.logger.info(`上传证书到主机:${crtPath}`);
} }
if (keyPath) { if (keyPath) {
keyPath = keyPath.trim();
transports.push({ transports.push({
localPath: tmpKeyPath, localPath: tmpKeyPath,
remotePath: keyPath, remotePath: keyPath,
}); });
this.logger.info(`上传私钥到主机:${keyPath}`); this.logger.info(`上传私钥到主机:${keyPath}`);
} }
if (this.icPath) { if (icPath) {
icPath = icPath.trim();
transports.push({ transports.push({
localPath: tmpIcPath, localPath: tmpIcPath,
remotePath: this.icPath, remotePath: icPath,
}); });
this.logger.info(`上传中间证书到主机:${this.icPath}`); this.logger.info(`上传中间证书到主机:${icPath}`);
} }
if (this.pfxPath) { if (pfxPath) {
pfxPath = pfxPath.trim();
transports.push({ transports.push({
localPath: tmpPfxPath, localPath: tmpPfxPath,
remotePath: this.pfxPath, remotePath: pfxPath,
}); });
this.logger.info(`上传PFX证书到主机${this.pfxPath}`); this.logger.info(`上传PFX证书到主机${pfxPath}`);
} }
if (this.derPath) { if (derPath) {
derPath = derPath.trim();
transports.push({ transports.push({
localPath: tmpDerPath, localPath: tmpDerPath,
remotePath: this.derPath, remotePath: derPath,
}); });
this.logger.info(`上传DER证书到主机${this.derPath}`); this.logger.info(`上传DER证书到主机${derPath}`);
} }
if (this.jksPath) { if (this.jksPath) {
jksPath = jksPath.trim();
transports.push({ transports.push({
localPath: tmpJksPath, localPath: tmpJksPath,
remotePath: this.jksPath, remotePath: jksPath,
}); });
this.logger.info(`上传jks证书到主机${this.jksPath}`); this.logger.info(`上传jks证书到主机${jksPath}`);
} }
this.logger.info('开始上传文件到服务器'); this.logger.info('开始上传文件到服务器');
await sshClient.uploadFiles({ await sshClient.uploadFiles({
@ -320,10 +327,10 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
//输出 //输出
this.hostCrtPath = crtPath; this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath; this.hostKeyPath = keyPath;
this.hostIcPath = this.icPath; this.hostIcPath = icPath;
this.hostPfxPath = this.pfxPath; this.hostPfxPath = pfxPath;
this.hostDerPath = this.derPath; this.hostDerPath = derPath;
this.hostJksPath = this.jksPath; this.hostJksPath = jksPath;
}; };
await certReader.readCertFile({ await certReader.readCertFile({