index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template>
  2. <div class="app-container">
  3. <el-row :gutter="16">
  4. <!-- 左侧部门树 -->
  5. <el-col :span="5">
  6. <el-card>
  7. <template #header>
  8. <div class="card-header">
  9. <span>部门结构</span>
  10. <el-button type="primary" size="small" @click="handleAddDept">新增部门</el-button>
  11. </div>
  12. </template>
  13. <el-tree
  14. ref="deptTreeRef"
  15. :data="deptTree"
  16. :props="{ label: 'name', children: 'children' }"
  17. node-key="id"
  18. highlight-current
  19. :default-expanded-keys="[0]"
  20. @node-click="handleDeptClick"
  21. >
  22. <template #default="{ node, data }">
  23. <span class="custom-tree-node">
  24. <span>{{ node.label }}</span>
  25. <span v-if="data.id !== 0" class="tree-actions">
  26. <el-button link type="primary" size="small" @click.stop="handleEditDept(data)">编辑</el-button>
  27. <el-button link type="danger" size="small" @click.stop="handleDeleteDept(data)">删除</el-button>
  28. </span>
  29. </span>
  30. </template>
  31. </el-tree>
  32. </el-card>
  33. </el-col>
  34. <!-- 右侧审批角色列表 -->
  35. <el-col :span="19">
  36. <el-card>
  37. <template #header>
  38. <div class="card-header">
  39. <span>审批角色列表</span>
  40. <el-button type="primary" @click="handleAdd">新增角色</el-button>
  41. </div>
  42. </template>
  43. <el-form :inline="true" :model="queryParams" class="search-form">
  44. <el-form-item label="角色编码">
  45. <el-input v-model="queryParams.roleCode" placeholder="请输入角色编码" clearable />
  46. </el-form-item>
  47. <el-form-item label="角色名称">
  48. <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable />
  49. </el-form-item>
  50. <el-form-item>
  51. <el-button type="primary" @click="handleQuery">查询</el-button>
  52. <el-button @click="resetQuery">重置</el-button>
  53. </el-form-item>
  54. </el-form>
  55. <el-table v-loading="loading" :data="tableData" border>
  56. <el-table-column type="index" label="序号" width="60" />
  57. <el-table-column prop="roleCode" label="角色编码" />
  58. <el-table-column prop="roleName" label="角色名称" />
  59. <el-table-column prop="username" label="登录账号" />
  60. <el-table-column prop="phone" label="手机号">
  61. <template #default="{ row }">
  62. <span v-if="row.phone">{{ row.phone }}</span>
  63. <span v-else class="text-gray">未填写</span>
  64. </template>
  65. </el-table-column>
  66. <el-table-column prop="deptName" label="所属部门" />
  67. <el-table-column prop="roleScope" label="描述" />
  68. <el-table-column prop="wecomUserId" label="企微账号" width="120">
  69. <template #default="{ row }">
  70. <el-tag v-if="row.wecomUserId" type="success" size="small">{{ row.wecomUserId }}</el-tag>
  71. <span v-else class="text-gray">未绑定</span>
  72. </template>
  73. </el-table-column>
  74. <el-table-column prop="status" label="状态" width="100">
  75. <template #default="{ row }">
  76. <el-tag :type="row.status === 1 ? 'success' : 'danger'">
  77. {{ row.status === 1 ? '正常' : '禁用' }}
  78. </el-tag>
  79. </template>
  80. </el-table-column>
  81. <el-table-column prop="createTime" label="创建时间" />
  82. <el-table-column label="操作" width="220">
  83. <template #default="{ row }">
  84. <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
  85. <el-button link type="primary" @click="handleWeComConfig(row)">企微配置</el-button>
  86. <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
  87. </template>
  88. </el-table-column>
  89. </el-table>
  90. <el-pagination
  91. v-model:current-page="queryParams.pageNum"
  92. v-model:page-size="queryParams.pageSize"
  93. :total="total"
  94. :page-sizes="[10, 20, 50]"
  95. layout="total, sizes, prev, pager, next, jumper"
  96. class="pagination"
  97. @current-change="handlePageChange"
  98. @size-change="handleSizeChange"
  99. />
  100. </el-card>
  101. </el-col>
  102. </el-row>
  103. <!-- 角色弹窗 -->
  104. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
  105. <el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
  106. <el-form-item label="角色编码" prop="roleCode">
  107. <el-input v-model="form.roleCode" :disabled="isEdit" />
  108. </el-form-item>
  109. <el-form-item label="角色名称" prop="roleName">
  110. <el-input v-model="form.roleName" />
  111. </el-form-item>
  112. <el-form-item label="登录账号" prop="username">
  113. <el-input v-model="form.username" placeholder="角色登录账号" />
  114. </el-form-item>
  115. <el-form-item label="手机号" prop="phone">
  116. <el-input v-model="form.phone" placeholder="请输入手机号" />
  117. </el-form-item>
  118. <el-form-item label="密码" prop="password">
  119. <el-input v-model="form.password" type="password" show-password :placeholder="isEdit ? '不填表示不修改密码' : '请输入密码'" />
  120. </el-form-item>
  121. <el-form-item label="所属部门" prop="deptId">
  122. <el-tree-select
  123. v-model="form.deptId"
  124. :data="deptTreeForSelect"
  125. :props="{ label: 'name', value: 'id', children: 'children' }"
  126. check-strictly
  127. clearable
  128. placeholder="请选择所属部门"
  129. style="width: 100%"
  130. />
  131. </el-form-item>
  132. <el-form-item label="描述">
  133. <el-input v-model="form.roleScope" type="textarea" />
  134. </el-form-item>
  135. <el-form-item label="状态">
  136. <el-radio-group v-model="form.status">
  137. <el-radio :label="1">正常</el-radio>
  138. <el-radio :label="0">禁用</el-radio>
  139. </el-radio-group>
  140. </el-form-item>
  141. </el-form>
  142. <template #footer>
  143. <el-button @click="dialogVisible = false">取消</el-button>
  144. <el-button type="primary" @click="submitForm">确认</el-button>
  145. </template>
  146. </el-dialog>
  147. <!-- 企微配置抽屉 -->
  148. <el-drawer v-model="wecomDrawerVisible" title="企微提醒配置" direction="rtl" size="400px">
  149. <el-form :model="wecomForm" label-width="100px">
  150. <el-alert
  151. title="提示:实际企微消息推送由外部对接系统完成,本系统仅保存配置和接收人映射。"
  152. type="info"
  153. :closable="false"
  154. style="margin-bottom: 20px;"
  155. />
  156. <el-form-item label="角色名称">
  157. <el-input v-model="wecomForm.roleName" disabled />
  158. </el-form-item>
  159. <el-form-item label="企微账号">
  160. <el-input v-model="wecomForm.wecomUserId" placeholder="请输入企业微信用户ID" />
  161. </el-form-item>
  162. <el-form-item label="开启提醒">
  163. <el-switch
  164. v-model="wecomForm.wecomRemindEnabled"
  165. :active-value="1"
  166. :inactive-value="0"
  167. active-text="开启"
  168. inactive-text="关闭"
  169. />
  170. </el-form-item>
  171. </el-form>
  172. <template #footer>
  173. <div style="flex: auto">
  174. <el-button @click="wecomDrawerVisible = false">取消</el-button>
  175. <el-button type="primary" :loading="wecomSubmitting" @click="submitWeComConfig">保存</el-button>
  176. </div>
  177. </template>
  178. </el-drawer>
  179. <!-- 部门弹窗 -->
  180. <el-dialog v-model="deptDialogVisible" :title="deptDialogTitle" width="500px">
  181. <el-form ref="deptFormRef" :model="deptForm" :rules="deptFormRules" label-width="100px">
  182. <el-form-item label="上级部门">
  183. <el-tree-select
  184. v-model="deptForm.parentId"
  185. :data="deptTreeForSelect"
  186. :props="{ label: 'name', value: 'id', children: 'children' }"
  187. check-strictly
  188. clearable
  189. placeholder="请选择上级部门"
  190. style="width: 100%"
  191. />
  192. </el-form-item>
  193. <el-form-item label="部门名称" prop="name">
  194. <el-input v-model="deptForm.name" />
  195. </el-form-item>
  196. <el-form-item label="部门编码">
  197. <el-input v-model="deptForm.deptCode" />
  198. </el-form-item>
  199. <el-form-item label="排序">
  200. <el-input-number v-model="deptForm.sort" :min="0" />
  201. </el-form-item>
  202. <el-form-item label="状态">
  203. <el-radio-group v-model="deptForm.status">
  204. <el-radio :label="1">正常</el-radio>
  205. <el-radio :label="0">禁用</el-radio>
  206. </el-radio-group>
  207. </el-form-item>
  208. </el-form>
  209. <template #footer>
  210. <el-button @click="deptDialogVisible = false">取消</el-button>
  211. <el-button type="primary" @click="submitDeptForm">确认</el-button>
  212. </template>
  213. </el-dialog>
  214. </div>
  215. </template>
  216. <script setup lang="ts">
  217. import { ref, shallowRef, reactive, onMounted, computed } from 'vue'
  218. import { ElMessage, ElMessageBox } from 'element-plus'
  219. import type { FormInstance, FormRules } from 'element-plus'
  220. import { listRole, addRole, updateRole, deleteRole, bindRoleWeCom } from '@/api/system/role'
  221. import { listDept, addDept, updateDept, deleteDept } from '@/api/system/dept'
  222. import type { Role, Dept } from '@/types/system'
  223. const loading = ref(false)
  224. const tableData = ref<Role[]>([])
  225. const total = ref(0)
  226. const deptList = ref<Dept[]>([])
  227. const selectedDeptId = ref<number | undefined>(undefined)
  228. const queryParams = reactive({
  229. pageNum: 1,
  230. pageSize: 10,
  231. roleCode: undefined as string | undefined,
  232. roleName: undefined as string | undefined,
  233. deptId: undefined as number | undefined
  234. })
  235. // 部门树(后端已返回嵌套结构,直接使用)
  236. const deptTree = computed(() => {
  237. const list = deptList.value
  238. // 如果有多个顶级部门,统一挂在虚拟根节点"总公司"下
  239. if (list.length > 1) {
  240. return [{ id: 0, parentId: 0, name: '总公司', children: list, sort: 0, status: 1 }]
  241. }
  242. return list
  243. })
  244. const deptTreeForSelect = shallowRef<Dept[]>([])
  245. // 角色弹窗
  246. const dialogVisible = ref(false)
  247. const dialogTitle = ref('')
  248. const isEdit = ref(false)
  249. const formRef = ref<FormInstance>()
  250. const form = reactive<Partial<Role>>({
  251. id: undefined,
  252. roleCode: '',
  253. roleName: '',
  254. username: '',
  255. phone: '',
  256. password: '',
  257. roleScope: '',
  258. deptId: undefined,
  259. status: 1
  260. })
  261. const formRules: FormRules = {
  262. roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
  263. roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
  264. phone: [{
  265. pattern: /^1[3-9]\d{9}$/,
  266. message: '请输入正确的手机号',
  267. trigger: 'blur'
  268. }]
  269. }
  270. // 部门弹窗
  271. const wecomDrawerVisible = ref(false)
  272. const wecomSubmitting = ref(false)
  273. const wecomForm = reactive({
  274. roleId: 0,
  275. roleName: '',
  276. wecomUserId: '',
  277. wecomRemindEnabled: 1
  278. })
  279. const deptDialogVisible = ref(false)
  280. const deptDialogTitle = ref('')
  281. const isEditDept = ref(false)
  282. const deptFormRef = ref<FormInstance>()
  283. const deptForm = reactive<Partial<Dept>>({
  284. id: undefined,
  285. parentId: 0,
  286. name: '',
  287. deptCode: '',
  288. sort: 0,
  289. status: 1
  290. })
  291. const deptFormRules: FormRules = {
  292. name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }]
  293. }
  294. async function loadDepts() {
  295. try {
  296. const res = await listDept()
  297. deptList.value = res
  298. deptTreeForSelect.value = [{ id: 0, parentId: 0, name: '根部门', sort: 0, status: 1 }, ...res]
  299. } catch (error: any) {
  300. ElMessage.error(error?.message || '加载部门失败')
  301. }
  302. }
  303. async function loadData() {
  304. loading.value = true
  305. try {
  306. const params = { ...queryParams }
  307. if (selectedDeptId.value) {
  308. params.deptId = selectedDeptId.value
  309. }
  310. const res = await listRole(params)
  311. tableData.value = res.list
  312. total.value = res.total
  313. } finally {
  314. loading.value = false
  315. }
  316. }
  317. function handleQuery() {
  318. queryParams.pageNum = 1
  319. loadData()
  320. }
  321. function handlePageChange() {
  322. loadData()
  323. }
  324. function handleSizeChange() {
  325. queryParams.pageNum = 1
  326. loadData()
  327. }
  328. function resetQuery() {
  329. queryParams.roleCode = undefined
  330. queryParams.roleName = undefined
  331. queryParams.pageNum = 1
  332. loadData()
  333. }
  334. function handleDeptClick(data: Dept) {
  335. selectedDeptId.value = data.id
  336. queryParams.pageNum = 1
  337. loadData()
  338. }
  339. function handleAdd() {
  340. isEdit.value = false
  341. dialogTitle.value = '新增角色'
  342. Object.assign(form, {
  343. id: undefined,
  344. roleCode: '',
  345. roleName: '',
  346. username: '',
  347. phone: '',
  348. password: '',
  349. roleScope: '',
  350. deptId: selectedDeptId.value,
  351. status: 1
  352. })
  353. dialogVisible.value = true
  354. }
  355. function handleEdit(row: Role) {
  356. isEdit.value = true
  357. dialogTitle.value = '编辑角色'
  358. Object.assign(form, {
  359. ...row,
  360. phone: row.phone || ''
  361. })
  362. dialogVisible.value = true
  363. }
  364. async function handleDelete(row: Role) {
  365. try {
  366. await ElMessageBox.confirm(`确认删除角色 "${row.roleName}" 吗?`, '提示', { type: 'warning' })
  367. await deleteRole(row.id)
  368. ElMessage.success('删除成功')
  369. loadData()
  370. } catch {
  371. // cancel
  372. }
  373. }
  374. function handleWeComConfig(row: Role) {
  375. wecomForm.roleId = row.id
  376. wecomForm.roleName = row.roleName
  377. wecomForm.wecomUserId = row.wecomUserId || ''
  378. wecomForm.wecomRemindEnabled = row.wecomRemindEnabled ?? 1
  379. wecomDrawerVisible.value = true
  380. }
  381. async function submitWeComConfig() {
  382. wecomSubmitting.value = true
  383. try {
  384. await bindRoleWeCom(wecomForm.roleId, {
  385. wecomUserId: wecomForm.wecomUserId,
  386. wecomRemindEnabled: wecomForm.wecomRemindEnabled
  387. })
  388. ElMessage.success('保存成功')
  389. wecomDrawerVisible.value = false
  390. loadData()
  391. } finally {
  392. wecomSubmitting.value = false
  393. }
  394. }
  395. async function submitForm() {
  396. const valid = await formRef.value?.validate().catch(() => false)
  397. if (!valid) return
  398. if (isEdit.value) {
  399. await updateRole(form)
  400. ElMessage.success('修改成功')
  401. } else {
  402. await addRole(form)
  403. ElMessage.success('新增成功')
  404. }
  405. dialogVisible.value = false
  406. loadData()
  407. }
  408. function handleEditDept(data: Dept) {
  409. isEditDept.value = true
  410. deptDialogTitle.value = '编辑部门'
  411. Object.assign(deptForm, {
  412. id: data.id,
  413. parentId: data.parentId ?? 0,
  414. name: data.name,
  415. deptCode: data.deptCode,
  416. sort: data.sort,
  417. status: data.status
  418. })
  419. deptDialogVisible.value = true
  420. }
  421. async function handleDeleteDept(data: Dept) {
  422. try {
  423. await ElMessageBox.confirm(`确认删除部门 "${data.name}" 吗?`, '提示', { type: 'warning' })
  424. await deleteDept(data.id)
  425. ElMessage.success('删除成功')
  426. loadDepts()
  427. } catch {
  428. // cancel
  429. }
  430. }
  431. // 部门管理
  432. function handleAddDept() {
  433. isEditDept.value = false
  434. deptDialogTitle.value = '新增部门'
  435. Object.assign(deptForm, {
  436. id: undefined, parentId: 0, name: '', deptCode: '', sort: 0, status: 1
  437. })
  438. deptDialogVisible.value = true
  439. }
  440. async function submitDeptForm() {
  441. const valid = await deptFormRef.value?.validate().catch(() => false)
  442. if (!valid) return
  443. if (isEditDept.value) {
  444. await updateDept(deptForm)
  445. ElMessage.success('修改成功')
  446. } else {
  447. await addDept(deptForm)
  448. ElMessage.success('新增成功')
  449. }
  450. deptDialogVisible.value = false
  451. loadDepts()
  452. }
  453. onMounted(() => {
  454. Promise.all([loadDepts(), loadData()])
  455. })
  456. </script>
  457. <style scoped>
  458. .card-header {
  459. display: flex;
  460. justify-content: space-between;
  461. align-items: center;
  462. }
  463. .search-form {
  464. margin-bottom: 20px;
  465. }
  466. .pagination {
  467. margin-top: 20px;
  468. justify-content: flex-end;
  469. }
  470. .custom-tree-node {
  471. flex: 1;
  472. display: flex;
  473. align-items: center;
  474. justify-content: space-between;
  475. font-size: 14px;
  476. padding-right: 8px;
  477. }
  478. .tree-actions {
  479. display: none;
  480. }
  481. .custom-tree-node:hover .tree-actions {
  482. display: inline;
  483. }
  484. .text-gray {
  485. color: #909399;
  486. }
  487. </style>