perf: 站点证书监控增加导出和分组功能

v2-dev
xiaojunnuo 2025-10-21 22:28:02 +08:00
parent e578c52fdf
commit 2ed12c429e
13 changed files with 619 additions and 10 deletions

View File

@ -227,6 +227,7 @@ export default {
},
notificationDefault: "Use Default Notification",
monitor: {
remark: "Remark",
title: "Site Certificate Monitoring",
description: "Check website certificates' expiration at 0:00 daily; reminders sent 10 days before expiration (using default notification channel);",
settingLink: "Site Monitoring Settings",
@ -248,6 +249,7 @@ export default {
certDomains: "Certificate Domains",
certProvider: "Issuer",
certStatus: "Certificate Status",
error: "Error",
status: {
ok: "Valid",
expired: "Expired",

View File

@ -232,6 +232,7 @@ export default {
},
notificationDefault: "使用默认通知",
monitor: {
remark: "备注",
title: "站点证书监控",
description: "每天0点检查网站证书的过期时间到期前10天时将发出提醒使用默认通知渠道;",
settingLink: "站点监控设置",
@ -253,6 +254,7 @@ export default {
certDomains: "证书域名",
certProvider: "颁发机构",
certStatus: "证书状态",
error: "错误信息",
status: {
ok: "正常",
expired: "过期",

View File

@ -0,0 +1,64 @@
import { dict } from "@fast-crud/fast-crud";
import { request } from "/src/api/service";
export function createApi() {
const apiPrefix = "/basic/group";
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 ListAll(type: string) {
return await request({
url: apiPrefix + "/all",
method: "post",
params: { type },
});
},
};
}
export const pipelineGroupApi = createApi();
export function createGroupDictRef(type: string) {
return dict({
url: "/basic/group/all?type=" + type,
value: "id",
label: "name",
});
}

View File

@ -0,0 +1,142 @@
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api";
import { ref } from "vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = pipelineGroupApi;
const typeRef = ref(context.type);
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;
form.type = typeRef.value;
const res = await api.AddObj(form);
return res;
};
return {
crudOptions: {
settings: {
plugins: {
mobile: {
props: {
rowHandle: {
width: 160,
},
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
search: {
initialForm: {
type: typeRef.value,
},
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px",
},
},
col: {
span: 22,
},
wrapper: {
width: 600,
},
},
rowHandle: {
width: 200,
group: {
editable: {
edit: {
text: t("certd.edit"),
order: -1,
type: "primary",
click({ row, index }) {
crudExpose.openEdit({
index,
row,
});
},
},
},
},
},
table: {
editable: {
enabled: true,
mode: "cell",
exclusive: true,
//排他式激活效果,将其他行的编辑状态触发保存
exclusiveEffect: "save", //自动保存其他行编辑状态cancel = 自动关闭其他行编辑状态
async updateCell(opts) {
const { row, key, value } = opts;
//如果是添加,需要返回{[rowKey]:xxx},比如:{id:2}
return await api.UpdateObj({ id: row.id, [key]: value });
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: true,
},
column: {
width: 100,
editable: {
disabled: true,
},
},
form: {
show: false,
},
},
name: {
title: t("certd.groupName"),
search: {
show: true,
},
type: "text",
form: {
rules: [
{
required: true,
message: t("certd.enterGroupName"),
},
],
},
column: {
width: 400,
},
},
},
},
};
}

View File

@ -0,0 +1,58 @@
<template>
<div class="pi-group-selector flex full-w">
<div class="flex-1">
<fs-dict-select :value="modelValue" :dict="groupDictRef" :allow-clear="true" @update:value="doUpdate"></fs-dict-select>
</div>
<fs-table-select
class="flex-0"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false, initialForm: { type: props.type } },
table: {
scroll: {
x: 540,
},
},
}"
:model-value="modelValue"
:dict="groupDictRef"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
@update:model-value="doUpdate"
@dialog-closed="doRefresh"
>
<template #default="scope">
<fs-button class="ml-5" type="primary" icon="ant-design:edit-outlined" @click="scope.open({ context: { type: props.type } })"></fs-button>
</template>
</fs-table-select>
</div>
</template>
<script setup lang="ts">
import { createGroupDictRef } from "./api";
import createCrudOptions from "./crud";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
const props = defineProps<{
modelValue?: number;
type: string;
}>();
defineOptions({
name: "GroupSelector",
});
const groupDictRef = createGroupDictRef(props.type);
const emit = defineEmits(["refresh", "update:modelValue", "change"]);
function doRefresh() {
emit("refresh");
groupDictRef.reloadDict();
}
function doUpdate(value: any) {
emit("update:modelValue", value);
}
</script>

View File

@ -0,0 +1,37 @@
<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";
export default defineComponent({
name: "BasicGroupManager",
setup() {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef,
};
},
});
</script>

View File

@ -1,6 +1,6 @@
// @ts-ignore
import { useI18n } from "/src/locales";
import { AddReq, ColumnCompositionProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { AddReq, ColumnCompositionProps, ColumnProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DataFormatterContext, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import * as settingApi from "./setting/api";
import dayjs from "dayjs";
@ -11,7 +11,8 @@ import { mitter } from "/@/utils/util.mitt";
import { useSiteIpMonitor } from "./ip/use";
import { useSiteImport } from "/@/views/certd/monitor/site/use";
import { ref } from "vue";
import GroupSelector from "../../basic/group/group-selector.vue";
import { createGroupDictRef } from "../../basic/group/api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = siteInfoApi;
@ -91,6 +92,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
});
}
const GroupTypeSite = "site";
const groupDictRef = createGroupDictRef(GroupTypeSite);
function getDefaultGroupId() {
const searchFrom = crudExpose.getSearchValidatedFormData();
if (searchFrom.groupId) {
return searchFrom.groupId;
}
}
return {
id: "siteMonitorCrud",
crudOptions: {
@ -100,6 +111,53 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
tabs: {
name: "groupId",
show: true,
},
toolbar: {
buttons: {
export: {
show: true,
},
},
export: {
dataFrom: "search",
columnFilter: (col: ColumnProps) => {
//列过滤器返回true则导出该列
//例如: 只导出show=true的列
return col.show === true;
},
dataFormatter: (opts: DataFormatterContext) => {
//例如 格式化日期
const { row, originalRow, col, exportCol } = opts;
const key = col.key;
const element = originalRow[key];
if (key.includes("Time") && element) {
row[key] = dayjs(element).format("YYYY-MM-DD HH:mm:ss");
}
if (col.width) {
exportCol.width = col.width / 10;
}
if (col.key === "certInfo" && originalRow?.certProvider) {
row[key] = originalRow?.certProvider + " " + originalRow?.certDomains;
}
//参数说明
// DataFormatterContext = {row: any,originalRow: any, key: string, col: ColumnProps, exportCol:ExportColumn}
// row = 当前行数据
// originalRow = 当前行原始数据
// key = 当前列的key
// col = 当前列的配置
// exportCol = 当前列的导出配置
},
},
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
@ -158,7 +216,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
}
await crudExpose.openAdd({});
const defaultGroupId = getDefaultGroupId();
await crudExpose.openAdd({
row: { groupId: defaultGroupId },
});
},
},
//导入按钮
@ -167,7 +228,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
text: t("certd.monitor.bulkImport"),
type: "primary",
async click() {
const defaultGroupId = getDefaultGroupId();
openSiteImportDialog({
defaultGroupId,
afterSubmit() {
crudExpose.doRefresh();
},
@ -219,10 +282,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
tabs: {
name: "disabled",
show: true,
},
// tabs: {
// name: "disabled",
// show: true,
// },
columns: {
id: {
title: "ID",
@ -403,6 +466,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
sorter: true,
width: 155,
show: false,
},
},
certExpiresTime: {
@ -451,6 +515,46 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
groupId: {
title: t("certd.fields.group"),
type: "dict-select",
search: {
show: true,
},
dict: groupDictRef,
form: {
component: {
name: GroupSelector,
vModel: "modelValue",
type: GroupTypeSite,
onRefresh() {
groupDictRef.reloadDict();
},
},
},
column: {
width: 130,
align: "center",
component: {
color: "auto",
},
sorter: true,
},
},
remark: {
title: t("certd.monitor.remark"),
search: {
show: false,
},
type: "text",
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
},
lastCheckTime: {
title: t("certd.monitor.lastCheckTime"),
search: {
@ -618,6 +722,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
},
error: {
title: t("certd.monitor.error"),
search: {
show: false,
},
type: "text",
form: { show: false },
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
},
},
},
};

View File

@ -1,13 +1,13 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import { useI18n } from "/src/locales";
import GroupSelector from "../../basic/group/group-selector.vue";
export function useSiteImport() {
const { t } = useI18n();
const { openCrudFormDialog } = useFormWrapper();
async function openSiteImportDialog(opts: { afterSubmit: any }) {
const { afterSubmit } = opts;
async function openSiteImportDialog(opts: { afterSubmit: any; defaultGroupId?: number }) {
const { afterSubmit, defaultGroupId } = opts;
await openCrudFormDialog<any>({
crudOptions: {
columns: {
@ -26,6 +26,21 @@ export function useSiteImport() {
},
},
},
groupId: {
type: "select",
title: t("certd.fields.group"),
form: {
value: defaultGroupId,
component: {
name: GroupSelector,
vModel: "modelValue",
type: "site",
},
col: {
span: 24,
},
},
},
},
form: {

View File

@ -0,0 +1,17 @@
ALTER TABLE cd_site_info ADD COLUMN "remark" varchar(512);
CREATE TABLE "cd_group"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"name" varchar(100) NOT NULL,
"icon" varchar(100),
"favorite" boolean NOT NULL DEFAULT (false),
"type" varchar(512),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
--
ALTER TABLE cd_site_info ADD COLUMN "group_id" integer;

View File

@ -0,0 +1,78 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { GroupService } from '../../../modules/basic/service/group-service.js';
/**
*
*/
@Provide()
@Controller('/api/basic/group')
export class GroupController extends CrudController<GroupService> {
@Inject()
service: GroupService;
@Inject()
authService: AuthService;
getService(): GroupService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
return await super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
return await super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return await super.delete(id);
}
@Post('/all', { summary: Constants.per.authOnly })
async all(@Query('type') type: string) {
const list: any = await this.service.find({
where: {
userId: this.getUserId(),
type,
},
});
return this.ok(list);
}
}

View File

@ -0,0 +1,38 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export const GROUP_TYPE_SITE = 'site';
@Entity('cd_group')
export class GroupEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ name: 'name', comment: '分组名称' })
name: string;
@Column({ name: 'icon', comment: '图标' })
icon: string;
@Column({ name: 'favorite', comment: '收藏' })
favorite: boolean;
@Column({ name: 'type', comment: '类型', length: 512 })
type: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}

View File

@ -0,0 +1,31 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { BaseService } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { merge } from 'lodash-es';
import { GroupEntity } from '../entity/group.js';
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class GroupService extends BaseService<GroupEntity> {
@InjectEntityModel(GroupEntity)
repository: Repository<GroupEntity>;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(bean: any) {
if (!bean.type) {
throw new Error('type is required');
}
bean = merge(
{
favorite: false,
},
bean
);
return await this.repository.save(bean);
}
}

View File

@ -56,6 +56,12 @@ export class SiteInfoEntity {
@Column({ name: 'disabled', comment: '禁用启用' })
disabled: boolean;
@Column({ name: 'remark', comment: '备注', length: 512 })
remark: string;
@Column({ name: 'group_id', comment: '分组id' })
groupId: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })