mirror of https://github.com/certd/certd
perf: 通知管理
parent
131ed13df1
commit
d9a00eeaf7
|
@ -48,4 +48,4 @@ admin/123456
|
|||
|
||||
## 五、备份恢复
|
||||
|
||||
将备份的`db.sqlite`覆盖到原来的位置即可
|
||||
将备份的`db.sqlite`覆盖到原来的位置,重启certd即可
|
||||
|
|
|
@ -81,4 +81,4 @@ services:
|
|||
|
||||
## 五、备份恢复
|
||||
|
||||
将备份的`db.sqlite`覆盖到原来的位置即可
|
||||
将备份的`db.sqlite`覆盖到原来的位置,重启certd即可
|
||||
|
|
|
@ -71,4 +71,4 @@ docker compose up -d
|
|||
|
||||
## 四、备份恢复
|
||||
|
||||
将备份的`db.sqlite`覆盖到原来的位置即可
|
||||
将备份的`db.sqlite`覆盖到原来的位置,重启certd即可
|
|
@ -42,4 +42,4 @@ kill -9 $(lsof -t -i:7001)
|
|||
|
||||
## 四、备份恢复
|
||||
|
||||
将备份的`db.sqlite`覆盖到原来的位置即可
|
||||
将备份的`db.sqlite`覆盖到原来的位置,重启certd即可
|
||||
|
|
|
@ -27,4 +27,4 @@
|
|||
|
||||
## 三、备份恢复
|
||||
|
||||
将备份的`db.sqlite`覆盖到原来的位置即可
|
||||
将备份的`db.sqlite`覆盖到原来的位置,重启certd即可
|
|
@ -2,7 +2,15 @@ import { Registrable } from "../registry/index.js";
|
|||
import { FormItemProps } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { AccessRequestHandleReq } from "../core";
|
||||
import { PluginRequestHandleReq } from "../plugin/index.js";
|
||||
|
||||
export type AccessRequestHandleReqInput<T = any> = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
access: T;
|
||||
};
|
||||
|
||||
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;
|
||||
|
||||
export type AccessInputDefine = FormItemProps & {
|
||||
title: string;
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
|
||||
export type PluginRequestHandleReq<T = any> = {
|
||||
typeName: string;
|
||||
action: string;
|
||||
input: T;
|
||||
data: any;
|
||||
};
|
||||
|
||||
export type AccessRequestHandleReqInput<T = any> = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
access: T;
|
||||
};
|
||||
export type AccessRequestHandleContext = {
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
utils: typeof utils;
|
||||
};
|
||||
|
||||
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;
|
|
@ -3,5 +3,4 @@ export * from "./run-history.js";
|
|||
export * from "./context.js";
|
||||
export * from "./storage.js";
|
||||
export * from "./file-store.js";
|
||||
export * from "./handler.js";
|
||||
export * from "./exceptions.js";
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from "./plugin/index.js";
|
|||
export * from "./context/index.js";
|
||||
export * from "./decorator/index.js";
|
||||
export * from "./service/index.js";
|
||||
export * from "./notification/index.js";
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { PluginRequestHandleReq } from "../plugin";
|
||||
import { Registrable } from "../registry/index.js";
|
||||
import { FormItemProps } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { IEmailService } from "../service";
|
||||
|
||||
export type NotificationBody = {
|
||||
userId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
pipelineId: number;
|
||||
historyId: number;
|
||||
url: string;
|
||||
extra?: any;
|
||||
options?: any;
|
||||
};
|
||||
|
||||
export type NotificationRequestHandleReqInput<T = any> = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
access: T;
|
||||
};
|
||||
|
||||
export type NotificationRequestHandleReq<T = any> = PluginRequestHandleReq<NotificationRequestHandleReqInput<T>>;
|
||||
|
||||
export type NotificationInputDefine = FormItemProps & {
|
||||
title: string;
|
||||
required?: boolean;
|
||||
encrypt?: boolean;
|
||||
};
|
||||
export type NotificationDefine = Registrable & {
|
||||
input?: {
|
||||
[key: string]: NotificationInputDefine;
|
||||
};
|
||||
};
|
||||
export interface INotificationService {
|
||||
send(body: NotificationBody): Promise<void>;
|
||||
}
|
||||
|
||||
export interface INotification extends INotificationService {
|
||||
ctx: NotificationContext;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type NotificationContext = {
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
utils: typeof utils;
|
||||
emailService: IEmailService;
|
||||
};
|
||||
|
||||
export abstract class BaseNotification implements INotification {
|
||||
ctx!: NotificationContext;
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
abstract send(body: NotificationBody): Promise<void>;
|
||||
setCtx(ctx: NotificationContext) {
|
||||
this.ctx = ctx;
|
||||
this.http = ctx.http;
|
||||
this.logger = ctx.logger;
|
||||
}
|
||||
|
||||
async onRequest(req: NotificationRequestHandleReq) {
|
||||
if (!req.action) {
|
||||
throw new Error("action is required");
|
||||
}
|
||||
|
||||
let methodName = req.action;
|
||||
if (!req.action.startsWith("on")) {
|
||||
methodName = `on${_.upperFirst(req.action)}`;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const method = this[methodName];
|
||||
if (method) {
|
||||
// @ts-ignore
|
||||
return await this[methodName](req.data);
|
||||
}
|
||||
throw new Error(`action ${req.action} not found`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// src/decorator/memoryCache.decorator.ts
|
||||
import { Decorator } from "../decorator/index.js";
|
||||
import * as _ from "lodash-es";
|
||||
import { notificationRegistry } from "./registry.js";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { NotificationContext, NotificationDefine, NotificationInputDefine } from "./api.js";
|
||||
|
||||
// 提供一个唯一 key
|
||||
export const NOTIFICATION_CLASS_KEY = "pipeline:notification";
|
||||
export const NOTIFICATION_INPUT_KEY = "pipeline:notification:input";
|
||||
|
||||
export function IsNotification(define: NotificationDefine): ClassDecorator {
|
||||
return (target: any) => {
|
||||
target = Decorator.target(target);
|
||||
|
||||
const inputs: any = {};
|
||||
const properties = Decorator.getClassProperties(target);
|
||||
for (const property in properties) {
|
||||
const input = Reflect.getMetadata(NOTIFICATION_INPUT_KEY, target, property);
|
||||
if (input) {
|
||||
inputs[property] = input;
|
||||
}
|
||||
}
|
||||
_.merge(define, { input: inputs });
|
||||
Reflect.defineMetadata(NOTIFICATION_CLASS_KEY, define, target);
|
||||
target.define = define;
|
||||
notificationRegistry.register(define.name, {
|
||||
define,
|
||||
target,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function NotificationInput(input?: NotificationInputDefine): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
target = Decorator.target(target, propertyKey);
|
||||
// const _type = Reflect.getMetadata("design:type", target, propertyKey);
|
||||
Reflect.defineMetadata(NOTIFICATION_INPUT_KEY, input, target, propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
export function newNotification(type: string, input: any, ctx?: NotificationContext) {
|
||||
const register = notificationRegistry.get(type);
|
||||
if (register == null) {
|
||||
throw new Error(`notification ${type} not found`);
|
||||
}
|
||||
// @ts-ignore
|
||||
const access = new register.target();
|
||||
for (const key in input) {
|
||||
access[key] = input[key];
|
||||
}
|
||||
if (!ctx) {
|
||||
ctx = {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
};
|
||||
}
|
||||
access.ctx = ctx;
|
||||
return access;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./api.js";
|
||||
export * from "./registry.js";
|
||||
export * from "./decorator.js";
|
|
@ -0,0 +1,4 @@
|
|||
import { createRegistry } from "../registry/index.js";
|
||||
|
||||
// @ts-ignore
|
||||
export const notificationRegistry = createRegistry("notification");
|
|
@ -3,12 +3,20 @@ import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.j
|
|||
import { FileStore } from "../core/file-store.js";
|
||||
import { IAccessService } from "../access/index.js";
|
||||
import { ICnameProxyService, IEmailService } from "../service/index.js";
|
||||
import { CancelError, IContext, PluginRequestHandleReq, RunnableCollection } from "../core/index.js";
|
||||
import { CancelError, IContext, RunnableCollection } from "../core/index.js";
|
||||
import { HttpRequestConfig, ILogger, logger, utils } from "@certd/basic";
|
||||
import { HttpClient } from "@certd/basic";
|
||||
import dayjs from "dayjs";
|
||||
import { IPluginConfigService } from "../service/config";
|
||||
import { upperFirst } from "lodash-es";
|
||||
|
||||
export type PluginRequestHandleReq<T = any> = {
|
||||
typeName: string;
|
||||
action: string;
|
||||
input: T;
|
||||
data: any;
|
||||
};
|
||||
|
||||
export type UserInfo = {
|
||||
role: "admin" | "user";
|
||||
id: any;
|
||||
|
|
|
@ -49,6 +49,17 @@ export const certdResources = [
|
|||
cache: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "通知设置",
|
||||
name: "NotificationManager",
|
||||
path: "/certd/notification",
|
||||
component: "/certd/notification/index.vue",
|
||||
meta: {
|
||||
icon: "ion:disc-outline",
|
||||
auth: true,
|
||||
cache: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "CNAME记录管理",
|
||||
name: "CnameRecord",
|
||||
|
|
|
@ -115,7 +115,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
}
|
||||
},
|
||||
cnameProviderId: {
|
||||
title: "CNAME提供者",
|
||||
title: "CNAME服务",
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
url: "/cname/provider/list",
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { request } from "/src/api/service";
|
||||
|
||||
export function createApi() {
|
||||
const apiPrefix = "/pi/notification";
|
||||
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 GetSimpleInfo(id: number) {
|
||||
return await request({
|
||||
url: apiPrefix + "/simpleInfo",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
async GetProviderDefine(type: string) {
|
||||
return await request({
|
||||
url: apiPrefix + "/define",
|
||||
method: "post",
|
||||
params: { type }
|
||||
});
|
||||
},
|
||||
|
||||
async GetProviderDefineByType(type: string) {
|
||||
return await request({
|
||||
url: apiPrefix + "/defineByType",
|
||||
method: "post",
|
||||
params: { type }
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
|
||||
import { computed, provide, ref, toRef } from "vue";
|
||||
import { useReference } from "/@/use/use-refrence";
|
||||
import { forEach, get, merge, set } from "lodash-es";
|
||||
|
||||
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
|
||||
provide("notificationApi", api);
|
||||
const notificationTypeDictRef = dict({
|
||||
url: "/pi/notification/getTypeDict"
|
||||
});
|
||||
const defaultPluginConfig = {
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value"
|
||||
}
|
||||
};
|
||||
|
||||
function buildDefineFields(define: any, form: any, mode: string) {
|
||||
const formWrapperRef = crudExpose.getFormWrapperRef();
|
||||
const columnsRef = toRef(formWrapperRef.formOptions, "columns");
|
||||
|
||||
for (const key in columnsRef.value) {
|
||||
if (key.indexOf(".") >= 0) {
|
||||
delete columnsRef.value[key];
|
||||
}
|
||||
}
|
||||
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
|
||||
forEach(define.input, (value: any, mapKey: any) => {
|
||||
const key = "body." + mapKey;
|
||||
const field = {
|
||||
...value,
|
||||
key
|
||||
};
|
||||
const column = merge({ title: key }, defaultPluginConfig, field);
|
||||
//eval
|
||||
useReference(column);
|
||||
|
||||
//设置默认值
|
||||
if (column.value != null && get(form, key) == null) {
|
||||
set(form, key, column.value);
|
||||
}
|
||||
//字段配置赋值
|
||||
columnsRef.value[key] = column;
|
||||
console.log("form", columnsRef.value);
|
||||
});
|
||||
}
|
||||
|
||||
const currentDefine = ref();
|
||||
|
||||
return {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: {
|
||||
width: 100
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
name: {
|
||||
title: "通知名称",
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: ["text"],
|
||||
form: {
|
||||
rules: [{ required: true, message: "请填写名称" }],
|
||||
helper: "随便填,当多个相同类型的通知时,便于区分"
|
||||
},
|
||||
column: {
|
||||
width: 200
|
||||
}
|
||||
},
|
||||
type: {
|
||||
title: "类型",
|
||||
type: "dict-select",
|
||||
dict: notificationTypeDictRef,
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
component: {
|
||||
color: "auto"
|
||||
}
|
||||
},
|
||||
form: {
|
||||
component: {
|
||||
disabled: false,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => {
|
||||
input = input?.toLowerCase();
|
||||
return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0;
|
||||
}
|
||||
},
|
||||
rules: [{ required: true, message: "请选择类型" }],
|
||||
valueChange: {
|
||||
immediate: true,
|
||||
async handle({ value, mode, form, immediate }) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
const define = await api.GetProviderDefine(value);
|
||||
currentDefine.value = define;
|
||||
console.log("define", define);
|
||||
if (!immediate) {
|
||||
form.body = {};
|
||||
}
|
||||
buildDefineFields(define, form, mode);
|
||||
}
|
||||
},
|
||||
helper: computed(() => {
|
||||
const define = currentDefine.value;
|
||||
if (define == null) {
|
||||
return "";
|
||||
}
|
||||
return define.desc;
|
||||
})
|
||||
},
|
||||
addForm: {
|
||||
value: typeRef
|
||||
}
|
||||
} as ColumnCompositionProps,
|
||||
setting: {
|
||||
column: { show: false },
|
||||
form: {
|
||||
show: false,
|
||||
valueBuilder({ value, form }) {
|
||||
form.body = {};
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const setting = JSON.parse(value);
|
||||
for (const key in setting) {
|
||||
form.body[key] = setting[key];
|
||||
}
|
||||
},
|
||||
valueResolve({ form }) {
|
||||
const setting = form.body;
|
||||
form.setting = JSON.stringify(setting);
|
||||
}
|
||||
}
|
||||
} as ColumnCompositionProps
|
||||
};
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// @ts-ignore
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { getCommonColumnDefine } from "./common";
|
||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { t } = useI18n();
|
||||
const api = context.api;
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async (req: EditReq) => {
|
||||
const { form, row } = req;
|
||||
form.id = row.id;
|
||||
const res = await api.UpdateObj(form);
|
||||
return res;
|
||||
};
|
||||
const delRequest = async (req: DelReq) => {
|
||||
const { row } = req;
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async (req: AddReq) => {
|
||||
const { form } = req;
|
||||
const res = await api.AddObj(form);
|
||||
return res;
|
||||
};
|
||||
|
||||
const typeRef = ref();
|
||||
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
form: {
|
||||
labelCol: {
|
||||
span: 6
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
width: 200
|
||||
},
|
||||
columns: {
|
||||
...commonColumnsDefine
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<fs-page>
|
||||
<template #header>
|
||||
<div class="title">
|
||||
通知管理
|
||||
<span class="sub">管理通知配置</span>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onActivated, onMounted } from "vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { createApi } from "./api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NotificationManager",
|
||||
setup() {
|
||||
const api = createApi();
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
onActivated(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<div class="notification-selector">
|
||||
<span v-if="target.name" class="mr-5 cd-flex-inline">
|
||||
<a-tag class="mr-5" color="green">{{ target.name }}</a-tag>
|
||||
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
|
||||
</span>
|
||||
<span v-else class="mlr-5 text-gray">{{ placeholder }}</span>
|
||||
<a-button class="ml-5" :size="size" @click="chooseForm.open">选择</a-button>
|
||||
<a-form-item-rest v-if="chooseForm.show">
|
||||
<a-modal v-model:open="chooseForm.show" title="选择通知" width="900px" @ok="chooseForm.ok">
|
||||
<div style="height: 400px; position: relative">
|
||||
<cert-notification-modal v-model="selectedId" :type="type" :from="from"></cert-notification-modal>
|
||||
</div>
|
||||
</a-modal>
|
||||
</a-form-item-rest>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, reactive, ref, watch, inject } from "vue";
|
||||
import CertNotificationModal from "./modal/index.vue";
|
||||
import { createApi } from "../api";
|
||||
import { message } from "ant-design-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NotificationSelector",
|
||||
components: { CertNotificationModal },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择"
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "middle"
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const api = createApi();
|
||||
|
||||
const target = ref({});
|
||||
const selectedId = ref();
|
||||
async function refreshTarget(value) {
|
||||
selectedId.value = value;
|
||||
if (value > 0) {
|
||||
target.value = await api.GetSimpleInfo(value);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (pipeline && pipeline.userId !== target.value.userId) {
|
||||
message.error("对不起,您不能修改他人流水线的通知");
|
||||
return;
|
||||
}
|
||||
selectedId.value = "";
|
||||
target.value = null;
|
||||
ctx.emit("update:modelValue", selectedId.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
async (value) => {
|
||||
selectedId.value = null;
|
||||
target.value = {};
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
await refreshTarget(value);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const providerDefine = ref({});
|
||||
|
||||
async function refreshProviderDefine(type) {
|
||||
providerDefine.value = await api.GetProviderDefine(type);
|
||||
}
|
||||
watch(
|
||||
() => {
|
||||
return props.type;
|
||||
},
|
||||
async (value) => {
|
||||
await refreshProviderDefine(value);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
//当不在pipeline中编辑时,可能为空
|
||||
const pipeline = inject("pipeline", null);
|
||||
|
||||
const chooseForm = reactive({
|
||||
show: false,
|
||||
open() {
|
||||
chooseForm.show = true;
|
||||
},
|
||||
ok: () => {
|
||||
chooseForm.show = false;
|
||||
console.log("choose ok:", selectedId.value);
|
||||
refreshTarget(selectedId.value);
|
||||
|
||||
if (pipeline && pipeline.userId !== target.value.userId) {
|
||||
message.error("对不起,您不能修改他人流水线的授权");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.emit("change", selectedId.value);
|
||||
ctx.emit("update:modelValue", selectedId.value);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
clear,
|
||||
target,
|
||||
selectedId,
|
||||
providerDefine,
|
||||
chooseForm
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.notification-selector {
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
// @ts-ignore
|
||||
import { ref } from "vue";
|
||||
import { getCommonColumnDefine } from "../../common";
|
||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { crudBinding } = crudExpose;
|
||||
const { props, ctx, api } = context;
|
||||
const lastResRef = ref();
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await context.api.GetList(query);
|
||||
};
|
||||
const editRequest = async (req: EditReq) => {
|
||||
const { form, row } = req;
|
||||
form.id = row.id;
|
||||
form.type = props.type;
|
||||
const res = await context.api.UpdateObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
const delRequest = async (req: DelReq) => {
|
||||
const { row } = req;
|
||||
return await context.api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async (req: AddReq) => {
|
||||
const { form } = req;
|
||||
form.type = props.type;
|
||||
const res = await context.api.AddObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
|
||||
const selectedRowKey = ref([props.modelValue]);
|
||||
|
||||
const onSelectChange = (changed: any) => {
|
||||
selectedRowKey.value = changed;
|
||||
ctx.emit("update:modelValue", changed[0]);
|
||||
};
|
||||
|
||||
const typeRef = ref("");
|
||||
context.typeRef = typeRef;
|
||||
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
|
||||
commonColumnsDefine.type.form.component.disabled = true;
|
||||
return {
|
||||
typeRef,
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
form: {
|
||||
wrapper: {
|
||||
width: "1050px"
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
width: 200
|
||||
},
|
||||
table: {
|
||||
scroll: {
|
||||
x: 800
|
||||
},
|
||||
rowSelection: {
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedRowKey,
|
||||
onChange: onSelectChange
|
||||
},
|
||||
customRow: (record: any) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
onSelectChange([record.id]);
|
||||
} // 点击行
|
||||
};
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
...commonColumnsDefine
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<fs-page class="page-cert-notification-modal">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, watch } from "vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { createApi } from "../../api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CertNotificationModal",
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
modelValue: {}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const api = createApi();
|
||||
const context: any = { props, ctx, api };
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
||||
|
||||
// 你可以调用此方法,重新初始化crud配置
|
||||
function onTypeChanged(value: any) {
|
||||
context.typeRef.value = value;
|
||||
crudExpose.setSearchFormData({ form: { type: value }, mergeForm: true });
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
watch(
|
||||
() => {
|
||||
return props.type;
|
||||
},
|
||||
(value) => {
|
||||
console.log("access type changed:", value);
|
||||
onTypeChanged(value);
|
||||
}
|
||||
);
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
onTypeChanged(props.type);
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.page-cert-notification {
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
CREATE TABLE "pi_notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "name" varchar(100) NOT NULL, "type" varchar(100) NOT NULL, "setting" text, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP));
|
|
@ -0,0 +1,91 @@
|
|||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||
import { Constants, CrudController } from '@certd/lib-server';
|
||||
import { NotificationService } from '../../modules/pipeline/service/notification-service.js';
|
||||
import { AuthService } from '../../modules/sys/authority/service/auth-service.js';
|
||||
|
||||
/**
|
||||
* 通知
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/api/pi/notification')
|
||||
export class NotificationController extends CrudController<NotificationService> {
|
||||
@Inject()
|
||||
service: NotificationService;
|
||||
@Inject()
|
||||
authService: AuthService;
|
||||
|
||||
getService(): NotificationService {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
@Post('/page', { summary: Constants.per.authOnly })
|
||||
async page(@Body(ALL) body) {
|
||||
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) {
|
||||
body.userId = this.getUserId();
|
||||
return super.list(body);
|
||||
}
|
||||
|
||||
@Post('/add', { summary: Constants.per.authOnly })
|
||||
async add(@Body(ALL) bean) {
|
||||
bean.userId = this.getUserId();
|
||||
return super.add(bean);
|
||||
}
|
||||
|
||||
@Post('/update', { summary: Constants.per.authOnly })
|
||||
async update(@Body(ALL) bean) {
|
||||
await this.service.checkUserId(bean.id, this.getUserId());
|
||||
return super.update(bean);
|
||||
}
|
||||
@Post('/info', { summary: Constants.per.authOnly })
|
||||
async info(@Query('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return super.info(id);
|
||||
}
|
||||
|
||||
@Post('/delete', { summary: Constants.per.authOnly })
|
||||
async delete(@Query('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@Post('/define', { summary: Constants.per.authOnly })
|
||||
async define(@Query('type') type: string) {
|
||||
const notification = this.service.getDefineByType(type);
|
||||
return this.ok(notification);
|
||||
}
|
||||
|
||||
@Post('/getTypeDict', { summary: Constants.per.authOnly })
|
||||
async getTypeDict() {
|
||||
const list = this.service.getDefineList();
|
||||
const dict = [];
|
||||
for (const item of list) {
|
||||
dict.push({
|
||||
value: item.name,
|
||||
label: item.title,
|
||||
});
|
||||
}
|
||||
return this.ok(dict);
|
||||
}
|
||||
|
||||
@Post('/simpleInfo', { summary: Constants.per.authOnly })
|
||||
async simpleInfo(@Query('id') id: number) {
|
||||
await this.authService.checkEntityUserId(this.ctx, this.service, id);
|
||||
const res = await this.service.getSimpleInfo(id);
|
||||
return this.ok(res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('pi_notification')
|
||||
export class NotificationEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'user_id', comment: 'UserId' })
|
||||
userId: string;
|
||||
|
||||
@Column({ name: 'type', comment: '通知类型' })
|
||||
type: string;
|
||||
|
||||
@Column({ name: 'name', comment: '名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'setting', comment: '通知配置', length: 10240 })
|
||||
setting: string;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
createTime: Date;
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '修改时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService, ValidateException } from '@certd/lib-server';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotificationEntity } from '../entity/notification.js';
|
||||
import { notificationRegistry } from '@certd/pipeline';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class NotificationService extends BaseService<NotificationEntity> {
|
||||
@InjectEntityModel(NotificationEntity)
|
||||
repository: Repository<NotificationEntity>;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async getSimpleInfo(id: number) {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
throw new ValidateException('该通知配置不存在,请确认是否已被删除');
|
||||
}
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
userId: entity.userId,
|
||||
};
|
||||
}
|
||||
|
||||
getDefineList() {
|
||||
return notificationRegistry.getDefineList();
|
||||
}
|
||||
|
||||
getDefineByType(type: string) {
|
||||
return notificationRegistry.getDefine(type);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
|
||||
|
||||
@IsNotification({
|
||||
name: 'email',
|
||||
title: '电子邮件',
|
||||
desc: '电子邮件通知',
|
||||
})
|
||||
export class EmailNotification extends BaseNotification {
|
||||
@NotificationInput({
|
||||
title: '收件人邮箱',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
mode: 'tags',
|
||||
open: false,
|
||||
},
|
||||
required: true,
|
||||
helper: '可以填写多个,填写一个按回车键再填写下一个',
|
||||
})
|
||||
receivers!: string[];
|
||||
|
||||
async send(body: NotificationBody) {
|
||||
await this.ctx.emailService.send({
|
||||
userId: body.userId,
|
||||
subject: body.title,
|
||||
content: body.content,
|
||||
receivers: this.receivers,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './qywx/index.js';
|
||||
export * from './email/index.js';
|
|
@ -0,0 +1,54 @@
|
|||
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
|
||||
|
||||
@IsNotification({
|
||||
name: 'qywx',
|
||||
title: '企业微信通知',
|
||||
desc: '企业微信群聊机器人通知',
|
||||
})
|
||||
export class QywxNotification extends BaseNotification {
|
||||
@NotificationInput({
|
||||
title: 'webhook地址',
|
||||
component: {
|
||||
placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
webhook = '';
|
||||
|
||||
@NotificationInput({
|
||||
title: '提醒指定成员',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
mode: 'tags',
|
||||
open: false,
|
||||
},
|
||||
required: false,
|
||||
helper: '填写成员名字,@all 为提醒所有人',
|
||||
})
|
||||
mentionedList!: string[];
|
||||
|
||||
async send(body: NotificationBody) {
|
||||
console.log('send qywx');
|
||||
|
||||
/**
|
||||
*
|
||||
* "msgtype": "text",
|
||||
* "text": {
|
||||
* "content": "hello world"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
await this.http.request({
|
||||
url: this.webhook,
|
||||
data: {
|
||||
msgtype: 'markdown',
|
||||
text: {
|
||||
content: `# ${body.title}\n\n${body.content}\n[查看详情](${body.url})`,
|
||||
mentioned_list: this.mentionedList,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue