feat: 邮件通知

This commit is contained in:
xiaojunnuo
2023-06-25 15:30:18 +08:00
parent 64afebecd4
commit 937e3fac19
32 changed files with 790 additions and 88 deletions
+2 -2
View File
@@ -106,8 +106,8 @@ function createService() {
* @description 创建请求方法
* @param {Object} service axios 实例
*/
function createRequestFunction(service) {
return function (config) {
function createRequestFunction(service: any) {
return function (config: any) {
const configDefault = {
headers: {
"Content-Type": get(config, "headers.Content-Type", "application/json")
+6 -5
View File
@@ -48,7 +48,10 @@ export function responseError(data = {}, msg = "请求失败", code = 500) {
* @description 记录和显示错误
* @param {Error} error 错误对象
*/
export function errorLog(error) {
export function errorLog(error: any) {
if (error?.response?.data?.message) {
error.message = error?.response?.data?.message;
}
// 打印到控制台
console.error(error);
// 显示提示
@@ -59,8 +62,6 @@ export function errorLog(error) {
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate(msg) {
const error = new Error(msg);
errorLog(error);
throw error;
export function errorCreate(msg: string) {
throw new Error(msg);
}
@@ -35,6 +35,28 @@ export const certdResources = [
meta: {
icon: "ion:disc-outline"
}
},
{
title: "设置",
name: "certdSettings",
path: "/certd/settings",
redirect: "/certd/settings/email",
meta: {
icon: "ion:settings-outline",
auth: true
},
children: [
{
title: "邮箱设置",
name: "email",
path: "/certd/settings/email",
component: "/certd/settings/email-setting.vue",
meta: {
icon: "ion:mail-outline",
auth: true
}
}
]
}
]
}
+10 -7
View File
@@ -1,15 +1,16 @@
// @ts-ignore
import _ from "lodash";
export function getEnvValue(key) {
export function getEnvValue(key: string) {
// @ts-ignore
return import.meta.env["VITE_APP_" + key];
}
export class EnvConfig {
API;
MODE;
STORAGE;
TITLE;
PM_ENABLED;
API: string;
MODE: string;
STORAGE: string;
TITLE: string;
PM_ENABLED: string;
constructor() {
this.init();
}
@@ -19,6 +20,7 @@ export class EnvConfig {
_.forEach(import.meta.env, (value, key) => {
if (key.startsWith("VITE_APP")) {
key = key.replace("VITE_APP_", "");
//@ts-ignore
this[key] = value;
}
});
@@ -26,7 +28,8 @@ export class EnvConfig {
this.MODE = import.meta.env.MODE;
}
get(key, defaultValue) {
get(key: string, defaultValue: string) {
//@ts-ignore
return this[key] ?? defaultValue;
}
isDev() {
@@ -2,9 +2,9 @@ import { env } from "./util.env";
export const site = {
/**
* @description 更新标题
* @param {String} title 标题
* @param titleText
*/
title: function (titleText) {
title: function (titleText: string) {
const processTitle = env.TITLE || "FsAdmin";
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
}
@@ -3,7 +3,7 @@ import { RunHistory } from "/@/views/certd/pipeline/pipeline/type";
const apiPrefix = "/pi/history";
export async function GetList(query) {
export async function GetList(query: any) {
const list = await request({
url: apiPrefix + "/list",
method: "post",
@@ -18,7 +18,7 @@ export async function GetList(query) {
return list;
}
export async function GetDetail(query): Promise<RunHistory> {
export async function GetDetail(query: any): Promise<RunHistory> {
const detail = await request({
url: apiPrefix + "/detail",
method: "post",
@@ -0,0 +1,200 @@
<template>
<a-drawer v-model:visible="notificationDrawerVisible" placement="right" :closable="true" width="600px" class="pi-notification-form" :after-visible-change="notificationDrawerOnAfterVisibleChange">
<template #title>
编辑触发器
<a-button v-if="mode === 'edit'" @click="notificationDelete()">
<template #icon><DeleteOutlined /></template>
</a-button>
</template>
<template v-if="currentNotification">
<pi-container>
<a-form ref="notificationFormRef" class="notification-form" :model="currentNotification" :label-col="labelCol" :wrapper-col="wrapperCol">
<fs-form-item
v-model="currentNotification.type"
:item="{
title: '类型',
key: 'type',
value: 'email',
component: {
name: 'a-select',
vModel: 'value',
disabled: !editMode,
options: [{ value: 'email', label: '邮件' }]
},
rules: [{ required: true, message: '此项必填' }]
}"
/>
<fs-form-item
v-model="currentNotification.when"
:item="{
title: '触发时机',
key: 'type',
value: ['error'],
component: {
name: 'a-select',
vModel: 'value',
disabled: !editMode,
mode: 'multiple',
options: [
{ value: 'start', label: '开始时' },
{ value: 'success', label: '成功时' },
{ value: 'error', label: '错误时' }
]
},
rules: [{ required: true, message: '此项必填' }]
}"
/>
<pi-notification-form-email ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email>
</a-form>
<template #footer>
<a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="notificationSave"> 确定 </a-button>
</a-form-item>
</template>
</pi-container>
</template>
</a-drawer>
</template>
<script lang="ts">
import { Modal } from "ant-design-vue";
import { ref } from "vue";
import _ from "lodash";
import { nanoid } from "nanoid";
import PiNotificationFormEmail from "./pi-notification-form-email.vue";
export default {
name: "PiNotificationForm",
components: { PiNotificationFormEmail },
props: {
editMode: {
type: Boolean,
default: true
}
},
emits: ["update"],
setup(props: any, context: any) {
/**
* notification drawer
* @returns
*/
function useNotificationForm() {
const mode = ref("add");
const callback = ref();
const currentNotification = ref({ type: undefined, when: [], options: {} });
const currentPlugin = ref({});
const notificationFormRef = ref(null);
const notificationDrawerVisible = ref(false);
const optionsRef = ref();
const rules = ref({
type: [
{
type: "string",
required: true,
message: "请选择类型"
}
],
when: [
{
type: "string",
required: true,
message: "请选择通知时机"
}
]
});
const notificationDrawerShow = () => {
notificationDrawerVisible.value = true;
};
const notificationDrawerClose = () => {
notificationDrawerVisible.value = false;
};
const notificationDrawerOnAfterVisibleChange = (val: any) => {
console.log("notificationDrawerOnAfterVisibleChange", val);
};
const notificationOpen = (notification: any, emit: any) => {
callback.value = emit;
currentNotification.value = _.cloneDeep(notification);
console.log("currentNotificationOpen", currentNotification.value);
notificationDrawerShow();
};
const notificationAdd = (emit: any) => {
mode.value = "add";
const notification = { id: nanoid(), type: "email", when: ["error"] };
notificationOpen(notification, emit);
};
const notificationEdit = (notification: any, emit: any) => {
mode.value = "edit";
notificationOpen(notification, emit);
};
const notificationView = (notification: any, emit: any) => {
mode.value = "view";
notificationOpen(notification, emit);
};
const notificationSave = async (e: any) => {
currentNotification.value.options = await optionsRef.value.getValue();
console.log("currentNotificationSave", currentNotification.value);
try {
await notificationFormRef.value.validate();
} catch (e) {
console.error("表单验证失败:", e);
return;
}
callback.value("save", currentNotification.value);
notificationDrawerClose();
};
const notificationDelete = () => {
Modal.confirm({
title: "确认",
content: `确定要删除此触发器吗?`,
async onOk() {
callback.value("delete");
notificationDrawerClose();
}
});
};
const blankFn = () => {
return {};
};
return {
notificationFormRef,
mode,
notificationAdd,
notificationEdit,
notificationView,
notificationDrawerShow,
notificationDrawerVisible,
notificationDrawerOnAfterVisibleChange,
currentNotification,
currentPlugin,
notificationSave,
notificationDelete,
rules,
blankFn,
optionsRef
};
}
return {
...useNotificationForm(),
labelCol: { span: 6 },
wrapperCol: { span: 16 }
};
}
};
</script>
<style lang="less">
.pi-notification-form {
}
</style>
@@ -0,0 +1,57 @@
<template>
<div>
<fs-form-item
v-model="optionsFormState.receivers"
:item="{
title: '收件邮箱',
key: 'type',
component: {
name: 'a-select',
vModel: 'value',
mode: 'tags'
},
rules: [{ required: true, message: '此项必填' }]
}"
/>
</div>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from "vue";
const props = defineProps({
options: {
type: Object as PropType<any>,
default: () => {}
}
});
const optionsFormState: Ref<any> = ref({});
watch(
() => {
return props.options;
},
() => {
optionsFormState.value = {
...props.options
};
},
{
immediate: true
}
);
const emit = defineEmits(["change"]);
function doEmit() {
emit("change", { ...optionsFormState.value });
}
function getValue() {
return { ...optionsFormState.value };
}
defineExpose({
doEmit,
getValue
});
</script>
@@ -2,7 +2,6 @@
<fs-page v-if="pipeline" class="page-pipeline-edit">
<template #header>
<div class="title">
<fs-button icon="ion:left" @click="goBack" />
<pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable>
</div>
<div class="more">
@@ -63,7 +62,7 @@
</div>
</div>
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': !editMode && index === pipeline.stages.length - 1 }">
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
<div class="title">
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
</div>
@@ -118,6 +117,48 @@
</a-button>
</div>
</div>
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, ii as number)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.type }}
</a-button>
</div>
</div>
<div class="task-container">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="notificationAdd()">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
添加通知
</a-button>
</div>
</div>
</div>
</div>
<div v-else class="stage last-stage">
<div class="title">
<pi-editable model-value="结束" :disabled="true" />
</div>
<div class="tasks">
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, index)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.type }}
</a-button>
</div>
</div>
</div>
</div>
</div>
@@ -140,6 +181,7 @@
<pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form>
<pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form>
<pi-task-view ref="taskViewRef"></pi-task-view>
<PiNotificationForm ref="notificationFormRef" :edit-mode="editMode"></PiNotificationForm>
</fs-page>
</template>
@@ -148,9 +190,10 @@ import { defineComponent, ref, provide, Ref, watch } from "vue";
import { useRouter } from "vue-router";
import PiTaskForm from "./component/task-form/index.vue";
import PiTriggerForm from "./component/trigger-form/index.vue";
import PiNotificationForm from "./component/notification-form/index.vue";
import PiTaskView from "./component/task-view/index.vue";
import PiStatusShow from "./component/status-show.vue";
import _ from "lodash-es";
import _ from "lodash";
import { message, Modal, notification } from "ant-design-vue";
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
import { nanoid } from "nanoid";
@@ -159,7 +202,7 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow },
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
props: {
pipelineId: {
type: [Number, String],
@@ -269,7 +312,7 @@ export default defineComponent({
return;
}
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [] }, detail.pipeline);
currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [], notifications: [] }, detail.pipeline);
pipeline.value = currentPipeline.value;
await loadHistoryList(true);
},
@@ -369,8 +412,13 @@ export default defineComponent({
pipeline.value.stages.splice(stageIndex, 0, stage);
});
};
function isLastStage(index: number) {
return !props.editMode && index === pipeline.value.stages.length - 1 && pipeline.value.notifications?.length < 1;
}
return {
stageAdd
stageAdd,
isLastStage
};
}
@@ -406,6 +454,41 @@ export default defineComponent({
};
}
function useNotification() {
const notificationFormRef = ref();
const notificationAdd = () => {
notificationFormRef.value.notificationAdd((type: string, value: any) => {
if (type === "save") {
if (pipeline.value.notifications == null) {
pipeline.value.notifications = [];
}
pipeline.value.notifications.push(value);
}
});
};
const notificationEdit = (notification: any, index: any) => {
if (notificationFormRef.value == null) {
return;
}
if (props.editMode) {
notificationFormRef.value.notificationEdit(notification, (type: string, value: any) => {
if (type === "delete") {
pipeline.value.notifications.splice(index, 1);
} else if (type === "save") {
pipeline.value.notifications[index] = value;
}
});
} else {
notificationFormRef.value.notificationView(notification, (type: string, value: any) => {});
}
};
return {
notificationAdd,
notificationEdit,
notificationFormRef
};
}
function useActions() {
const saveLoading = ref();
const run = async () => {
@@ -484,6 +567,7 @@ export default defineComponent({
historyCancel
};
}
const useTaskRet = useTask();
const useStageRet = useStage(useTaskRet);
@@ -495,7 +579,8 @@ export default defineComponent({
...useStageRet,
...useTrigger(),
...useActions(),
...useHistory()
...useHistory(),
...useNotification()
};
}
});
@@ -8,9 +8,9 @@ export class PluginManager {
* 初始化plugins
* @param plugins
*/
init(plugins) {
init(plugins: any) {
const list = plugins;
const map = {};
const map: any = {};
for (const plugin of list) {
map[plugin.key] = plugin;
}
@@ -21,7 +21,7 @@ export class PluginManager {
return this.map[name];
}
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
const steps = this.collectionPreStepOutputs({
pipeline,
currentStageIndex,
@@ -42,7 +42,7 @@ export class PluginManager {
return options;
}
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
const steps: any[] = [];
// 开始放step
for (let i = 0; i < currentStageIndex; i++) {
@@ -0,0 +1,12 @@
import { request } from "/@/api/service";
const apiPrefix = "/basic/email";
export async function TestSend(receiver: string) {
await request({
url: apiPrefix + "/test",
method: "post",
data: {
receiver
}
});
}
@@ -0,0 +1,26 @@
import { request } from "/@/api/service";
const apiPrefix = "/sys/settings";
export const SettingKeys = {
Email: "email"
};
export async function SettingsGet(key: string) {
return await request({
url: apiPrefix + "/get",
method: "post",
params: {
key
}
});
}
export async function SettingsSave(key: string, setting: any) {
await request({
url: apiPrefix + "/save",
method: "post",
data: {
key,
setting: JSON.stringify(setting)
}
});
}
@@ -0,0 +1,128 @@
<template>
<fs-page class="page-setting-email">
<template #header>
<div class="title">邮件设置</div>
</template>
<div class="email-form">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="STMP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="STMP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-input v-model:value="formState.port" />
</a-form-item>
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="formState.auth.user" />
</a-form-item>
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="formState.auth.pass" />
</a-form-item>
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
<a-input v-model:value="formState.sender" />
</a-form-item>
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
<div>
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
<a-input v-model:value="testFormState.receiver" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">测试</a-button>
</a-form-item>
</a-form>
</div>
</div>
</fs-page>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import * as api from "./api";
import * as emailApi from "./api.email";
import { SettingKeys } from "./api";
import { notification } from "ant-design-vue";
interface FormState {
host: string;
port: number;
auth: {
user: string;
pass: string;
};
secure: boolean; // use TLS
tls: {
// do not fail on invalid certs
rejectUnauthorized?: boolean;
};
sender: string;
}
const formState = reactive<Partial<FormState>>({
auth: {
user: "",
pass: ""
},
tls: {}
});
async function load() {
const data: any = await api.SettingsGet(SettingKeys.Email);
const setting = JSON.parse(data.setting);
Object.assign(formState, setting);
}
load();
const onFinish = async (form: any) => {
console.log("Success:", form);
await api.SettingsSave(SettingKeys.Email, form);
notification.success({
message: "保存成功"
});
};
const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo);
};
interface TestFormState {
receiver: string;
loading: boolean;
}
const testFormState = reactive<TestFormState>({
receiver: "",
loading: false
});
async function onTestSend() {
testFormState.loading = true;
try {
await emailApi.TestSend(testFormState.receiver);
notification.success({
message: "发送成功"
});
} finally {
testFormState.loading = false;
}
}
</script>
<style lang="less">
.page-setting-email {
.email-form {
width: 500px;
margin: 20px;
}
}
</style>