fix: 修复左侧菜单收起时无法展开子菜单的bug

v2-dev
xiaojunnuo 2024-12-24 17:09:06 +08:00
parent 8ebf95a222
commit 005622307e
21 changed files with 367 additions and 172 deletions

View File

@ -12,7 +12,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
* 全自动部署更新证书目前支持部署到主机、阿里云、腾讯云等目前已支持40+部署插件) * 全自动部署更新证书目前支持部署到主机、阿里云、腾讯云等目前已支持40+部署插件)
* 支持通配符域名/泛域名支持多个域名打到一个证书上支持pem、pfx、der、jks等多种证书格式 * 支持通配符域名/泛域名支持多个域名打到一个证书上支持pem、pfx、der、jks等多种证书格式
* 邮件通知、webhook通知 * 邮件通知、webhook通知
* 私有化部署数据保存本地镜像由Github Actions构建过程公开透明 * 私有化部署,数据保存本地,授权信息加密存储,镜像由Github Actions构建过程公开透明
* 支持SQLitePostgreSQL、MySQL数据库 * 支持SQLitePostgreSQL、MySQL数据库

View File

@ -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 # 数据库类型

View File

@ -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数据库
--- ---

View File

@ -233,9 +233,7 @@ cert.jksjks格式证书文件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.jksjks格式证书文件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;

View File

@ -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";
} }

View File

@ -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>

View File

@ -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>
);
}; };
} }
}); });

View File

@ -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";

View File

@ -41,3 +41,8 @@
.fs-search .ant-row{ .fs-search .ant-row{
} }
.ant-modal {
max-width: calc(100% - 32px) !important ;
}

View File

@ -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"
} }
}, },

View File

@ -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 = {

View File

@ -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();

View File

@ -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>;
}
} }
} }
}, },

View File

@ -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"

View File

@ -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>

View File

@ -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;

View File

@ -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"

View File

@ -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
}
} }
} }
} }

View File

@ -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',

View File

@ -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;

View File

@ -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);