xto-fronted 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.env.development +4 -0
  2. package/.env.production +4 -0
  3. package/README.md +94 -196
  4. package/dist/{components/Layout/TopMenu.vue.d.ts → App.vue.d.ts} +1 -1
  5. package/dist/api/auth.d.ts +8 -10
  6. package/dist/api/system.d.ts +11 -12
  7. package/dist/api/user.d.ts +12 -3
  8. package/dist/components/Layout/Footer.vue.d.ts +1 -1
  9. package/dist/components/Layout/Header.vue.d.ts +3 -14
  10. package/dist/components/Layout/Sidebar.vue.d.ts +1 -1
  11. package/dist/components/Layout/Tabs.vue.d.ts +1 -1
  12. package/dist/components/Layout/index.vue.d.ts +1 -1
  13. package/dist/composables/useAuth.d.ts +4 -19
  14. package/dist/directives/permission.d.ts +0 -1
  15. package/dist/index-CWRs4WMN.js +372 -0
  16. package/dist/index-CpxpXTQX.js +1462 -0
  17. package/dist/index-Cu3Z2-PY.js +345 -0
  18. package/dist/index-DPEVEyik.js +475 -0
  19. package/dist/index-DYnXaqYf.js +142 -0
  20. package/dist/index.d.ts +12 -25
  21. package/dist/index.es.js +76 -1521
  22. package/dist/index.umd.js +1 -20
  23. package/dist/main.d.ts +0 -1
  24. package/dist/router/dynamicRoutes.d.ts +33 -17
  25. package/dist/router/index.d.ts +4 -26
  26. package/dist/router/layoutRoute.d.ts +18 -0
  27. package/dist/router/staticRoutes.d.ts +2 -18
  28. package/dist/setup.d.ts +17 -0
  29. package/dist/stores/app.d.ts +15 -9
  30. package/dist/stores/auth.d.ts +48 -62
  31. package/dist/stores/index.d.ts +3 -1
  32. package/dist/stores/menu.d.ts +29 -47
  33. package/dist/stores/user.d.ts +84 -64
  34. package/dist/style.css +1 -1
  35. package/dist/utils/auth.d.ts +10 -10
  36. package/dist/utils/permission.d.ts +10 -1
  37. package/dist/utils/request.d.ts +7 -23
  38. package/dist/{components/Layout/Breadcrumb.vue.d.ts → views/dashboard/index.vue.d.ts} +1 -1
  39. package/dist/{components/Error → views/error}/403.vue.d.ts +1 -1
  40. package/dist/{components/Error → views/error}/404.vue.d.ts +1 -1
  41. package/dist/views/login/index.vue.d.ts +4 -0
  42. package/dist/views/system/menu/index.vue.d.ts +4 -0
  43. package/dist/views/system/role/index.vue.d.ts +4 -0
  44. package/dist/views/system/user/index.vue.d.ts +4 -0
  45. package/dist/vite.svg +9 -9
  46. package/index.html +13 -0
  47. package/package.json +27 -31
  48. package/public/vite.svg +10 -0
  49. package/src/App.vue +20 -0
  50. package/src/api/auth.ts +26 -0
  51. package/src/api/system.ts +65 -0
  52. package/src/api/user.ts +46 -0
  53. package/src/assets/styles/_dark.scss +407 -0
  54. package/src/assets/styles/_reset.scss +126 -0
  55. package/src/assets/styles/_root.scss +140 -0
  56. package/src/assets/styles/_transition.scss +119 -0
  57. package/src/assets/styles/_variables.scss +45 -0
  58. package/src/assets/styles/index.scss +187 -0
  59. package/src/components/Layout/Footer.vue +17 -0
  60. package/src/components/Layout/Header.vue +390 -0
  61. package/src/components/Layout/Sidebar.vue +297 -0
  62. package/src/components/Layout/Tabs.vue +134 -0
  63. package/src/components/Layout/index.vue +62 -0
  64. package/src/composables/useAuth.ts +45 -0
  65. package/src/composables/useForm.ts +79 -0
  66. package/src/composables/useTable.ts +97 -0
  67. package/src/directives/permission.ts +38 -0
  68. package/src/enums/index.ts +63 -0
  69. package/src/env.d.ts +17 -0
  70. package/src/index.ts +48 -0
  71. package/src/main.ts +34 -0
  72. package/src/router/dynamicRoutes.ts +163 -0
  73. package/src/router/index.ts +81 -0
  74. package/src/router/layoutRoute.ts +45 -0
  75. package/src/router/staticRoutes.ts +43 -0
  76. package/src/setup.ts +54 -0
  77. package/src/stores/app.ts +163 -0
  78. package/src/stores/auth.ts +66 -0
  79. package/src/stores/index.ts +15 -0
  80. package/src/stores/menu.ts +80 -0
  81. package/src/stores/user.ts +73 -0
  82. package/src/style.css +11 -0
  83. package/src/types/api.d.ts +84 -0
  84. package/src/types/global.d.ts +45 -0
  85. package/src/types/router.d.ts +48 -0
  86. package/src/types/xto.d.ts +149 -0
  87. package/src/utils/auth.ts +62 -0
  88. package/src/utils/permission.ts +42 -0
  89. package/src/utils/request.ts +124 -0
  90. package/src/utils/storage.ts +63 -0
  91. package/src/views/dashboard/index.vue +284 -0
  92. package/src/views/error/403.vue +57 -0
  93. package/src/views/error/404.vue +57 -0
  94. package/src/views/login/index.vue +248 -0
  95. package/src/views/system/menu/index.vue +381 -0
  96. package/src/views/system/role/index.vue +304 -0
  97. package/src/views/system/user/index.vue +327 -0
  98. package/tsconfig.json +26 -0
  99. package/tsconfig.node.json +11 -0
  100. package/vite.config.ts +140 -0
  101. package/dist/api/menu.d.ts +0 -4
  102. package/dist/components/Login/index.vue.d.ts +0 -25
  103. package/dist/components/SettingDrawer/index.vue.d.ts +0 -19
  104. package/dist/composables/index.d.ts +0 -8
  105. package/dist/composables/useApp.d.ts +0 -65
  106. package/dist/composables/useMenu.d.ts +0 -34
  107. package/dist/config/index.d.ts +0 -31
