mirror of https://github.com/certd/certd
fix: 修复左侧菜单收起时无法展开子菜单的bug
parent
8ebf95a222
commit
005622307e
|
@ -12,7 +12,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
|
||||||
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等,目前已支持40+部署插件)
|
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等,目前已支持40+部署插件)
|
||||||
* 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式
|
* 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式
|
||||||
* 邮件通知、webhook通知
|
* 邮件通知、webhook通知
|
||||||
* 私有化部署,数据保存本地,镜像由Github Actions构建,过程公开透明
|
* 私有化部署,数据保存本地,授权信息加密存储,镜像由Github Actions构建,过程公开透明
|
||||||
* 支持SQLite,PostgreSQL、MySQL数据库
|
* 支持SQLite,PostgreSQL、MySQL数据库
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ services:
|
||||||
# 配置规则: certd_ + 配置项, 点号用_代替
|
# 配置规则: certd_ + 配置项, 点号用_代替
|
||||||
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false
|
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false
|
||||||
- certd_system_resetAdminPasswd=false
|
- certd_system_resetAdminPasswd=false
|
||||||
|
# 默认使用sqlite文件数据库,如果需要使用其他数据库,请设置以下环境变量
|
||||||
# #↓↓↓↓ ----------------------------- 使用postgresql数据库,需要提前创建数据库
|
# #↓↓↓↓ ----------------------------- 使用postgresql数据库,需要提前创建数据库
|
||||||
# - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录
|
# - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录
|
||||||
# - certd_typeorm_dataSource_default_type=postgres # 数据库类型
|
# - certd_typeorm_dataSource_default_type=postgres # 数据库类型
|
||||||
|
|
|
@ -30,7 +30,7 @@ features:
|
||||||
- title: 多证书格式支持
|
- title: 多证书格式支持
|
||||||
details: 支持pem、pfx、der、jks等多种证书格式,支持Google、Letsencrypt、ZeroSSL证书颁发机构
|
details: 支持pem、pfx、der、jks等多种证书格式,支持Google、Letsencrypt、ZeroSSL证书颁发机构
|
||||||
- title: 支持私有化部署
|
- title: 支持私有化部署
|
||||||
details: 保障数据安全
|
details: 授权数据加密存储,保障数据安全
|
||||||
- title: 多数据库支持
|
- title: 多数据库支持
|
||||||
details: 支持SQLite、Postgresql、MySQL数据库
|
details: 支持SQLite、Postgresql、MySQL数据库
|
||||||
---
|
---
|
||||||
|
|
|
@ -233,9 +233,7 @@ cert.jks:jks格式证书文件,java服务器使用
|
||||||
// return null;
|
// return null;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
let inputChanged = this.ctx.inputChanged;
|
let inputChanged = false;
|
||||||
if (inputChanged) {
|
|
||||||
this.logger.info("input hash 有变更,检查是否需要重新申请证书");
|
|
||||||
//判断域名有没有变更
|
//判断域名有没有变更
|
||||||
/**
|
/**
|
||||||
* "renewDays": 35,
|
* "renewDays": 35,
|
||||||
|
@ -258,12 +256,13 @@ cert.jks:jks格式证书文件,java服务器使用
|
||||||
const thisInput = JSON.stringify(pick(this, checkInputChanges));
|
const thisInput = JSON.stringify(pick(this, checkInputChanges));
|
||||||
inputChanged = oldInput !== thisInput;
|
inputChanged = oldInput !== thisInput;
|
||||||
|
|
||||||
if (inputChanged) {
|
|
||||||
this.logger.info(`旧参数:${oldInput}`);
|
this.logger.info(`旧参数:${oldInput}`);
|
||||||
this.logger.info(`新参数:${thisInput}`);
|
this.logger.info(`新参数:${thisInput}`);
|
||||||
|
if (inputChanged) {
|
||||||
this.logger.info("输入参数变更,准备申请新证书");
|
this.logger.info("输入参数变更,准备申请新证书");
|
||||||
return null;
|
return null;
|
||||||
}
|
} else {
|
||||||
|
this.logger.info("输入参数未变更,不需要更新证书");
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldCert: CertReader | undefined = undefined;
|
let oldCert: CertReader | undefined = undefined;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<FsTimeHumanize :model-value="value" :use-format-greater="1000000000000" :options="{ units: ['d'] }"></FsTimeHumanize>
|
<FsTimeHumanize :model-value="value" :use-format-greater="1000000000000000" :options="{ units: ['y', 'd'] }"></FsTimeHumanize>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
</span>
|
</span>
|
||||||
|
@ -35,12 +35,14 @@ const color = computed(() => {
|
||||||
if (props.value == null) {
|
if (props.value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (props.value === -1) {
|
//距离今天多少天
|
||||||
|
const days = dayjs(props.value).diff(dayjs(), "day");
|
||||||
|
if (props.value === -1 || days > 365) {
|
||||||
return "green";
|
return "green";
|
||||||
}
|
}
|
||||||
|
|
||||||
//小于3天 红色
|
//小于3天 红色
|
||||||
if (dayjs().add(3, "day").valueOf() > props.value) {
|
if (days <= 6) {
|
||||||
return "red";
|
return "red";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<a-menu v-model:open-keys="openKeys" v-model:selected-keys="selectedKeys" class="fs-menu" mode="inline" theme="light" :items="items" @click="onClick" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="tsx" setup>
|
||||||
|
import { ref, watch, defineOptions } from "vue";
|
||||||
|
import { routerUtils } from "/@/utils/util.router";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { utils } from "@fast-crud/fast-crud";
|
||||||
|
import * as _ from "lodash-es";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "FsMenu"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
menus: any[];
|
||||||
|
expandSelected: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const items = ref([]);
|
||||||
|
|
||||||
|
function buildItemMenus(menus: any) {
|
||||||
|
if (menus == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list: any = [];
|
||||||
|
for (const sub of menus) {
|
||||||
|
const item: any = {
|
||||||
|
key: sub.path,
|
||||||
|
label: sub.title,
|
||||||
|
title: sub.title,
|
||||||
|
icon: () => {
|
||||||
|
return <fsIcon icon={sub.icon ?? sub.meta?.icon} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
list.push(item);
|
||||||
|
if (sub.children && sub.children.length > 0) {
|
||||||
|
item.children = buildItemMenus(sub.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.menus,
|
||||||
|
(menus) => {
|
||||||
|
items.value = buildItemMenus(menus);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onClick(item: any) {
|
||||||
|
await routerUtils.open(item.key);
|
||||||
|
}
|
||||||
|
const route = useRoute();
|
||||||
|
const selectedKeys = ref([]);
|
||||||
|
const openKeys = ref([]);
|
||||||
|
|
||||||
|
function openSelectedParents(fullPath: any) {
|
||||||
|
if (!props.expandSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.menus == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keys: any = [];
|
||||||
|
let changed = false;
|
||||||
|
utils.deepdash.forEachDeep(props.menus, (value: any, key: any, parent: any, context: any) => {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.path === fullPath) {
|
||||||
|
_.forEach(context.parents, (item) => {
|
||||||
|
if (item.value instanceof Array) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keys.push(item.value.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (keys.length > 0) {
|
||||||
|
for (const key of keys) {
|
||||||
|
if (openKeys.value.indexOf(key) === -1) {
|
||||||
|
openKeys.value.push(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return route.fullPath;
|
||||||
|
},
|
||||||
|
(path) => {
|
||||||
|
// path = route.fullPath;
|
||||||
|
selectedKeys.value = [path];
|
||||||
|
const changed = openSelectedParents(path);
|
||||||
|
if (changed) {
|
||||||
|
// onOpenChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.fs-menu {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
.fs-icon {
|
||||||
|
font-size: 16px !important;
|
||||||
|
min-width: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,60 +6,8 @@ import "./index.less";
|
||||||
import { utils } from "@fast-crud/fast-crud";
|
import { utils } from "@fast-crud/fast-crud";
|
||||||
import { routerUtils } from "/@/utils/util.router";
|
import { routerUtils } from "/@/utils/util.router";
|
||||||
|
|
||||||
function useBetterScroll(enabled = true) {
|
defineOptions()
|
||||||
const bsRef = ref(null);
|
|
||||||
const asideMenuRef = ref();
|
|
||||||
|
|
||||||
let onOpenChange = () => {};
|
|
||||||
if (enabled) {
|
|
||||||
function bsInit() {
|
|
||||||
if (asideMenuRef.value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bsRef.value = new BScroll(asideMenuRef.value, {
|
|
||||||
mouseWheel: true,
|
|
||||||
click: true,
|
|
||||||
momentum: false,
|
|
||||||
// 如果你愿意可以打开显示滚动条
|
|
||||||
scrollbar: {
|
|
||||||
fade: true,
|
|
||||||
interactive: false
|
|
||||||
},
|
|
||||||
bounce: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bsDestroy() {
|
|
||||||
if (bsRef.value != null && bsRef.value.destroy) {
|
|
||||||
try {
|
|
||||||
bsRef.value.destroy();
|
|
||||||
} catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
} finally {
|
|
||||||
bsRef.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
bsInit();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
bsDestroy();
|
|
||||||
});
|
|
||||||
onOpenChange = async () => {
|
|
||||||
console.log("onOpenChange");
|
|
||||||
setTimeout(() => {
|
|
||||||
bsRef.value?.refresh();
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
onOpenChange,
|
|
||||||
asideMenuRef
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "FsMenu",
|
name: "FsMenu",
|
||||||
inheritAttrs: true,
|
inheritAttrs: true,
|
||||||
|
@ -75,6 +23,31 @@ export default defineComponent({
|
||||||
await routerUtils.open(item.key);
|
await routerUtils.open(item.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const items = ref([]);
|
||||||
|
|
||||||
|
function buildItemMenus(menus: any) {
|
||||||
|
if (menus == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list: any = [];
|
||||||
|
for (const sub of menus) {
|
||||||
|
const item: any = {
|
||||||
|
key: sub.path,
|
||||||
|
label: sub.title,
|
||||||
|
title: sub.title,
|
||||||
|
icon: () => {
|
||||||
|
return <fsIcon icon={sub.icon ?? sub.meta?.icon} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
list.push(item);
|
||||||
|
if (sub.children && sub.children.length > 0) {
|
||||||
|
item.children = buildItemMenus(sub.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
items.value = buildItemMenus(props.menus);
|
||||||
|
console.log("items", items.value);
|
||||||
const fsIcon = resolveComponent("FsIcon");
|
const fsIcon = resolveComponent("FsIcon");
|
||||||
|
|
||||||
const buildMenus = (children: any) => {
|
const buildMenus = (children: any) => {
|
||||||
|
@ -114,7 +87,7 @@ export default defineComponent({
|
||||||
open(sub.path);
|
open(sub.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots.push(<a-sub-menu key={sub.path} v-slots={subSlots} onTitleClick={onTitleClick} />);
|
slots.push(<a-sub-menu key={sub.path} v-slots={subSlots} />);
|
||||||
} else {
|
} else {
|
||||||
slots.push(
|
slots.push(
|
||||||
<a-menu-item key={sub.path} title={sub.title}>
|
<a-menu-item key={sub.path} title={sub.title}>
|
||||||
|
@ -132,6 +105,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
const selectedKeys = ref([]);
|
const selectedKeys = ref([]);
|
||||||
const openKeys = ref([]);
|
const openKeys = ref([]);
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -168,7 +142,7 @@ export default defineComponent({
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll as any);
|
// const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll as any);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => {
|
() => {
|
||||||
|
@ -179,7 +153,7 @@ export default defineComponent({
|
||||||
selectedKeys.value = [path];
|
selectedKeys.value = [path];
|
||||||
const changed = openSelectedParents(path);
|
const changed = openSelectedParents(path);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
onOpenChange();
|
// onOpenChange();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -191,22 +165,19 @@ export default defineComponent({
|
||||||
<a-menu
|
<a-menu
|
||||||
mode={"inline"}
|
mode={"inline"}
|
||||||
theme={"light"}
|
theme={"light"}
|
||||||
v-slots={slots}
|
// v-slots={slots}
|
||||||
onClick={onSelect}
|
// onClick={onSelect}
|
||||||
onOpenChange={onOpenChange}
|
// onOpenChange={onOpenChange}
|
||||||
v-models={[
|
v-models={[
|
||||||
[openKeys.value, "openKeys"],
|
[openKeys.value, "openKeys"],
|
||||||
[selectedKeys.value, "selectedKeys"]
|
[selectedKeys.value, "selectedKeys"]
|
||||||
]}
|
]}
|
||||||
{...ctx.attrs}
|
items={items.value}
|
||||||
|
inlineCollapsed={!props.expandSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll };
|
const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll };
|
||||||
return (
|
return <div>{menu}</div>;
|
||||||
<div ref={asideMenuRef} class={classNames}>
|
|
||||||
{menu}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -106,7 +106,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onErrorCaptured, onMounted, ref } from "vue";
|
import { computed, onErrorCaptured, onMounted, ref } from "vue";
|
||||||
import FsMenu from "./components/menu/index.jsx";
|
import FsMenu from "./components/menu/index.vue";
|
||||||
import FsLocale from "./components/locale/index.vue";
|
import FsLocale from "./components/locale/index.vue";
|
||||||
import FsUserInfo from "./components/user-info/index.vue";
|
import FsUserInfo from "./components/user-info/index.vue";
|
||||||
import FsTabs from "./components/tabs/index.vue";
|
import FsTabs from "./components/tabs/index.vue";
|
||||||
|
|
|
@ -41,3 +41,8 @@
|
||||||
.fs-search .ant-row{
|
.fs-search .ant-row{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ant-modal {
|
||||||
|
max-width: calc(100% - 32px) !important ;
|
||||||
|
}
|
|
@ -501,7 +501,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
sorter: true,
|
sorter: true,
|
||||||
width: 80,
|
width: 120,
|
||||||
align: "center"
|
align: "center"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,6 +57,12 @@ const StatusEnum: StatusEnumType = {
|
||||||
label: "禁用",
|
label: "禁用",
|
||||||
color: "gray",
|
color: "gray",
|
||||||
icon: "ant-design:stop-outlined"
|
icon: "ant-design:stop-outlined"
|
||||||
|
},
|
||||||
|
no_deploy_count: {
|
||||||
|
value: "no_deploy_count",
|
||||||
|
label: "次数不足",
|
||||||
|
color: "gray",
|
||||||
|
icon: "ant-design:stop-outlined"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const statusUtil = {
|
export const statusUtil = {
|
||||||
|
|
|
@ -14,14 +14,10 @@
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<h3>套餐</h3>
|
|
||||||
<a-row :gutter="8" class="mt-10">
|
<a-row :gutter="8" class="mt-10">
|
||||||
<a-col v-for="item of suites" :key="item.id" class="mb-10 suite-card-col">
|
<a-col v-for="item of suites" :key="item.id" class="mb-10 suite-card-col">
|
||||||
<product-info :product="item" @order="doOrder" />
|
<product-info :product="item" @order="doOrder" />
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
|
||||||
<h3>加量包</h3>
|
|
||||||
<a-row :gutter="8" class="mt-10">
|
|
||||||
<a-col v-for="item of addons" :key="item.id" class="mb-10 suite-card-col">
|
<a-col v-for="item of addons" :key="item.id" class="mb-10 suite-card-col">
|
||||||
<product-info :product="item" @order="doOrder" />
|
<product-info :product="item" @order="doOrder" />
|
||||||
</a-col>
|
</a-col>
|
||||||
|
@ -44,7 +40,7 @@ const addons = ref([]);
|
||||||
async function loadProducts() {
|
async function loadProducts() {
|
||||||
const list = await api.ProductList();
|
const list = await api.ProductList();
|
||||||
suites.value = list.filter((x: any) => x.type === "suite");
|
suites.value = list.filter((x: any) => x.type === "suite");
|
||||||
addons.value = list.filter((x: any) => x.type === "addone");
|
addons.value = list.filter((x: any) => x.type === "addon");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadProducts();
|
loadProducts();
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
|
import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
|
||||||
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
|
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
@ -207,7 +208,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
component: {
|
component: {
|
||||||
name: SuiteValue,
|
name: SuiteValue,
|
||||||
vModel: "modelValue",
|
vModel: "modelValue",
|
||||||
unit: "次"
|
unit: "次",
|
||||||
|
used: compute(({ row }) => {
|
||||||
|
return row.deployCountUsed;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
align: "center"
|
align: "center"
|
||||||
}
|
}
|
||||||
|
@ -254,38 +258,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
column: {
|
column: {
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
|
component: {
|
||||||
|
name: UserSuiteStatus,
|
||||||
|
userSuite: compute(({ row }) => {
|
||||||
|
return row;
|
||||||
|
}),
|
||||||
|
currentSuite: context.detail
|
||||||
|
},
|
||||||
conditionalRender: {
|
conditionalRender: {
|
||||||
match() {
|
match() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
cellRender({ row }) {
|
|
||||||
if (row.activeTime == null) {
|
|
||||||
return <a-tag color="blue">未使用</a-tag>;
|
|
||||||
}
|
|
||||||
const now = dayjs().valueOf();
|
|
||||||
//已过期
|
|
||||||
const isExpired = row.expiresTime != -1 && now > row.expiresTime;
|
|
||||||
if (isExpired) {
|
|
||||||
return <a-tag color="error">已过期</a-tag>;
|
|
||||||
}
|
|
||||||
//如果在激活时间之前
|
|
||||||
if (now < row.activeTime) {
|
|
||||||
return <a-tag color="blue">待生效</a-tag>;
|
|
||||||
}
|
|
||||||
|
|
||||||
//是否是当前套餐
|
|
||||||
const suites = context.detail.value.suites;
|
|
||||||
if (suites && suites.length > 0) {
|
|
||||||
const suite = suites[0];
|
|
||||||
if (suite.productType === "suite" && suite.id === row.id) {
|
|
||||||
return <a-tag color="success">当前套餐</a-tag>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 是否在激活时间和过期时间之间
|
|
||||||
if (now > row.activeTime && (row.expiresTime == -1 || now < row.expiresTime)) {
|
|
||||||
return <a-tag color="success">生效中</a-tag>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<fs-page>
|
<fs-page>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title flex-baseline">
|
||||||
我的套餐
|
我的套餐
|
||||||
<span class="sub">我的所有套餐</span>
|
<div class="sub">
|
||||||
|
<div class="flex-o">当前套餐:<suite-card></suite-card></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
<template #actionbar-right> </template>
|
||||||
|
</fs-crud>
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,6 +19,7 @@ import { onActivated, onMounted, ref } from "vue";
|
||||||
import { useFs } from "@fast-crud/fast-crud";
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
import api, { SuiteDetail } from "/@/views/certd/suite/mine/api";
|
import api, { SuiteDetail } from "/@/views/certd/suite/mine/api";
|
||||||
|
import SuiteCard from "/@/views/framework/home/dashboard/suite-card.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "MySuites"
|
name: "MySuites"
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<a-tag :color="binding.color">{{ binding.text }}</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "UserSuiteStatus"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userSuite: any;
|
||||||
|
currentSuite?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const binding = computed(() => {
|
||||||
|
const userSuite = props.userSuite;
|
||||||
|
if (!userSuite) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (userSuite.activeTime == null) {
|
||||||
|
return { color: "blue", text: "未使用" };
|
||||||
|
}
|
||||||
|
const now = dayjs().valueOf();
|
||||||
|
//已过期
|
||||||
|
const isExpired = userSuite.expiresTime != -1 && now > userSuite.expiresTime;
|
||||||
|
if (isExpired) {
|
||||||
|
return { color: "error", text: "已过期" };
|
||||||
|
}
|
||||||
|
//如果在激活时间之前
|
||||||
|
if (now < userSuite.activeTime) {
|
||||||
|
return { color: "blue", text: "待生效" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSuite.isEmpty) {
|
||||||
|
return { color: "gray", text: "已用完" };
|
||||||
|
}
|
||||||
|
|
||||||
|
//是否是当前套餐
|
||||||
|
if (props.currentSuite && props.currentSuite.productType === "suite" && props.currentSuite.id === userSuite.id) {
|
||||||
|
return { color: "success", text: "当前套餐" };
|
||||||
|
}
|
||||||
|
// 是否在激活时间和过期时间之间
|
||||||
|
if (now > userSuite.activeTime && (userSuite.expiresTime == -1 || now < userSuite.expiresTime)) {
|
||||||
|
if (userSuite.productType === "suite") {
|
||||||
|
return { color: "success", text: "有效期内" };
|
||||||
|
}
|
||||||
|
return { color: "success", text: "生效中" };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -16,7 +16,13 @@
|
||||||
<suite-value :model-value="product.content.maxDomainCount" unit="个" />
|
<suite-value :model-value="product.content.maxDomainCount" unit="个" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-between mt-5">
|
<div class="flex-between mt-5">
|
||||||
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 部署次数:</div>
|
<div class="flex-o">
|
||||||
|
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
|
||||||
|
部署次数:
|
||||||
|
<a-tooltip title="只有运行成功才会扣除部署次数">
|
||||||
|
<fs-icon class="font-size-16 ml-5" icon="mingcute:question-line"></fs-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
<suite-value :model-value="product.content.maxDeployCount" unit="次" />
|
<suite-value :model-value="product.content.maxDeployCount" unit="次" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-between mt-5">
|
<div class="flex-between mt-5">
|
||||||
|
@ -43,7 +49,7 @@
|
||||||
<div class="price flex-between mt-5">
|
<div class="price flex-between mt-5">
|
||||||
<div class="flex-o">价格</div>
|
<div class="flex-o">价格</div>
|
||||||
<div class="flex-o price-text">
|
<div class="flex-o price-text">
|
||||||
<price-input style="font-size: 18px; color: red" :model-value="selected?.price" :edit="false" />
|
<price-input style="color: red" :font-size="20" :model-value="selected?.price" :edit="false" />
|
||||||
<span class="ml-5" style="font-size: 12px"> / {{ durationDict.dataMap[selected.duration]?.label }}</span>
|
<span class="ml-5" style="font-size: 12px"> / {{ durationDict.dataMap[selected.duration]?.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,7 +64,7 @@ import { durationDict } from "/@/views/certd/suite/api";
|
||||||
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { dict } from "@fast-crud/fast-crud";
|
import { dict, FsIcon } from "@fast-crud/fast-crud";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
product: any;
|
product: any;
|
||||||
|
|
|
@ -9,11 +9,17 @@
|
||||||
<suite-value :model-value="detail.pipelineCount.max" :used="detail.pipelineCount.used" unit="条" />
|
<suite-value :model-value="detail.pipelineCount.max" :used="detail.pipelineCount.used" unit="条" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-between mt-5">
|
<div class="flex-between mt-5">
|
||||||
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />域名数量:</div>
|
<div class="flex-o">
|
||||||
|
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
|
||||||
|
域名数量:
|
||||||
|
</div>
|
||||||
<suite-value :model-value="detail.domainCount.max" :used="detail.domainCount.used" unit="个" />
|
<suite-value :model-value="detail.domainCount.max" :used="detail.domainCount.used" unit="个" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-between mt-5">
|
<div class="flex-between mt-5">
|
||||||
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 部署次数:</div>
|
<div class="flex-o">
|
||||||
|
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
|
||||||
|
部署次数:
|
||||||
|
</div>
|
||||||
<suite-value :model-value="detail.deployCount.max" :used="detail.deployCount.used" unit="次" />
|
<suite-value :model-value="detail.deployCount.max" :used="detail.deployCount.used" unit="次" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-between mt-5">
|
<div class="flex-between mt-5">
|
||||||
|
@ -41,6 +47,7 @@ import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import ExpiresTimeText from "/@/components/expires-time-text.vue";
|
import ExpiresTimeText from "/@/components/expires-time-text.vue";
|
||||||
import api, { SuiteDetail } from "/@/views/certd/suite/mine/api";
|
import api, { SuiteDetail } from "/@/views/certd/suite/mine/api";
|
||||||
|
import { FsIcon } from "@fast-crud/fast-crud";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "SuiteCard"
|
name: "SuiteCard"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
import { pipelineGroupApi } from "./api";
|
import { pipelineGroupApi } from "./api";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
|
import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
|
||||||
|
@ -6,6 +6,7 @@ import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
|
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
|
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
|
||||||
|
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
|
||||||
|
|
||||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const api = pipelineGroupApi;
|
const api = pipelineGroupApi;
|
||||||
|
@ -68,6 +69,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toolbar: { show: false },
|
||||||
rowHandle: {
|
rowHandle: {
|
||||||
width: 200,
|
width: 200,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
|
@ -234,7 +236,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
component: {
|
component: {
|
||||||
name: SuiteValue,
|
name: SuiteValue,
|
||||||
vModel: "modelValue",
|
vModel: "modelValue",
|
||||||
unit: "次"
|
unit: "次",
|
||||||
|
used: compute(({ row }) => {
|
||||||
|
return row.deployCountUsed;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
align: "center"
|
align: "center"
|
||||||
}
|
}
|
||||||
|
@ -281,29 +286,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
column: {
|
column: {
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
|
component: {
|
||||||
|
name: UserSuiteStatus,
|
||||||
|
userSuite: compute(({ row }) => {
|
||||||
|
return row;
|
||||||
|
})
|
||||||
|
},
|
||||||
conditionalRender: {
|
conditionalRender: {
|
||||||
match() {
|
match() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
cellRender({ row }) {
|
|
||||||
if (row.activeTime == null) {
|
|
||||||
return <a-tag color="blue">未使用</a-tag>;
|
|
||||||
}
|
|
||||||
const now = dayjs().valueOf();
|
|
||||||
//已过期
|
|
||||||
const isExpired = row.expiresTime != -1 && now > row.expiresTime;
|
|
||||||
if (isExpired) {
|
|
||||||
return <a-tag color="error">已过期</a-tag>;
|
|
||||||
}
|
|
||||||
//如果在激活时间之前
|
|
||||||
if (now < row.activeTime) {
|
|
||||||
return <a-tag color="blue">待生效</a-tag>;
|
|
||||||
}
|
|
||||||
// 是否在激活时间和过期时间之间
|
|
||||||
if (now > row.activeTime && (row.expiresTime == -1 || now < row.expiresTime)) {
|
|
||||||
return <a-tag color="success">生效中</a-tag>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -342,6 +334,29 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center"
|
align: "center"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ const development = {
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: './data/db.sqlite',
|
database: './data/db.sqlite',
|
||||||
synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true
|
synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true
|
||||||
logging: true,
|
logging: false,
|
||||||
highlightSql: false,
|
highlightSql: false,
|
||||||
|
|
||||||
// 配置实体模型 或者 entities: '/entity',
|
// 配置实体模型 或者 entities: '/entity',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
import { Provide } from '@midwayjs/core';
|
||||||
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
|
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
|
||||||
import { logger } from '@certd/basic';
|
import { logger } from '@certd/basic';
|
||||||
import { Result } from '@certd/lib-server';
|
import { BaseException, Result } from '@certd/lib-server';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class GlobalExceptionMiddleware implements IWebMiddleware {
|
export class GlobalExceptionMiddleware implements IWebMiddleware {
|
||||||
|
@ -14,7 +14,11 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
|
||||||
await next();
|
await next();
|
||||||
logger.info('请求完成:', url, Date.now() - startTime + 'ms');
|
logger.info('请求完成:', url, Date.now() - startTime + 'ms');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
|
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err.message);
|
||||||
|
if (!(err instanceof BaseException)) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
if (err.code == null || typeof err.code !== 'number') {
|
if (err.code == null || typeof err.code !== 'number') {
|
||||||
err.code = 1;
|
err.code = 1;
|
||||||
|
|
|
@ -148,7 +148,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
return new PipelineDetail(pipeline);
|
return new PipelineDetail(pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(bean: PipelineEntity) {
|
async update(bean: Partial<PipelineEntity>) {
|
||||||
//更新非trigger部分
|
//更新非trigger部分
|
||||||
await super.update(bean);
|
await super.update(bean);
|
||||||
}
|
}
|
||||||
|
@ -304,13 +304,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async trigger(id: any, stepId?: string) {
|
async trigger(id: any, stepId?: string) {
|
||||||
|
const entity: PipelineEntity = await this.info(id);
|
||||||
|
if (isComm()) {
|
||||||
|
await this.checkHasDeployCount(id, entity.userId);
|
||||||
|
}
|
||||||
this.cron.register({
|
this.cron.register({
|
||||||
name: `pipeline.${id}.trigger.once`,
|
name: `pipeline.${id}.trigger.once`,
|
||||||
cron: null,
|
cron: null,
|
||||||
job: async () => {
|
job: async () => {
|
||||||
logger.info('用户手动启动job');
|
logger.info('用户手动启动job');
|
||||||
try {
|
try {
|
||||||
await this.run(id, null, stepId);
|
await this.doRun(entity, null, stepId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('手动job执行失败:', e);
|
logger.error('手动job执行失败:', e);
|
||||||
}
|
}
|
||||||
|
@ -318,6 +322,21 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkHasDeployCount(pipelineId: number, userId: number) {
|
||||||
|
try {
|
||||||
|
return await this.userSuiteService.checkHasDeployCount(userId);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NeedSuiteException) {
|
||||||
|
logger.error(e.message);
|
||||||
|
await this.update({
|
||||||
|
id: pipelineId,
|
||||||
|
status: 'no_deploy_count',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async delete(id: any) {
|
async delete(id: any) {
|
||||||
await this.clearTriggers(id);
|
await this.clearTriggers(id);
|
||||||
//TODO 删除storage
|
//TODO 删除storage
|
||||||
|
@ -390,10 +409,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
|
|
||||||
async run(id: number, triggerId: string, stepId?: string) {
|
async run(id: number, triggerId: string, stepId?: string) {
|
||||||
const entity: PipelineEntity = await this.info(id);
|
const entity: PipelineEntity = await this.info(id);
|
||||||
|
await this.doRun(entity, triggerId, stepId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
|
||||||
|
const id = entity.id;
|
||||||
let suite: UserSuiteEntity = null;
|
let suite: UserSuiteEntity = null;
|
||||||
if (isComm()) {
|
if (isComm()) {
|
||||||
suite = await this.userSuiteService.checkHasDeployCount(entity.userId);
|
suite = await this.checkHasDeployCount(id, entity.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipeline = JSON.parse(entity.content);
|
const pipeline = JSON.parse(entity.content);
|
||||||
|
|
Loading…
Reference in New Issue