diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd.ts index 373727a9..eed1e588 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/certd.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/certd.ts @@ -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", diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts index f5535c99..ff900a85 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts @@ -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: "过期", diff --git a/packages/ui/certd-client/src/views/certd/basic/group/api.ts b/packages/ui/certd-client/src/views/certd/basic/group/api.ts new file mode 100644 index 00000000..343697e3 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/basic/group/api.ts @@ -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", + }); +} diff --git a/packages/ui/certd-client/src/views/certd/basic/group/crud.tsx b/packages/ui/certd-client/src/views/certd/basic/group/crud.tsx new file mode 100644 index 00000000..b01c258c --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/basic/group/crud.tsx @@ -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 => { + 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, + }, + }, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/basic/group/group-selector.vue b/packages/ui/certd-client/src/views/certd/basic/group/group-selector.vue new file mode 100644 index 00000000..7a959cb4 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/basic/group/group-selector.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/basic/group/index.vue b/packages/ui/certd-client/src/views/certd/basic/group/index.vue new file mode 100644 index 00000000..26f2665b --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/basic/group/index.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx index 1ff7dbcd..dd62c7cd 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx @@ -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 {value}; + }, + }, + }, 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 {value}; + }, + }, + }, }, }, }; diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx index 4fe714e4..614496d2 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx @@ -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({ 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: { diff --git a/packages/ui/certd-server/db/migration/v10033__monitor_remark.sql b/packages/ui/certd-server/db/migration/v10033__monitor_remark.sql new file mode 100644 index 00000000..bad71cb3 --- /dev/null +++ b/packages/ui/certd-server/db/migration/v10033__monitor_remark.sql @@ -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; + diff --git a/packages/ui/certd-server/src/controller/user/basic/group-controller.ts b/packages/ui/certd-server/src/controller/user/basic/group-controller.ts new file mode 100644 index 00000000..0192ae2b --- /dev/null +++ b/packages/ui/certd-server/src/controller/user/basic/group-controller.ts @@ -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 { + @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); + } +} diff --git a/packages/ui/certd-server/src/modules/basic/entity/group.ts b/packages/ui/certd-server/src/modules/basic/entity/group.ts new file mode 100644 index 00000000..92a41319 --- /dev/null +++ b/packages/ui/certd-server/src/modules/basic/entity/group.ts @@ -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; +} diff --git a/packages/ui/certd-server/src/modules/basic/service/group-service.ts b/packages/ui/certd-server/src/modules/basic/service/group-service.ts new file mode 100644 index 00000000..64fdf755 --- /dev/null +++ b/packages/ui/certd-server/src/modules/basic/service/group-service.ts @@ -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 { + @InjectEntityModel(GroupEntity) + repository: Repository; + + //@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); + } +} diff --git a/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts b/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts index f4dc9501..869a9543 100644 --- a/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts +++ b/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts @@ -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' })