index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div>
  3. <Breadcrumb />
  4. <TableCard
  5. v-model:page-no="queryParams.pageNo"
  6. v-model:page-size="queryParams.pageSize"
  7. :data="filteredData"
  8. :columns="columns"
  9. :total="total"
  10. @change="handlePageChange"
  11. @selection-change="handleSelectionChange"
  12. >
  13. <template #toolbar-left>
  14. <el-button v-permission="'sys:role:add'" :icon="CirclePlus" size="large" @click="handleAdd">
  15. 新增
  16. </el-button>
  17. <el-button
  18. v-permission="'sys:role:delete'"
  19. :icon="Delete"
  20. size="large"
  21. :disabled="selectedRows.length === 0"
  22. @click="handleBatchDelete"
  23. >
  24. 删除
  25. </el-button>
  26. <el-button :icon="Refresh" size="large" @click="handleRefresh"> 刷新 </el-button>
  27. </template>
  28. <template #toolbar-right>
  29. <el-input
  30. v-model="searchKeyword"
  31. :prefix-icon="Search"
  32. size="large"
  33. placeholder="请输入角色名称或权限字符"
  34. style="width: 300px"
  35. clearable
  36. />
  37. </template>
  38. <template #column-status="{ row }">
  39. <el-tag :type="row.status === '正常' ? 'success' : 'danger'">
  40. {{ row.status }}
  41. </el-tag>
  42. </template>
  43. <template #column-operation="{ row }">
  44. <el-button v-permission="'sys:role:edit'" type="primary" link @click="handleEdit(row)">
  45. 编辑
  46. </el-button>
  47. <el-button v-permission="'sys:role:delete'" type="danger" link @click="handleDelete(row)">
  48. 删除
  49. </el-button>
  50. <el-button v-permission="'sys:role:assign'" type="primary" link @click="handleAssign(row)">
  51. 分配权限
  52. </el-button>
  53. </template>
  54. </TableCard>
  55. <!-- 新增/编辑角色对话框 -->
  56. <el-dialog
  57. v-model="roleDialogVisible"
  58. :title="isEdit ? '编辑角色' : '新增角色'"
  59. width="500px"
  60. destroy-on-close
  61. >
  62. <el-form ref="formRef" :model="roleForm" :rules="formRules" label-width="80px">
  63. <el-form-item label="角色名称" prop="roleName">
  64. <el-input v-model="roleForm.roleName" placeholder="请输入角色名称" />
  65. </el-form-item>
  66. <el-form-item label="权限字符" prop="roleKey">
  67. <el-input v-model="roleForm.roleKey" placeholder="请输入权限字符" />
  68. </el-form-item>
  69. <el-form-item label="排序" prop="sort">
  70. <el-input-number v-model="roleForm.sort" :min="0" :max="999" />
  71. </el-form-item>
  72. <el-form-item label="状态" prop="status">
  73. <el-switch v-model="roleForm.status" active-text="正常" inactive-text="停用" />
  74. </el-form-item>
  75. <el-form-item label="备注" prop="remark">
  76. <el-input v-model="roleForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
  77. </el-form-item>
  78. </el-form>
  79. <template #footer>
  80. <el-button @click="roleDialogVisible = false">取消</el-button>
  81. <el-button type="primary" @click="handleSubmit">确定</el-button>
  82. </template>
  83. </el-dialog>
  84. <!-- 分配权限对话框 -->
  85. <el-dialog v-model="assignDialogVisible" title="分配权限" width="500px" destroy-on-close>
  86. <el-tree
  87. ref="treeRef"
  88. :data="menuTree"
  89. show-checkbox
  90. check-strictly
  91. node-key="id"
  92. default-expand-all
  93. :props="{ label: 'label', children: 'children' }"
  94. />
  95. <template #footer>
  96. <el-button @click="assignDialogVisible = false">取消</el-button>
  97. <el-button type="primary" @click="handleAssignSubmit">确定</el-button>
  98. </template>
  99. </el-dialog>
  100. </div>
  101. </template>
  102. <script setup lang="ts">
  103. import TableCard from '@/components/TableCard/index.vue'
  104. import { CirclePlus, Delete, Search, Refresh } from '@element-plus/icons-vue'
  105. import { ElMessageBox } from 'element-plus'
  106. import type { FormInstance, FormRules } from 'element-plus'
  107. // ========== 类型定义 ==========
  108. interface RoleRow {
  109. id: number
  110. roleName: string
  111. roleKey: string
  112. sort: number
  113. status: string
  114. remark: string
  115. createTime: string
  116. menuIds: number[]
  117. }
  118. interface RoleForm {
  119. id?: number
  120. roleName: string
  121. roleKey: string
  122. sort: number
  123. status: boolean
  124. remark: string
  125. }
  126. interface MenuItem {
  127. id: number
  128. label: string
  129. children?: MenuItem[]
  130. }
  131. // ========== 表格配置 ==========
  132. const columns = [
  133. { type: 'selection' as const, width: 55 },
  134. { prop: 'roleName', label: '角色名称', minWidth: 120 },
  135. { prop: 'roleKey', label: '权限字符', minWidth: 120 },
  136. { prop: 'sort', label: '排序', minWidth: 80 },
  137. { prop: 'status', label: '状态', minWidth: 80 },
  138. { prop: 'createTime', label: '创建时间', minWidth: 160 },
  139. { prop: 'operation', label: '操作', minWidth: 280, isAction: true },
  140. ]
  141. // ========== Mock 数据 ==========
  142. const tableData = ref<RoleRow[]>([
  143. {
  144. id: 1,
  145. roleName: '超级管理员',
  146. roleKey: 'admin',
  147. sort: 1,
  148. status: '正常',
  149. remark: '拥有所有权限',
  150. createTime: '2024-01-01 10:00:00',
  151. menuIds: [
  152. 1, 100, 101, 1011, 1012, 1013, 102, 1021, 1022, 1023, 1024, 103, 1031, 1032, 1033, 200, 300,
  153. 301,
  154. ],
  155. },
  156. {
  157. id: 2,
  158. roleName: '内容编辑',
  159. roleKey: 'editor',
  160. sort: 2,
  161. status: '正常',
  162. remark: '内容管理权限',
  163. createTime: '2024-01-15 14:30:00',
  164. menuIds: [1, 200, 300, 301],
  165. },
  166. {
  167. id: 3,
  168. roleName: '运营人员',
  169. roleKey: 'operator',
  170. sort: 3,
  171. status: '正常',
  172. remark: '运营相关权限',
  173. createTime: '2024-02-01 09:00:00',
  174. menuIds: [1, 300, 301],
  175. },
  176. {
  177. id: 4,
  178. roleName: '普通用户',
  179. roleKey: 'user',
  180. sort: 4,
  181. status: '正常',
  182. remark: '基本查看权限',
  183. createTime: '2024-02-20 11:15:00',
  184. menuIds: [1],
  185. },
  186. {
  187. id: 5,
  188. roleName: '已禁用角色',
  189. roleKey: 'disabled',
  190. sort: 5,
  191. status: '停用',
  192. remark: '已禁用的测试角色',
  193. createTime: '2024-03-01 16:45:00',
  194. menuIds: [],
  195. },
  196. ])
  197. // ========== 搜索 & 分页 ==========
  198. const searchKeyword = ref('')
  199. const selectedRows = ref<RoleRow[]>([])
  200. const queryParams = reactive({
  201. pageNo: 1,
  202. pageSize: 10,
  203. })
  204. const filteredData = computed(() => {
  205. if (!searchKeyword.value) return tableData.value
  206. const keyword = searchKeyword.value.toLowerCase()
  207. return tableData.value.filter(
  208. (row) =>
  209. row.roleName.toLowerCase().includes(keyword) || row.roleKey.toLowerCase().includes(keyword)
  210. )
  211. })
  212. const total = computed(() => filteredData.value.length)
  213. function handlePageChange(page: number, size: number) {
  214. queryParams.pageNo = page
  215. queryParams.pageSize = size
  216. }
  217. function handleSelectionChange(selection: RoleRow[]) {
  218. selectedRows.value = selection
  219. }
  220. function handleRefresh() {
  221. searchKeyword.value = ''
  222. queryParams.pageNo = 1
  223. ElMessage.success('刷新成功')
  224. }
  225. // ========== 新增/编辑角色 ==========
  226. const roleDialogVisible = ref(false)
  227. const isEdit = ref(false)
  228. const formRef = ref<FormInstance>()
  229. const createDefaultForm = (): RoleForm => ({
  230. roleName: '',
  231. roleKey: '',
  232. sort: 0,
  233. status: true,
  234. remark: '',
  235. })
  236. const roleForm = reactive<RoleForm>(createDefaultForm())
  237. const formRules = reactive<FormRules>({
  238. roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
  239. roleKey: [{ required: true, message: '请输入权限字符', trigger: 'blur' }],
  240. })
  241. function handleAdd() {
  242. isEdit.value = false
  243. Object.assign(roleForm, createDefaultForm())
  244. roleDialogVisible.value = true
  245. }
  246. function handleEdit(row: RoleRow) {
  247. isEdit.value = true
  248. Object.assign(roleForm, {
  249. id: row.id,
  250. roleName: row.roleName,
  251. roleKey: row.roleKey,
  252. sort: row.sort,
  253. status: row.status === '正常',
  254. remark: row.remark,
  255. })
  256. roleDialogVisible.value = true
  257. }
  258. function handleSubmit() {
  259. formRef.value?.validate((valid) => {
  260. if (!valid) return
  261. const statusText = roleForm.status ? '正常' : '停用'
  262. const now = new Date()
  263. const pad = (n: number) => String(n).padStart(2, '0')
  264. const formattedDate = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
  265. if (isEdit.value && roleForm.id) {
  266. const index = tableData.value.findIndex((r) => r.id === roleForm.id)
  267. if (index !== -1) {
  268. tableData.value[index] = {
  269. ...tableData.value[index],
  270. roleName: roleForm.roleName,
  271. roleKey: roleForm.roleKey,
  272. sort: roleForm.sort,
  273. status: statusText,
  274. remark: roleForm.remark,
  275. }
  276. }
  277. ElMessage.success('修改成功')
  278. } else {
  279. const newId =
  280. tableData.value.length > 0 ? Math.max(...tableData.value.map((r) => r.id)) + 1 : 1
  281. tableData.value.push({
  282. id: newId,
  283. roleName: roleForm.roleName,
  284. roleKey: roleForm.roleKey,
  285. sort: roleForm.sort,
  286. status: statusText,
  287. remark: roleForm.remark,
  288. createTime: formattedDate,
  289. menuIds: [],
  290. })
  291. ElMessage.success('新增成功')
  292. }
  293. roleDialogVisible.value = false
  294. })
  295. }
  296. // ========== 删除 ==========
  297. function handleDelete(row: RoleRow) {
  298. ElMessageBox.confirm(`确认删除角色「${row.roleName}」?`, '提示', {
  299. confirmButtonText: '确定',
  300. cancelButtonText: '取消',
  301. type: 'warning',
  302. })
  303. .then(() => {
  304. const index = tableData.value.findIndex((r) => r.id === row.id)
  305. if (index !== -1) {
  306. tableData.value.splice(index, 1)
  307. ElMessage.success('删除成功')
  308. }
  309. })
  310. .catch(() => {})
  311. }
  312. function handleBatchDelete() {
  313. ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 个角色?`, '提示', {
  314. confirmButtonText: '确定',
  315. cancelButtonText: '取消',
  316. type: 'warning',
  317. })
  318. .then(() => {
  319. const ids = new Set(selectedRows.value.map((r) => r.id))
  320. tableData.value = tableData.value.filter((r) => !ids.has(r.id))
  321. selectedRows.value = []
  322. ElMessage.success('删除成功')
  323. })
  324. .catch(() => {})
  325. }
  326. // ========== 分配权限 ==========
  327. const assignDialogVisible = ref(false)
  328. const currentAssignRole = ref<RoleRow | null>(null)
  329. const treeRef = ref<{
  330. getCheckedKeys: () => number[]
  331. setCheckedKeys: (keys: number[]) => void
  332. }>()
  333. const menuTree = ref<MenuItem[]>([
  334. { id: 1, label: '首页', children: [] },
  335. {
  336. id: 100,
  337. label: '系统管理',
  338. children: [
  339. {
  340. id: 101,
  341. label: '用户管理',
  342. children: [
  343. { id: 1011, label: '新增用户' },
  344. { id: 1012, label: '编辑用户' },
  345. { id: 1013, label: '删除用户' },
  346. ],
  347. },
  348. {
  349. id: 102,
  350. label: '角色管理',
  351. children: [
  352. { id: 1021, label: '新增角色' },
  353. { id: 1022, label: '编辑角色' },
  354. { id: 1023, label: '删除角色' },
  355. { id: 1024, label: '分配权限' },
  356. ],
  357. },
  358. {
  359. id: 103,
  360. label: '菜单管理',
  361. children: [
  362. { id: 1031, label: '新增菜单' },
  363. { id: 1032, label: '编辑菜单' },
  364. { id: 1033, label: '删除菜单' },
  365. ],
  366. },
  367. ],
  368. },
  369. { id: 200, label: 'SDK管理', children: [] },
  370. {
  371. id: 300,
  372. label: '内容管理',
  373. children: [{ id: 301, label: '文章管理' }],
  374. },
  375. ])
  376. function handleAssign(row: RoleRow) {
  377. currentAssignRole.value = row
  378. assignDialogVisible.value = true
  379. nextTick(() => {
  380. treeRef.value?.setCheckedKeys(row.menuIds)
  381. })
  382. }
  383. function handleAssignSubmit() {
  384. if (currentAssignRole.value) {
  385. const checkedKeys = treeRef.value?.getCheckedKeys() ?? []
  386. const index = tableData.value.findIndex((r) => r.id === currentAssignRole.value!.id)
  387. if (index !== -1) {
  388. tableData.value[index].menuIds = checkedKeys
  389. }
  390. ElMessage.success('权限分配成功')
  391. }
  392. assignDialogVisible.value = false
  393. }
  394. </script>
  395. <style lang="scss" scoped></style>