@@ -0,0 +1,381 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive, computed, onMounted } from 'vue'
3
+ import { Form, FormItem, Input, Select, Switch, InputNumber } from '@xto/form'
4
+ import { Card, Tag } from '@xto/data'
5
+ import { Modal, Message } from '@xto/feedback'
6
+ import { Space, Button } from '@xto/base'
7
+ import { MenuType, MenuTypeOptions, Status } from '@/enums'
8
+
9
+ // 菜单数据类型
10
+ interface Menu {
11
+ id: number
12
+ parentId: number | null
13
+ name: string
14
+ path: string
15
+ component: string
16
+ redirect: string
17
+ icon: string
18
+ title: string
19
+ type: MenuType
20
+ sort: number
21
+ status: Status
22
+ hidden: boolean
23
+ keepAlive: boolean
24
+ children?: Menu[]
25
+ }
26
+
27
+ // Mock 数据
28
+ const mockMenus: Menu[] = [
29
+ {
30
+ id: 1,
31
+ parentId: null,
32
+ name: 'Dashboard',
33
+ path: '/dashboard',
34
+ component: 'dashboard/index',
35
+ redirect: '',
36
+ icon: 'dashboard',
37
+ title: '仪表盘',
38
+ type: MenuType.MENU,
39
+ sort: 1,
40
+ status: Status.ENABLED,
41
+ hidden: false,
42
+ keepAlive: true
43
+ },
44
+ {
45
+ id: 2,
46
+ parentId: null,
47
+ name: 'System',
48
+ path: '/system',
49
+ component: '',
50
+ redirect: '/system/user',
51
+ icon: 'setting',
52
+ title: '系统管理',
53
+ type: MenuType.DIRECTORY,
54
+ sort: 2,
55
+ status: Status.ENABLED,
56
+ hidden: false,
57
+ keepAlive: false,
58
+ children: [
59
+ {
60
+ id: 21,
61
+ parentId: 2,
62
+ name: 'SystemUser',
63
+ path: '/system/user',
64
+ component: 'system/user/index',
65
+ redirect: '',
66
+ icon: 'user',
67
+ title: '用户管理',
68
+ type: MenuType.MENU,
69
+ sort: 1,
70
+ status: Status.ENABLED,
71
+ hidden: false,
72
+ keepAlive: true
73
+ },
74
+ {
75
+ id: 22,
76
+ parentId: 2,
77
+ name: 'SystemRole',
78
+ path: '/system/role',
79
+ component: 'system/role/index',
80
+ redirect: '',
81
+ icon: 'role',
82
+ title: '角色管理',
83
+ type: MenuType.MENU,
84
+ sort: 2,
85
+ status: Status.ENABLED,
86
+ hidden: false,
87
+ keepAlive: true
88
+ },
89
+ {
90
+ id: 23,
91
+ parentId: 2,
92
+ name: 'SystemMenu',
93
+ path: '/system/menu',
94
+ component: 'system/menu/index',
95
+ redirect: '',
96
+ icon: 'menu',
97
+ title: '菜单管理',
98
+ type: MenuType.MENU,
99
+ sort: 3,
100
+ status: Status.ENABLED,
101
+ hidden: false,
102
+ keepAlive: true
103
+ }
104
+ ]
105
+ }
106
+ ]
107
+
108
+ const loading = ref(false)
109
+ const menuList = ref<Menu[]>([])
110
+
111
+ // 弹窗
112
+ const modalVisible = ref(false)
113
+ const modalTitle = computed(() => formData.id ? '编辑菜单' : '新增菜单')
114
+ const formData = reactive({
115
+ id: 0,
116
+ parentId: null as number | null,
117
+ name: '',
118
+ path: '',
119
+ component: '',
120
+ redirect: '',
121
+ icon: '',
122
+ title: '',
123
+ type: MenuType.MENU,
124
+ sort: 0,
125
+ status: Status.ENABLED,
126
+ hidden: false,
127
+ keepAlive: true
128
+ })
129
+
130
+ const rules: Record<string, any[]> = {
131
+ name: [
132
+ { required: true, message: '请输入菜单名称', trigger: 'blur' }
133
+ ],
134
+ path: [
135
+ { required: true, message: '请输入路由路径', trigger: 'blur' }
136
+ ],
137
+ title: [
138
+ { required: true, message: '请输入菜单标题', trigger: 'blur' }
139
+ ]
140
+ }
141
+
142
+ const formRef = ref()
143
+
144
+ // 获取菜单列表
145
+ const getMenuList = () => {
146
+ loading.value = true
147
+ setTimeout(() => {
148
+ menuList.value = mockMenus
149
+ loading.value = false
150
+ }, 300)
151
+ }
152
+
153
+ // 新增
154
+ const handleAdd = (parentId: number | null = null) => {
155
+ Object.assign(formData, {
156
+ id: 0,
157
+ parentId,
158
+ name: '',
159
+ path: '',
160
+ component: '',
161
+ redirect: '',
162
+ icon: '',
163
+ title: '',
164
+ type: MenuType.MENU,
165
+ sort: 0,
166
+ status: Status.ENABLED,
167
+ hidden: false,
168
+ keepAlive: true
169
+ })
170
+ modalVisible.value = true
171
+ }
172
+
173
+ // 编辑
174
+ const handleEdit = (node: Menu) => {
175
+ Object.assign(formData, node)
176
+ modalVisible.value = true
177
+ }
178
+
179
+ // 提交
180
+ const handleSubmit = async () => {
181
+ try {
182
+ await formRef.value?.validate()
183
+ Message.success(formData.id ? '编辑成功' : '新增成功')
184
+ modalVisible.value = false
185
+ getMenuList()
186
+ } catch (error) {
187
+ console.error(error)
188
+ }
189
+ }
190
+
191
+ // 菜单图标
192
+ const getMenuIcon = (icon?: string) => {
193
+ const iconMap: Record<string, string> = {
194
+ dashboard: '📊',
195
+ system: '⚙️',
196
+ user: '👤',
197
+ role: '👥',
198
+ menu: '📋',
199
+ setting: '🔧'
200
+ }
201
+ return iconMap[icon || ''] || '📄'
202
+ }
203
+
204
+ onMounted(() => {
205
+ getMenuList()
206
+ })
207
+ </script>
208
+
209
+ <template>
210
+ <div class="menu-page">
211
+ <Card class="menu-card">
212
+ <!-- 工具栏 -->
213
+ <div class="toolbar">
214
+ <Button type="primary" @click="handleAdd()">新增菜单</Button>
215
+ </div>
216
+
217
+ <!-- 菜单树 -->
218
+ <div class="menu-tree">
219
+ <table class="tree-table">
220
+ <thead>
221
+ <tr>
222
+ <th>菜单名称</th>
223
+ <th>图标</th>
224
+ <th>路由路径</th>
225
+ <th>类型</th>
226
+ <th>排序</th>
227
+ <th>状态</th>
228
+ <th>操作</th>
229
+ </tr>
230
+ </thead>
231
+ <tbody>
232
+ <template v-for="menu in menuList" :key="menu.id">
233
+ <tr class="tree-row tree-row--level-0">
234
+ <td>
235
+ <span class="menu-name" @click="handleEdit(menu)">{{ menu.title }}</span>
236
+ </td>
237
+ <td>{{ getMenuIcon(menu.icon) }}</td>
238
+ <td>{{ menu.path }}</td>
239
+ <td>
240
+ <Tag :type="menu.type === MenuType.DIRECTORY ? 'primary' : menu.type === MenuType.MENU ? 'success' : 'warning'" size="small">
241
+ {{ menu.type === MenuType.DIRECTORY ? '目录' : menu.type === MenuType.MENU ? '菜单' : '按钮' }}
242
+ </Tag>
243
+ </td>
244
+ <td>{{ menu.sort }}</td>
245
+ <td>
246
+ <Tag :type="menu.status === Status.ENABLED ? 'success' : 'danger'" size="small">
247
+ {{ menu.status === Status.ENABLED ? '启用' : '禁用' }}
248
+ </Tag>
249
+ </td>
250
+ <td>
251
+ <Space>
252
+ <Button type="primary" link size="small" @click="handleAdd(menu.id)">新增</Button>
253
+ <Button type="primary" link size="small" @click="handleEdit(menu)">编辑</Button>
254
+ </Space>
255
+ </td>
256
+ </tr>
257
+ <template v-if="menu.children" v-for="child in menu.children" :key="child.id">
258
+ <tr class="tree-row tree-row--level-1">
259
+ <td>
260
+ <span class="tree-indent"></span>
261
+ <span class="menu-name" @click="handleEdit(child)">{{ child.title }}</span>
262
+ </td>
263
+ <td>{{ getMenuIcon(child.icon) }}</td>
264
+ <td>{{ child.path }}</td>
265
+ <td>
266
+ <Tag :type="child.type === MenuType.DIRECTORY ? 'primary' : child.type === MenuType.MENU ? 'success' : 'warning'" size="small">
267
+ {{ child.type === MenuType.DIRECTORY ? '目录' : child.type === MenuType.MENU ? '菜单' : '按钮' }}
268
+ </Tag>
269
+ </td>
270
+ <td>{{ child.sort }}</td>
271
+ <td>
272
+ <Tag :type="child.status === Status.ENABLED ? 'success' : 'danger'" size="small">
273
+ {{ child.status === Status.ENABLED ? '启用' : '禁用' }}
274
+ </Tag>
275
+ </td>
276
+ <td>
277
+ <Space>
278
+ <Button type="primary" link size="small" @click="handleEdit(child)">编辑</Button>
279
+ </Space>
280
+ </td>
281
+ </tr>
282
+ </template>
283
+ </template>
284
+ </tbody>
285
+ </table>
286
+ </div>
287
+ </Card>
288
+
289
+ <!-- 编辑弹窗 -->
290
+ <Modal v-model="modalVisible" :title="modalTitle" width="600px">
291
+ <Form ref="formRef" :model="formData" :rules="rules" label-width="80px">
292
+ <FormItem label="上级菜单">
293
+ <Input v-model="formData.parentId" placeholder="上级菜单ID" disabled />
294
+ </FormItem>
295
+ <FormItem label="菜单类型">
296
+ <Select v-model="formData.type" :options="MenuTypeOptions" />
297
+ </FormItem>
298
+ <FormItem label="菜单名称" prop="name">
299
+ <Input v-model="formData.name" placeholder="请输入菜单名称(路由name)" />
300
+ </FormItem>
301
+ <FormItem label="菜单标题" prop="title">
302
+ <Input v-model="formData.title" placeholder="请输入菜单标题" />
303
+ </FormItem>
304
+ <FormItem label="路由路径" prop="path">
305
+ <Input v-model="formData.path" placeholder="请输入路由路径" />
306
+ </FormItem>
307
+ <FormItem label="组件路径">
308
+ <Input v-model="formData.component" placeholder="请输入组件路径" />
309
+ </FormItem>
310
+ <FormItem label="图标">
311
+ <Input v-model="formData.icon" placeholder="请输入图标名称" />
312
+ </FormItem>
313
+ <FormItem label="排序">
314
+ <InputNumber v-model="formData.sort" :min="0" />
315
+ </FormItem>
316
+ <FormItem label="状态">
317
+ <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
318
+ </FormItem>
319
+ <FormItem label="隐藏">
320
+ <Switch v-model="formData.hidden" />
321
+ </FormItem>
322
+ <FormItem label="缓存">
323
+ <Switch v-model="formData.keepAlive" />
324
+ </FormItem>
325
+ </Form>
326
+ <template #footer>
327
+ <Space>
328
+ <Button @click="modalVisible = false">取消</Button>
329
+ <Button type="primary" @click="handleSubmit">确定</Button>
330
+ </Space>
331
+ </template>
332
+ </Modal>
333
+ </div>
334
+ </template>
335
+
336
+ <style lang="scss" scoped>
337
+ .menu-page {
338
+ padding: 20px;
339
+
340
+ .toolbar {
341
+ margin-bottom: 15px;
342
+ }
343
+ }
344
+
345
+ .tree-table {
346
+ width: 100%;
347
+ border-collapse: collapse;
348
+
349
+ th, td {
350
+ padding: 12px;
351
+ text-align: left;
352
+ border-bottom: 1px solid var(--color-border-lighter);
353
+ }
354
+
355
+ th {
356
+ font-weight: 500;
357
+ color: var(--color-text-regular);
358
+ background-color: var(--color-fill-light);
359
+ }
360
+
361
+ .tree-row--level-1 {
362
+ td:first-child {
363
+ padding-left: 30px;
364
+ }
365
+ }
366
+ }
367
+
368
+ .tree-indent {
369
+ display: inline-block;
370
+ width: 20px;
371
+ }
372
+
373
+ .menu-name {
374
+ cursor: pointer;
375
+ color: var(--color-primary);
376
+
377
+ &:hover {
378
+ text-decoration: underline;
379
+ }
380
+ }
381
+ </style>
@@ -0,0 +1,304 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive, computed, onMounted } from 'vue'
3
+ import { Form, FormItem, Input, Select, Switch } from '@xto/form'
4
+ import { Tag, Card, Pagination } from '@xto/data'
5
+ import { Modal, Message, Popconfirm } from '@xto/feedback'
6
+ import { Space, Button } from '@xto/base'
7
+ import { Status, StatusOptions } from '@/enums'
8
+
9
+ // 角色数据类型
10
+ interface Role {
11
+ id: number
12
+ name: string
13
+ code: string
14
+ description: string
15
+ status: Status
16
+ permissions: string[]
17
+ createTime: string
18
+ }
19
+
20
+ // Mock 数据
21
+ const mockRoles: Role[] = [
22
+ { id: 1, name: '超级管理员', code: 'admin', description: '拥有所有权限', status: Status.ENABLED, permissions: ['*'], createTime: '2024-01-01 10:00:00' },
23
+ { id: 2, name: '编辑', code: 'editor', description: '内容编辑权限', status: Status.ENABLED, permissions: ['user:read', 'user:write'], createTime: '2024-01-02 10:00:00' },
24
+ { id: 3, name: '访客', code: 'viewer', description: '只读权限', status: Status.ENABLED, permissions: ['user:read'], createTime: '2024-01-03 10:00:00' }
25
+ ]
26
+
27
+ const loading = ref(false)
28
+ const roleList = ref<Role[]>([])
29
+ const total = ref(0)
30
+ const currentPage = ref(1)
31
+ const pageSize = ref(10)
32
+
33
+ // 搜索条件
34
+ const searchForm = reactive({
35
+ keyword: '',
36
+ status: undefined as Status | undefined
37
+ })
38
+
39
+ // 弹窗
40
+ const modalVisible = ref(false)
41
+ const modalTitle = computed(() => formData.id ? '编辑角色' : '新增角色')
42
+ const formData = reactive({
43
+ id: 0,
44
+ name: '',
45
+ code: '',
46
+ description: '',
47
+ status: Status.ENABLED,
48
+ permissions: [] as string[]
49
+ })
50
+
51
+ const rules: Record<string, any[]> = {
52
+ name: [
53
+ { required: true, message: '请输入角色名称', trigger: 'blur' }
54
+ ],
55
+ code: [
56
+ { required: true, message: '请输入角色编码', trigger: 'blur' }
57
+ ]
58
+ }
59
+
60
+ const formRef = ref()
61
+
62
+ // 获取角色列表
63
+ const getRoleList = () => {
64
+ loading.value = true
65
+ setTimeout(() => {
66
+ let list = [...mockRoles]
67
+
68
+ if (searchForm.keyword) {
69
+ list = list.filter(role =>
70
+ role.name.includes(searchForm.keyword) ||
71
+ role.code.includes(searchForm.keyword)
72
+ )
73
+ }
74
+ if (searchForm.status !== undefined) {
75
+ list = list.filter(role => role.status === searchForm.status)
76
+ }
77
+
78
+ total.value = list.length
79
+ roleList.value = list.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
80
+ loading.value = false
81
+ }, 300)
82
+ }
83
+
84
+ // 搜索
85
+ const handleSearch = () => {
86
+ currentPage.value = 1
87
+ getRoleList()
88
+ }
89
+
90
+ // 重置
91
+ const handleReset = () => {
92
+ searchForm.keyword = ''
93
+ searchForm.status = undefined
94
+ currentPage.value = 1
95
+ getRoleList()
96
+ }
97
+
98
+ // 新增
99
+ const handleAdd = () => {
100
+ Object.assign(formData, {
101
+ id: 0,
102
+ name: '',
103
+ code: '',
104
+ description: '',
105
+ status: Status.ENABLED,
106
+ permissions: []
107
+ })
108
+ modalVisible.value = true
109
+ }
110
+
111
+ // 编辑
112
+ const handleEdit = (row: Role) => {
113
+ Object.assign(formData, row)
114
+ modalVisible.value = true
115
+ }
116
+
117
+ // 删除
118
+ const handleDelete = (_id: number) => {
119
+ Message.success('删除成功')
120
+ getRoleList()
121
+ }
122
+
123
+ // 提交
124
+ const handleSubmit = async () => {
125
+ try {
126
+ await formRef.value?.validate()
127
+ Message.success(formData.id ? '编辑成功' : '新增成功')
128
+ modalVisible.value = false
129
+ getRoleList()
130
+ } catch (error) {
131
+ console.error(error)
132
+ }
133
+ }
134
+
135
+ onMounted(() => {
136
+ getRoleList()
137
+ })
138
+ </script>
139
+
140
+ <template>
141
+ <div class="role-page">
142
+ <!-- 搜索栏 -->
143
+ <Card class="search-card">
144
+ <Form :model="searchForm" inline>
145
+ <FormItem label="关键词">
146
+ <Input
147
+ v-model="searchForm.keyword"
148
+ placeholder="角色名称/编码"
149
+ clearable
150
+ @keyup.enter="handleSearch"
151
+ />
152
+ </FormItem>
153
+ <FormItem label="状态">
154
+ <Select
155
+ v-model="searchForm.status"
156
+ :options="StatusOptions"
157
+ placeholder="请选择"
158
+ clearable
159
+ />
160
+ </FormItem>
161
+ <FormItem>
162
+ <Space>
163
+ <Button type="primary" @click="handleSearch">搜索</Button>
164
+ <Button @click="handleReset">重置</Button>
165
+ </Space>
166
+ </FormItem>
167
+ </Form>
168
+ </Card>
169
+
170
+ <!-- 表格 -->
171
+ <Card class="table-card">
172
+ <!-- 工具栏 -->
173
+ <div class="toolbar">
174
+ <Button type="primary" @click="handleAdd">新增角色</Button>
175
+ </div>
176
+
177
+ <!-- 表格 -->
178
+ <table class="data-table">
179
+ <thead>
180
+ <tr>
181
+ <th>ID</th>
182
+ <th>角色名称</th>
183
+ <th>角色编码</th>
184
+ <th>描述</th>
185
+ <th>状态</th>
186
+ <th>创建时间</th>
187
+ <th>操作</th>
188
+ </tr>
189
+ </thead>
190
+ <tbody>
191
+ <tr v-if="loading">
192
+ <td colspan="7" class="loading-cell">加载中...</td>
193
+ </tr>
194
+ <tr v-else-if="roleList.length === 0">
195
+ <td colspan="7" class="empty-cell">暂无数据</td>
196
+ </tr>
197
+ <tr v-else v-for="row in roleList" :key="row.id">
198
+ <td>{{ row.id }}</td>
199
+ <td>{{ row.name }}</td>
200
+ <td>
201
+ <Tag type="primary" size="small">{{ row.code }}</Tag>
202
+ </td>
203
+ <td>{{ row.description }}</td>
204
+ <td>
205
+ <Tag :type="row.status === Status.ENABLED ? 'success' : 'danger'" size="small">
206
+ {{ row.status === Status.ENABLED ? '启用' : '禁用' }}
207
+ </Tag>
208
+ </td>
209
+ <td>{{ row.createTime }}</td>
210
+ <td>
211
+ <Space>
212
+ <Button type="primary" link size="small" @click="handleEdit(row)">编辑</Button>
213
+ <Popconfirm title="确定删除该角色吗?" @confirm="handleDelete(row.id)">
214
+ <Button type="danger" link size="small">删除</Button>
215
+ </Popconfirm>
216
+ </Space>
217
+ </td>
218
+ </tr>
219
+ </tbody>
220
+ </table>
221
+
222
+ <!-- 分页 -->
223
+ <div class="pagination-wrapper">
224
+ <Pagination
225
+ v-model:current-page="currentPage"
226
+ v-model:page-size="pageSize"
227
+ :total="total"
228
+ :page-sizes="[10, 20, 50, 100]"
229
+ layout="total, sizes, prev, pager, next"
230
+ @current-change="getRoleList"
231
+ @size-change="getRoleList"
232
+ />
233
+ </div>
234
+ </Card>
235
+
236
+ <!-- 编辑弹窗 -->
237
+ <Modal v-model="modalVisible" :title="modalTitle" width="500px">
238
+ <Form ref="formRef" :model="formData" :rules="rules" label-width="80px">
239
+ <FormItem label="角色名称" prop="name">
240
+ <Input v-model="formData.name" placeholder="请输入角色名称" />
241
+ </FormItem>
242
+ <FormItem label="角色编码" prop="code">
243
+ <Input v-model="formData.code" placeholder="请输入角色编码" />
244
+ </FormItem>
245
+ <FormItem label="描述">
246
+ <Input v-model="formData.description" placeholder="请输入描述" />
247
+ </FormItem>
248
+ <FormItem label="状态">
249
+ <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
250
+ </FormItem>
251
+ </Form>
252
+ <template #footer>
253
+ <Space>
254
+ <Button @click="modalVisible = false">取消</Button>
255
+ <Button type="primary" @click="handleSubmit">确定</Button>
256
+ </Space>
257
+ </template>
258
+ </Modal>
259
+ </div>
260
+ </template>
261
+
262
+ <style lang="scss" scoped>
263
+ .role-page {
264
+ padding: 20px;
265
+
266
+ .search-card {
267
+ margin-bottom: 20px;
268
+ }
269
+
270
+ .toolbar {
271
+ margin-bottom: 15px;
272
+ }
273
+ }
274
+
275
+ .data-table {
276
+ width: 100%;
277
+ border-collapse: collapse;
278
+
279
+ th, td {
280
+ padding: 12px;
281
+ text-align: left;
282
+ border-bottom: 1px solid var(--color-border-lighter);
283
+ }
284
+
285
+ th {
286
+ font-weight: 500;
287
+ color: var(--color-text-regular);
288
+ background-color: var(--color-fill-light);
289
+ }
290
+
291
+ .loading-cell,
292
+ .empty-cell {
293
+ text-align: center;
294
+ color: var(--color-text-secondary);
295
+ padding: 40px;
296
+ }
297
+ }
298
+
299
+ .pagination-wrapper {
300
+ display: flex;
301
+ justify-content: flex-end;
302
+ margin-top: 20px;
303
+ }
304
+ </style>