|
|
@@ -1,45 +1,278 @@
|
|
|
+<template>
|
|
|
+ <AidopDemoShell title="通知分层配置" subtitle="S8 / 配置 / 通知分层">
|
|
|
+ <div class="toolbar">
|
|
|
+ <el-button type="primary" @click="openCreate">新增</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table :data="rows" border stripe v-loading="loading" style="width: 100%; margin-top: 12px">
|
|
|
+ <el-table-column prop="sceneCode" label="场景编码" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span>{{ row.sceneCode }}</span>
|
|
|
+ <el-tag v-if="row.sceneCode === 'S8_DEMO_DEFAULT'" type="info" size="small" style="margin-left: 6px">DEMO</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="severity" label="严重度" width="100">
|
|
|
+ <template #default="{ row }">{{ severityLabel(row.severity) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="levelCode" label="层级" width="130">
|
|
|
+ <template #default="{ row }">{{ levelLabel(row.levelCode) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="targetRoleIds" label="目标角色" min-width="160">
|
|
|
+ <template #default="{ row }">{{ formatRoleIds(row.targetRoleIds) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="notifyChannel" label="通知渠道" min-width="160">
|
|
|
+ <template #default="{ row }">{{ row.notifyChannel }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="updatedAt" label="更新时间" width="170">
|
|
|
+ <template #default="{ row }">{{ row.updatedAt ?? row.createdAt }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="130" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="small" @click="openEdit(row)">编辑</el-button>
|
|
|
+ <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- Dialog -->
|
|
|
+ <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑通知分层' : '新增通知分层'" width="560px" destroy-on-close>
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
|
+ <el-form-item label="场景编码" prop="sceneCode">
|
|
|
+ <el-select
|
|
|
+ v-model="form.sceneCode"
|
|
|
+ filterable
|
|
|
+ allow-create
|
|
|
+ placeholder="选择或输入场景编码"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option v-for="opt in sceneOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="严重度" prop="severity">
|
|
|
+ <el-select v-model="form.severity" style="width: 100%">
|
|
|
+ <el-option v-for="opt in severityOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="层级" prop="levelCode">
|
|
|
+ <el-select v-model="form.levelCode" style="width: 100%">
|
|
|
+ <el-option v-for="opt in levelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="目标角色" prop="targetRoleIdList">
|
|
|
+ <el-select v-model="form.targetRoleIdList" multiple placeholder="选择角色" style="width: 100%" v-loading="rolesLoading">
|
|
|
+ <el-option v-for="opt in roleOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="通知渠道" prop="notifyChannelList">
|
|
|
+ <el-checkbox-group v-model="form.notifyChannelList">
|
|
|
+ <el-checkbox v-for="opt in channelOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
|
|
+ 渠道启停受全局通知配置控制(ApprovalFlowNotifyConfig)
|
|
|
+ </span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </AidopDemoShell>
|
|
|
+</template>
|
|
|
+
|
|
|
<script setup lang="ts" name="aidopS8NotificationLayerConfig">
|
|
|
-import S8CrudConfigPage from '../components/config/S8CrudConfigPage.vue';
|
|
|
+import { ref, reactive, onMounted } from 'vue';
|
|
|
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
|
|
+import AidopDemoShell from '../../components/AidopDemoShell.vue';
|
|
|
import { s8ConfigApi } from '../api/s8ConfigApi';
|
|
|
+import service from '/@/utils/request';
|
|
|
+
|
|
|
+// ── static option sets ──────────────────────────────────────────────────────
|
|
|
|
|
|
-const columns = [
|
|
|
- { key: 'sceneCode', label: '场景编码', width: 140 },
|
|
|
- { key: 'severity', label: '严重度', width: 120 },
|
|
|
- { key: 'levelCode', label: '层级', width: 120 },
|
|
|
- { key: 'notifyChannel', label: '通知渠道', width: 120 },
|
|
|
- { key: 'targetRoleIds', label: '目标角色' },
|
|
|
+const severityOptions = [
|
|
|
+ { value: 'LOW', label: '低' },
|
|
|
+ { value: 'MEDIUM', label: '中' },
|
|
|
+ { value: 'HIGH', label: '高' },
|
|
|
+ { value: 'CRITICAL', label: '严重' },
|
|
|
];
|
|
|
|
|
|
-const fields = [
|
|
|
- { key: 'sceneCode', label: '场景编码', type: 'select', required: true, optionsKey: 'scenes' },
|
|
|
- { key: 'severity', label: '严重度', type: 'select', required: true, optionsKey: 'severities' },
|
|
|
- { key: 'levelCode', label: '层级', type: 'input', required: true },
|
|
|
- { key: 'notifyChannel', label: '通知渠道', type: 'input' },
|
|
|
- { key: 'targetRoleIds', label: '目标角色', type: 'textarea' },
|
|
|
-] as const;
|
|
|
+const levelOptions = [
|
|
|
+ { value: 'L1_OPERATOR', label: 'L1 操作员' },
|
|
|
+ { value: 'L2_MANAGER', label: 'L2 主管' },
|
|
|
+ { value: 'L3_DIRECTOR', label: 'L3 总监' },
|
|
|
+];
|
|
|
+
|
|
|
+const channelOptions = [
|
|
|
+ { value: 'SignalR', label: 'SignalR' },
|
|
|
+ { value: 'Email', label: 'Email' },
|
|
|
+ { value: 'Sms', label: 'Sms' },
|
|
|
+ { value: 'DingTalk', label: 'DingTalk' },
|
|
|
+ { value: 'WorkWeixin', label: 'WorkWeixin' },
|
|
|
+];
|
|
|
+
|
|
|
+// ── state ───────────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+const rolesLoading = ref(false);
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const isEdit = ref(false);
|
|
|
+const editId = ref<number>(0);
|
|
|
|
|
|
-const buildDefault = () => ({
|
|
|
+const rows = ref<any[]>([]);
|
|
|
+const sceneOptions = ref<Array<{ value: string; label: string }>>([]);
|
|
|
+const roleOptions = ref<Array<{ value: string; label: string }>>([]);
|
|
|
+
|
|
|
+const formRef = ref<FormInstance>();
|
|
|
+
|
|
|
+const defaultForm = () => ({
|
|
|
sceneCode: '',
|
|
|
severity: 'MEDIUM',
|
|
|
- levelCode: '',
|
|
|
- notifyChannel: 'log',
|
|
|
- targetRoleIds: '',
|
|
|
+ levelCode: 'L1_OPERATOR',
|
|
|
+ targetRoleIdList: [] as string[],
|
|
|
+ notifyChannelList: [] as string[],
|
|
|
});
|
|
|
|
|
|
-async function loadOptions() {
|
|
|
- const [scenes, severities] = await Promise.all([s8ConfigApi.scenes({ tenantId: 1, factoryId: 1 }), s8ConfigApi.severities()]);
|
|
|
- return { scenes: scenes.map((item: any) => ({ label: item.sceneName, value: item.sceneCode })), severities };
|
|
|
+const form = reactive(defaultForm());
|
|
|
+
|
|
|
+const rules: FormRules = {
|
|
|
+ sceneCode: [{ required: true, message: '场景编码必填', trigger: 'blur' }],
|
|
|
+ severity: [{ required: true, message: '严重度必填', trigger: 'change' }],
|
|
|
+ levelCode: [{ required: true, message: '层级必填', trigger: 'change' }],
|
|
|
+ targetRoleIdList: [{ required: true, type: 'array', min: 1, message: '目标角色必填', trigger: 'change' }],
|
|
|
+ notifyChannelList: [{ required: true, type: 'array', min: 1, message: '通知渠道至少选一个', trigger: 'change' }],
|
|
|
+};
|
|
|
+
|
|
|
+// ── helpers ──────────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+function severityLabel(val: string): string {
|
|
|
+ return severityOptions.find((o) => o.value === val)?.label ?? val;
|
|
|
+}
|
|
|
+
|
|
|
+function levelLabel(val: string): string {
|
|
|
+ return levelOptions.find((o) => o.value === val)?.label ?? val;
|
|
|
+}
|
|
|
+
|
|
|
+function formatRoleIds(csv: string | null | undefined): string {
|
|
|
+ if (!csv) return '';
|
|
|
+ return csv
|
|
|
+ .split(',')
|
|
|
+ .filter(Boolean)
|
|
|
+ .map((v) => roleOptions.value.find((o) => o.value === v)?.label ?? v)
|
|
|
+ .join(', ');
|
|
|
+}
|
|
|
+
|
|
|
+// ── data loading ─────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+async function loadList() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const data = await service.get('/api/aidop/s8/config/notification-layers', { params: { tenantId: 1, factoryId: 1 } }).then((r: any) => r.data);
|
|
|
+ rows.value = Array.isArray(data) ? data : [];
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadScenes() {
|
|
|
+ try {
|
|
|
+ const data: any[] = await s8ConfigApi.scenes({ tenantId: 1, factoryId: 1 });
|
|
|
+ sceneOptions.value = data.map((item: any) => ({ value: item.sceneCode, label: `${item.sceneCode}(${item.sceneName ?? ''})` }));
|
|
|
+ } catch {
|
|
|
+ sceneOptions.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadRoleOptions() {
|
|
|
+ rolesLoading.value = true;
|
|
|
+ try {
|
|
|
+ const data = await s8ConfigApi.notificationLayers.roleOptions();
|
|
|
+ roleOptions.value = Array.isArray(data) ? data : [];
|
|
|
+ } catch {
|
|
|
+ roleOptions.value = [];
|
|
|
+ } finally {
|
|
|
+ rolesLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadList();
|
|
|
+ loadScenes();
|
|
|
+ loadRoleOptions();
|
|
|
+});
|
|
|
+
|
|
|
+// ── dialog ───────────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+function openCreate() {
|
|
|
+ isEdit.value = false;
|
|
|
+ editId.value = 0;
|
|
|
+ Object.assign(form, defaultForm());
|
|
|
+ dialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function openEdit(row: any) {
|
|
|
+ isEdit.value = true;
|
|
|
+ editId.value = row.id;
|
|
|
+ Object.assign(form, {
|
|
|
+ sceneCode: row.sceneCode ?? '',
|
|
|
+ severity: row.severity ?? 'MEDIUM',
|
|
|
+ levelCode: row.levelCode ?? 'L1_OPERATOR',
|
|
|
+ targetRoleIdList: row.targetRoleIds ? row.targetRoleIds.split(',').filter(Boolean) : [],
|
|
|
+ notifyChannelList: row.notifyChannel ? row.notifyChannel.split(',').filter(Boolean) : [],
|
|
|
+ });
|
|
|
+ dialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSave() {
|
|
|
+ if (!formRef.value) return;
|
|
|
+ const valid = await formRef.value.validate().catch(() => false);
|
|
|
+ if (!valid) return;
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ tenantId: 1,
|
|
|
+ factoryId: 1,
|
|
|
+ sceneCode: form.sceneCode,
|
|
|
+ severity: form.severity,
|
|
|
+ levelCode: form.levelCode,
|
|
|
+ targetRoleIds: form.targetRoleIdList.join(','),
|
|
|
+ notifyChannel: form.notifyChannelList.join(','),
|
|
|
+ };
|
|
|
+
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ if (isEdit.value) {
|
|
|
+ await service.put(`/api/aidop/s8/config/notification-layers/${editId.value}`, payload);
|
|
|
+ } else {
|
|
|
+ await service.post('/api/aidop/s8/config/notification-layers', payload);
|
|
|
+ }
|
|
|
+ ElMessage.success('保存成功');
|
|
|
+ dialogVisible.value = false;
|
|
|
+ loadList();
|
|
|
+ } catch (err: any) {
|
|
|
+ const msg = err?.response?.data?.message ?? err?.message ?? '保存失败';
|
|
|
+ ElMessage.error(msg);
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(row: any) {
|
|
|
+ await ElMessageBox.confirm(`确认删除 ${row.sceneCode} / ${row.severity} / ${row.levelCode}?`, '删除确认', { type: 'warning' });
|
|
|
+ try {
|
|
|
+ await service.delete(`/api/aidop/s8/config/notification-layers/${row.id}`);
|
|
|
+ ElMessage.success('已删除');
|
|
|
+ loadList();
|
|
|
+ } catch (err: any) {
|
|
|
+ ElMessage.error(err?.message ?? '删除失败');
|
|
|
+ }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
-<template>
|
|
|
- <S8CrudConfigPage
|
|
|
- title="通知分层配置"
|
|
|
- subtitle="S8 / 配置 / 通知分层"
|
|
|
- endpoint="/api/aidop/s8/config/notification-layers"
|
|
|
- :columns="columns"
|
|
|
- :fields="fields"
|
|
|
- :build-default="buildDefault"
|
|
|
- :load-options="loadOptions"
|
|
|
- />
|
|
|
-</template>
|
|
|
+<style scoped>
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+</style>
|