|
|
@@ -0,0 +1,409 @@
|
|
|
+<template>
|
|
|
+ <div class="notify-config-container">
|
|
|
+ <el-card shadow="hover" :body-style="{ padding: '12px 16px' }">
|
|
|
+ <el-tabs v-model="activeTab">
|
|
|
+ <!-- ── 通知模板 ── -->
|
|
|
+ <el-tab-pane label="通知模板" name="template">
|
|
|
+ <div class="tpl-toolbar">
|
|
|
+ <el-form :inline="true" :model="tplQuery" @submit.prevent>
|
|
|
+ <el-form-item label="通知类型">
|
|
|
+ <el-select v-model="tplQuery.notifyType" placeholder="全部" clearable style="width: 160px">
|
|
|
+ <el-option v-for="t in notifyTypes" :key="t.value" :label="`${t.label} (${t.value})`" :value="t.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="业务类型">
|
|
|
+ <el-select v-model="tplQuery.bizType" placeholder="全部(含全局默认)" clearable filterable style="width: 220px">
|
|
|
+ <el-option label="全局默认(空)" value="" />
|
|
|
+ <el-option v-for="b in bizTypes" :key="b.code" :label="`${b.name} (${b.code})`" :value="b.code" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="loadTemplates">查询</el-button>
|
|
|
+ <el-button type="success" @click="openAddTemplate">新增覆盖</el-button>
|
|
|
+ <el-button text @click="showVariables = true">查看可用变量</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table :data="templates" v-loading="tplLoading" border stripe size="small">
|
|
|
+ <el-table-column prop="notifyType" label="通知类型" width="120" />
|
|
|
+ <el-table-column prop="bizType" label="业务类型" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.bizType ? 'warning' : 'info'">
|
|
|
+ {{ row.bizType || '全局默认' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="title" label="标题模板" min-width="220" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="content" label="正文模板" min-width="280" show-overflow-tooltip />
|
|
|
+ <el-table-column label="启用" width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.isEnabled ? 'success' : 'info'">{{ row.isEnabled ? '启用' : '停用' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="预置" width="70" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.isSystem" size="small" type="primary">系统</el-tag>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="160" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="small" text @click="openEditTemplate(row)">编辑</el-button>
|
|
|
+ <el-button size="small" text type="danger" @click="removeTemplate(row)" :disabled="row.isSystem">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- ── 通知渠道 ── -->
|
|
|
+ <el-tab-pane label="通知渠道" name="channel">
|
|
|
+ <div class="cfg-tip">
|
|
|
+ <el-alert type="info" :closable="false" show-icon>
|
|
|
+ <template #title>
|
|
|
+ 启用状态、凭据保存到数据库后立即生效(DB 覆盖层 + JSON 兜底)。
|
|
|
+ Source="DB" 表示已在库中保存过;"JSON" 表示使用 ApprovalFlow.json 兜底值,可点"编辑"落盘。
|
|
|
+ 外部渠道需先在 <code>ApprovalFlow.json</code> 打开 <code>Notify.XXX=true</code> 或在此保存 Enabled=true。
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ <div style="margin-top: 8px">
|
|
|
+ <el-button type="warning" plain size="small" @click="handleTestSend">发送 SignalR 站内测试消息</el-button>
|
|
|
+ <span class="hint">仅测试 SignalR 链路:会向当前登录用户推送一条测试通知。</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table :data="channels" v-loading="cfgLoading" border stripe size="small" style="margin-top: 10px">
|
|
|
+ <el-table-column prop="channelKey" label="渠道" width="130">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="channelTagType(row.channelKey)">{{ channelLabel(row.channelKey) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="启用" width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="webhookUrl" label="Webhook URL" min-width="260" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">{{ row.webhookUrl || '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="templateId" label="模板Id" width="130">
|
|
|
+ <template #default="{ row }">{{ row.templateId || '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="source" label="来源" width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.source === 'DB' ? 'success' : ''">{{ row.source }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作" width="100" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="small" text @click="openEditChannel(row)">编辑</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 模板编辑弹窗 -->
|
|
|
+ <el-dialog v-model="tplDialogVisible" :title="tplIsEdit ? '编辑通知模板' : '新增通知模板'" width="620px" destroy-on-close>
|
|
|
+ <el-form ref="tplFormRef" :model="tplForm" :rules="tplRules" label-width="100px">
|
|
|
+ <el-form-item label="通知类型" prop="notifyType">
|
|
|
+ <el-select v-model="tplForm.notifyType" placeholder="请选择" style="width: 100%" :disabled="tplIsEdit && tplForm.isSystem">
|
|
|
+ <el-option v-for="t in notifyTypes" :key="t.value" :label="`${t.label} (${t.value})`" :value="t.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="业务类型" prop="bizType">
|
|
|
+ <el-select v-model="tplForm.bizType" placeholder="全局默认留空;选业务类型则仅对该业务覆盖" clearable filterable style="width: 100%" :disabled="tplIsEdit && tplForm.isSystem">
|
|
|
+ <el-option label="全局默认(空)" value="" />
|
|
|
+ <el-option v-for="b in bizTypes" :key="b.code" :label="`${b.name} (${b.code})`" :value="b.code" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="标题" prop="title">
|
|
|
+ <el-input v-model="tplForm.title" placeholder="支持变量 {title}" maxlength="256" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="正文" prop="content">
|
|
|
+ <el-input v-model="tplForm.content" type="textarea" :rows="3" placeholder="支持变量 {title} {nodeName} 等" maxlength="1024" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="启用">
|
|
|
+ <el-switch v-model="tplForm.isEnabled" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="tplForm.remark" maxlength="256" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="tplDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSaveTemplate">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 渠道编辑弹窗 -->
|
|
|
+ <el-dialog v-model="cfgDialogVisible" title="编辑通知渠道配置" width="560px" destroy-on-close>
|
|
|
+ <el-form ref="cfgFormRef" :model="cfgForm" label-width="110px">
|
|
|
+ <el-form-item label="渠道">
|
|
|
+ <el-tag size="small">{{ channelLabel(cfgForm.channelKey) }}({{ cfgForm.channelKey }})</el-tag>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="启用">
|
|
|
+ <el-switch v-model="cfgForm.enabled" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="needWebhook(cfgForm.channelKey)" label="Webhook URL">
|
|
|
+ <el-input v-model="cfgForm.webhookUrl" placeholder="群机器人 Webhook URL" maxlength="512" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="cfgForm.channelKey === 'DingTalk'" label="加签 Secret">
|
|
|
+ <el-input v-model="cfgForm.secret" placeholder="钉钉加签 Secret(可选)" maxlength="128" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="cfgForm.channelKey === 'Sms'" label="短信模板Id">
|
|
|
+ <el-input v-model="cfgForm.templateId" placeholder="运营商短信模板 Id" maxlength="64" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="cfgForm.remark" maxlength="256" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="cfgDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSaveChannel">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 变量说明 -->
|
|
|
+ <el-dialog v-model="showVariables" title="通知模板可用变量" width="620px">
|
|
|
+ <el-table :data="variables" size="small" border>
|
|
|
+ <el-table-column prop="key" label="变量" width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small">{{ '{' + row.key + '}' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="description" label="含义" />
|
|
|
+ <el-table-column prop="appliedTo" label="适用通知类型" width="220" />
|
|
|
+ </el-table>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="approvalFlowNotifyConfig">
|
|
|
+import { ref, onMounted, reactive } from 'vue';
|
|
|
+import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
|
|
|
+import {
|
|
|
+ getNotifyTemplateList,
|
|
|
+ saveNotifyTemplate,
|
|
|
+ deleteNotifyTemplate,
|
|
|
+ getNotifyTemplateVariables,
|
|
|
+ getNotifyConfigList,
|
|
|
+ saveNotifyConfig,
|
|
|
+ testSendNotify,
|
|
|
+ getBizTypeList,
|
|
|
+ NotifyTemplate,
|
|
|
+ NotifyChannel,
|
|
|
+} from '/@/views/approvalFlow/api';
|
|
|
+
|
|
|
+const activeTab = ref<'template' | 'channel'>('template');
|
|
|
+
|
|
|
+// ── 通知类型列表(与后端 FlowNotificationTypeEnum 保持一致) ──
|
|
|
+const notifyTypes = [
|
|
|
+ { value: 'NewTask', label: '待审批通知' },
|
|
|
+ { value: 'Urge', label: '催办' },
|
|
|
+ { value: 'FlowCompleted', label: '流程完成' },
|
|
|
+ { value: 'Transferred', label: '转办' },
|
|
|
+ { value: 'Returned', label: '退回' },
|
|
|
+ { value: 'AddSign', label: '加签' },
|
|
|
+ { value: 'Withdrawn', label: '撤回' },
|
|
|
+ { value: 'Escalated', label: '升级' },
|
|
|
+ { value: 'Timeout', label: '超时提醒' },
|
|
|
+];
|
|
|
+
|
|
|
+const bizTypes = ref<Array<{ code: string; name: string }>>([]);
|
|
|
+
|
|
|
+// ═══════════════════════ 模板 Tab ═══════════════════════
|
|
|
+
|
|
|
+const tplQuery = reactive<{ notifyType: string; bizType: string }>({ notifyType: '', bizType: '' });
|
|
|
+const templates = ref<NotifyTemplate[]>([]);
|
|
|
+const tplLoading = ref(false);
|
|
|
+
|
|
|
+async function loadTemplates() {
|
|
|
+ tplLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getNotifyTemplateList({
|
|
|
+ notifyType: tplQuery.notifyType || undefined,
|
|
|
+ bizType: tplQuery.bizType === '' ? undefined : tplQuery.bizType,
|
|
|
+ });
|
|
|
+ templates.value = res?.data?.result ?? res?.data ?? [];
|
|
|
+ } finally {
|
|
|
+ tplLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const tplDialogVisible = ref(false);
|
|
|
+const tplIsEdit = ref(false);
|
|
|
+const tplForm = reactive<any>({
|
|
|
+ id: undefined,
|
|
|
+ notifyType: 'NewTask',
|
|
|
+ bizType: '',
|
|
|
+ title: '',
|
|
|
+ content: '',
|
|
|
+ isEnabled: true,
|
|
|
+ remark: '',
|
|
|
+ isSystem: false,
|
|
|
+});
|
|
|
+const tplRules = {
|
|
|
+ notifyType: [{ required: true, message: '必填', trigger: 'change' }],
|
|
|
+ title: [{ required: true, message: '必填', trigger: 'blur' }],
|
|
|
+ content: [{ required: true, message: '必填', trigger: 'blur' }],
|
|
|
+};
|
|
|
+const tplFormRef = ref<FormInstance>();
|
|
|
+
|
|
|
+function openAddTemplate() {
|
|
|
+ tplIsEdit.value = false;
|
|
|
+ Object.assign(tplForm, { id: undefined, notifyType: 'NewTask', bizType: '', title: '', content: '', isEnabled: true, remark: '', isSystem: false });
|
|
|
+ tplDialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function openEditTemplate(row: NotifyTemplate) {
|
|
|
+ tplIsEdit.value = true;
|
|
|
+ Object.assign(tplForm, { ...row });
|
|
|
+ tplDialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSaveTemplate() {
|
|
|
+ await tplFormRef.value?.validate();
|
|
|
+ await saveNotifyTemplate({
|
|
|
+ notifyType: tplForm.notifyType,
|
|
|
+ bizType: tplForm.bizType || '',
|
|
|
+ title: tplForm.title,
|
|
|
+ content: tplForm.content,
|
|
|
+ isEnabled: tplForm.isEnabled,
|
|
|
+ remark: tplForm.remark,
|
|
|
+ });
|
|
|
+ ElMessage.success('已保存');
|
|
|
+ tplDialogVisible.value = false;
|
|
|
+ await loadTemplates();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeTemplate(row: NotifyTemplate) {
|
|
|
+ await ElMessageBox.confirm(`确定删除模板【${row.notifyType} / ${row.bizType || '全局'}】?`, '提示', { type: 'warning' });
|
|
|
+ await deleteNotifyTemplate({ id: row.id! });
|
|
|
+ ElMessage.success('已删除');
|
|
|
+ await loadTemplates();
|
|
|
+}
|
|
|
+
|
|
|
+const variables = ref<any[]>([]);
|
|
|
+const showVariables = ref(false);
|
|
|
+async function loadVariables() {
|
|
|
+ const res: any = await getNotifyTemplateVariables();
|
|
|
+ variables.value = res?.data?.result ?? res?.data ?? [];
|
|
|
+}
|
|
|
+
|
|
|
+// ═══════════════════════ 渠道 Tab ═══════════════════════
|
|
|
+
|
|
|
+const channels = ref<NotifyChannel[]>([]);
|
|
|
+const cfgLoading = ref(false);
|
|
|
+
|
|
|
+async function loadChannels() {
|
|
|
+ cfgLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getNotifyConfigList();
|
|
|
+ channels.value = res?.data?.result ?? res?.data ?? [];
|
|
|
+ } finally {
|
|
|
+ cfgLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const cfgDialogVisible = ref(false);
|
|
|
+const cfgForm = reactive<any>({
|
|
|
+ channelKey: '',
|
|
|
+ enabled: false,
|
|
|
+ webhookUrl: '',
|
|
|
+ secret: '',
|
|
|
+ templateId: '',
|
|
|
+ remark: '',
|
|
|
+});
|
|
|
+const cfgFormRef = ref<FormInstance>();
|
|
|
+
|
|
|
+function openEditChannel(row: NotifyChannel) {
|
|
|
+ Object.assign(cfgForm, {
|
|
|
+ channelKey: row.channelKey,
|
|
|
+ enabled: row.enabled,
|
|
|
+ webhookUrl: row.webhookUrl ?? '',
|
|
|
+ secret: row.secret ?? '',
|
|
|
+ templateId: row.templateId ?? '',
|
|
|
+ remark: row.remark ?? '',
|
|
|
+ });
|
|
|
+ cfgDialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSaveChannel() {
|
|
|
+ await saveNotifyConfig({
|
|
|
+ channelKey: cfgForm.channelKey,
|
|
|
+ enabled: cfgForm.enabled,
|
|
|
+ webhookUrl: cfgForm.webhookUrl || undefined,
|
|
|
+ secret: cfgForm.secret || undefined,
|
|
|
+ templateId: cfgForm.templateId || undefined,
|
|
|
+ remark: cfgForm.remark || undefined,
|
|
|
+ });
|
|
|
+ ElMessage.success('已保存(立即生效)');
|
|
|
+ cfgDialogVisible.value = false;
|
|
|
+ await loadChannels();
|
|
|
+}
|
|
|
+
|
|
|
+async function handleTestSend() {
|
|
|
+ const res: any = await testSendNotify();
|
|
|
+ const ok = res?.data?.result ?? res?.data;
|
|
|
+ if (ok) ElMessage.success('已向您发送一条 SignalR 测试通知');
|
|
|
+ else ElMessage.warning('当前登录会话未在线,未发送(请确认已登录前台并持有活动 SignalR 连接)');
|
|
|
+}
|
|
|
+
|
|
|
+function needWebhook(key: string) {
|
|
|
+ return key === 'DingTalk' || key === 'WorkWeixin';
|
|
|
+}
|
|
|
+
|
|
|
+function channelLabel(key: string) {
|
|
|
+ return (
|
|
|
+ {
|
|
|
+ SignalR: '站内消息',
|
|
|
+ Email: '邮件',
|
|
|
+ Sms: '短信',
|
|
|
+ DingTalk: '钉钉',
|
|
|
+ WorkWeixin: '企业微信',
|
|
|
+ }[key] ?? key
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function channelTagType(key: string): 'primary' | 'success' | 'warning' | 'info' | 'danger' {
|
|
|
+ return (
|
|
|
+ ({
|
|
|
+ SignalR: 'primary',
|
|
|
+ Email: 'success',
|
|
|
+ Sms: 'warning',
|
|
|
+ DingTalk: 'info',
|
|
|
+ WorkWeixin: 'info',
|
|
|
+ } as const)[key] ?? 'info'
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// ── 初始化 ──
|
|
|
+onMounted(async () => {
|
|
|
+ try {
|
|
|
+ const res: any = await getBizTypeList();
|
|
|
+ bizTypes.value = (res?.data?.result ?? res?.data ?? []).map((b: any) => ({ code: b.code, name: b.name }));
|
|
|
+ } catch (e) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ await Promise.all([loadTemplates(), loadChannels(), loadVariables()]);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.notify-config-container {
|
|
|
+ padding: 8px;
|
|
|
+ .tpl-toolbar {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ .cfg-tip {
|
|
|
+ .hint {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|