longz
2026-04-28 bfa7d3b23e526809b04299d9c2719c3449aba3be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
<template>
  <div class="page">
    <h3 class="section-title">审核节点配置</h3>
    <el-table :data="nodeRows" v-loading="nodesLoading" stripe border class="node-table">
      <el-table-column prop="nodeLabel" label="节点" min-width="160" show-overflow-tooltip />
      <el-table-column prop="nodeDesc" label="审批节点说明" min-width="280" show-overflow-tooltip />
      <el-table-column label="是否开启" width="120" align="center">
        <template #default="{ row }">
          <el-switch
            v-permission="'config:approval-settings:update'"
            :model-value="row.enabled === 1"
            :loading="!!row._saving"
            @change="(v: boolean) => onToggleEnabled(row, v)"
          />
        </template>
      </el-table-column>
    </el-table>
    <p class="hint">
      节点关闭后新的预约单将该节点隐藏无需审批(开启关闭不影响历史预约)
    </p>
 
    <template v-if="visitDeptNodeEnabled">
      <h3 class="section-title section-title-spaced">访客主管部门配置</h3>
      <el-form label-width="120px" class="dept-form">
        <el-form-item label="主管部门" required>
          <el-tree-select
            v-model="competentDeptId"
            :data="deptTree"
            :props="{ value: 'id', label: 'label', children: 'children' }"
            check-strictly
            :render-after-expand="false"
            placeholder="请选择部门"
            clearable
            filterable
            style="width: 320px"
          />
        </el-form-item>
        <el-form-item>
          <el-button v-permission="'config:approval-settings:update'" type="primary" :loading="savingDept" @click="saveCompetentDept">保存</el-button>
        </el-form-item>
      </el-form>
    </template>
  </div>
</template>
 
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import http from "../api/http";
 
const UI_NODE_CODES = [
  "LEVEL2_DEPT_LEADER",
  "LEVEL3_LOCAL_DEPT_LEADER",
  "LEVEL4_VISIT_DEPT",
  "LEVEL5_VISIT_DEPT_LEADER",
] as const;
 
const NODE_COPY: Record<string, { node: string; desc: string }> = {
  LEVEL2_DEPT_LEADER: {
    node: "部门负责人审批",
    desc: "由预约的通行部门/区域的负责人进行审批",
  },
  LEVEL3_LOCAL_DEPT_LEADER: {
    node: "属地管理部门领导审批",
    desc: "当来访涉及访问属地单位时,由属地管理部门领导进行审批",
  },
  LEVEL4_VISIT_DEPT: {
    node: "访客主管部门",
    desc: "指定一个部门为主管部门进行审批",
  },
  LEVEL5_VISIT_DEPT_LEADER: {
    node: "访客主管部门领导审批",
    desc: "当来访日期为国定假期/节假日时,由主管部门的部门领导进行审批",
  },
};
 
type ApiNode = {
  id: number;
  uuid: string;
  nodeCode: string;
  nodeName?: string;
  enabled: number;
  sortOrder?: number;
};
 
type NodeRow = ApiNode & {
  nodeLabel: string;
  nodeDesc: string;
  _saving?: boolean;
};
 
const nodesLoading = ref(false);
const rawNodes = ref<ApiNode[]>([]);
 
const nodeRows = computed(() => {
  const order = new Map(UI_NODE_CODES.map((c, i) => [c, i]));
  const list = rawNodes.value
    .filter((n) => order.has(n.nodeCode as (typeof UI_NODE_CODES)[number]))
    .sort((a, b) => (order.get(a.nodeCode) ?? 0) - (order.get(b.nodeCode) ?? 0));
  return list.map((n) => {
    const ui = NODE_COPY[n.nodeCode] || { node: n.nodeName || n.nodeCode, desc: "—" };
    return {
      ...n,
      nodeLabel: ui.node,
      nodeDesc: ui.desc,
    } as NodeRow;
  });
});
 
const visitDeptNodeEnabled = computed(() =>
  rawNodes.value.some(
    (n) =>
      (n.nodeCode === "LEVEL4_VISIT_DEPT" || n.nodeCode === "LEVEL5_VISIT_DEPT_LEADER") &&
      n.enabled === 1,
  ),
);
 
async function loadNodes() {
  nodesLoading.value = true;
  try {
    const { data } = await http.get("/config/approval-nodes");
    rawNodes.value = (data.result || []) as ApiNode[];
  } catch (e: unknown) {
    ElMessage.error(e instanceof Error ? e.message : "加载节点失败");
  } finally {
    nodesLoading.value = false;
  }
}
 
async function onToggleEnabled(row: NodeRow, val: boolean) {
  const prev = row.enabled;
  row._saving = true;
  try {
    const nextEnabled = val ? 1 : 0;
    await http.patch(`/config/approval-nodes/${row.id}/enabled`, {
      uuid: row.uuid,
      enabled: nextEnabled,
    });
    row.enabled = nextEnabled;
    const target = rawNodes.value.find((n) => n.id === row.id);
    if (target) target.enabled = nextEnabled;
    ElMessage.success("已更新");
  } catch (e: unknown) {
    row.enabled = prev;
    ElMessage.error(e instanceof Error ? e.message : "更新失败");
  } finally {
    row._saving = false;
  }
}
 
type DeptTree = { id: number; label: string; children?: DeptTree[] };
 
const deptTree = ref<DeptTree[]>([]);
const competentDeptId = ref<number | undefined>(undefined);
const savingDept = ref(false);
 
function mapDept(nodes: Record<string, unknown>[]): DeptTree[] {
  return nodes.map((n) => ({
    id: n.id as number,
    label: (n.status === 0 ? `${n.deptName}(禁用)` : String(n.deptName)) as string,
    children: n.children ? mapDept(n.children as Record<string, unknown>[]) : undefined,
  }));
}
 
async function loadDeptTree() {
  const { data } = await http.get("/sys/dept-manage/tree");
  const raw = (data.result || []) as Record<string, unknown>[];
  deptTree.value = mapDept(raw);
}
 
async function loadCompetentDept() {
  try {
    const { data } = await http.get("/config/visitor-competent-dept");
    const id = data.result?.deptId as number | null | undefined;
    competentDeptId.value = id != null && id > 0 ? id : undefined;
  } catch {
    competentDeptId.value = undefined;
  }
}
 
async function saveCompetentDept() {
  if (visitDeptNodeEnabled.value && !competentDeptId.value) {
    ElMessage.error("请选择主管部门");
    return;
  }
  savingDept.value = true;
  try {
    await http.put("/config/visitor-competent-dept", {
      deptId: competentDeptId.value ?? null,
    });
    ElMessage.success("已保存");
  } catch (e: unknown) {
    ElMessage.error(e instanceof Error ? e.message : "保存失败");
  } finally {
    savingDept.value = false;
  }
}
 
onMounted(async () => {
  await loadNodes();
  try {
    await loadDeptTree();
  } catch {
    /* ignore */
  }
  await loadCompetentDept();
});
</script>
 
<style scoped>
.page {
  padding: 0;
}
.section-title {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 12px;
  padding-left: 10px;
  border-left: 3px solid #409eff;
  color: #303133;
}
.section-title-spaced {
  margin-top: 28px;
}
.node-table {
  max-width: 960px;
}
.hint {
  margin: 10px 0 0;
  font-size: 13px;
  color: #909399;
  line-height: 1.5;
  max-width: 960px;
}
.dept-form {
  max-width: 560px;
  margin-top: 8px;
}
</style>