xto-fronted 0.4.101 → 0.4.103

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 (123) hide show
  1. package/dist/{index-C1j4f3mM.js → index-BlN5yXBT.js} +2 -2
  2. package/dist/{index-BKj-34y6.js → index-BsPaXQak.js} +2 -2
  3. package/dist/{index-CJSTBnGF.js → index-CNx4xpUG.js} +981 -971
  4. package/dist/{index-BK4Mut6H.js → index-DEChud2_.js} +2 -2
  5. package/dist/{index-B6s_uLJE.js → index-DdACCkye.js} +23 -23
  6. package/dist/index.js +1 -1
  7. package/dist/stores/app.d.ts +8 -2
  8. package/dist/stores/auth.d.ts +2 -2
  9. package/dist/style.css +1 -1
  10. package/package.json +94 -94
  11. package/src/App.vue +48 -48
  12. package/src/assets/styles/_dark.scss +639 -639
  13. package/src/assets/styles/_root.scss +183 -183
  14. package/src/assets/styles/_variables.scss +69 -69
  15. package/src/assets/styles/index.scss +460 -460
  16. package/src/components/Layout/Header.vue +1 -1
  17. package/src/components/Layout/MixTopMenu.vue +1185 -1185
  18. package/src/components/Layout/Sidebar.vue +229 -229
  19. package/src/components/Layout/SidebarMenuItem.vue +163 -163
  20. package/src/components/Layout/TopMenu.vue +1177 -1177
  21. package/src/components/Layout/index.vue +199 -199
  22. package/src/composables/useI18n.ts +43 -43
  23. package/src/index.ts +114 -100
  24. package/src/router/layoutRoute.ts +70 -70
  25. package/src/stores/app.ts +9 -0
  26. package/src/stores/index.ts +15 -15
  27. package/src/stores/locale.ts +66 -66
  28. package/src/types/json-bigint.d.ts +18 -18
  29. package/src/types/xto.d.ts +172 -172
  30. package/src/utils/request.ts +184 -184
  31. package/src/views/dashboard/index.vue +545 -545
  32. package/src/views/error/403.vue +251 -251
  33. package/src/views/error/404.vue +253 -253
  34. package/src/views/login/index.vue +586 -586
  35. package/src/views/system/menu/index.vue +690 -690
  36. package/src/views/system/role/index.vue +583 -583
  37. package/src/views/system/user/index.vue +655 -655
  38. package/dist/assets/404-C9Uh6Uu-.css +0 -1
  39. package/dist/assets/404-zjGLLssH.js +0 -1
  40. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  41. package/dist/assets/index-B5xc4gQB.css +0 -1
  42. package/dist/assets/index-BDgOY6Rp.js +0 -1
  43. package/dist/assets/index-BIoRANs0.js +0 -1
  44. package/dist/assets/index-BRR97dc6.js +0 -1
  45. package/dist/assets/index-Bz0BgZQ1.js +0 -1
  46. package/dist/assets/index-CAdztNsv.css +0 -1
  47. package/dist/assets/index-CCXrcISf.css +0 -1
  48. package/dist/assets/index-CfpZmcpk.css +0 -1
  49. package/dist/assets/index-CwJSA85U.js +0 -1
  50. package/dist/assets/index-CwRA10ac.js +0 -1
  51. package/dist/assets/index-D8NDxq9d.js +0 -1
  52. package/dist/assets/index-DEB6-Iv_.js +0 -2
  53. package/dist/assets/index-DM4Ezclc.css +0 -1
  54. package/dist/assets/index-DYv7nImj.css +0 -1
  55. package/dist/assets/index-t-2Y0KhA.css +0 -1
  56. package/dist/assets/vendor-CUVPinTg.js +0 -13
  57. package/dist/assets/vue-vendor-DeJXJVbN.js +0 -29
  58. package/dist/assets/xto-base-CL2NKZJJ.css +0 -1
  59. package/dist/assets/xto-base-PwLGsxxb.js +0 -1
  60. package/dist/assets/xto-business--V1F5Gwb.css +0 -1
  61. package/dist/assets/xto-core-CtL4zKiV.js +0 -1
  62. package/dist/assets/xto-data-MxZsiJgi.css +0 -1
  63. package/dist/assets/xto-data-bCXQa7fT.js +0 -1
  64. package/dist/assets/xto-feedback-Bxx38c3P.css +0 -1
  65. package/dist/assets/xto-feedback-CPydp0kn.js +0 -1
  66. package/dist/assets/xto-form-Cu6q3VLG.css +0 -1
  67. package/dist/assets/xto-form-bywohdAf.js +0 -1
  68. package/dist/assets/xto-layout-BDD6sSlM.css +0 -1
  69. package/dist/assets/xto-navigation-Bbdpine9.js +0 -1
  70. package/dist/assets/xto-navigation-XfpyMpEo.css +0 -1
  71. package/dist/index-3ekBp4iW.js +0 -479
  72. package/dist/index-58aI1w0v.js +0 -515
  73. package/dist/index-A_B_Ap_A.js +0 -4240
  74. package/dist/index-B-lMqzxZ.js +0 -479
  75. package/dist/index-B5DLfOYb.js +0 -189
  76. package/dist/index-BAmYUT0G.js +0 -189
  77. package/dist/index-BJlOXgu5.js +0 -515
  78. package/dist/index-BMQao91y.js +0 -189
  79. package/dist/index-BRvi9qW-.js +0 -515
  80. package/dist/index-BVGW4DDQ.js +0 -189
  81. package/dist/index-BXg94yA2.js +0 -515
  82. package/dist/index-BYAkZ2gD.js +0 -641
  83. package/dist/index-BfXnrw05.js +0 -515
  84. package/dist/index-Bmb0rt9C.js +0 -641
  85. package/dist/index-Bmf0YbVq.js +0 -189
  86. package/dist/index-C1BnOFy7.js +0 -3145
  87. package/dist/index-C2-a5KSQ.js +0 -4233
  88. package/dist/index-C3K89jzC.js +0 -515
  89. package/dist/index-C92NkXAn.js +0 -479
  90. package/dist/index-CAHSv7LK.js +0 -4285
  91. package/dist/index-CVH7bDsl.js +0 -4285
  92. package/dist/index-Ccp6zfq-.js +0 -4290
  93. package/dist/index-CeZ0CSSs.js +0 -641
  94. package/dist/index-Cf8E7FM1.js +0 -4270
  95. package/dist/index-CgyQqbdx.js +0 -189
  96. package/dist/index-ChowNrlU.js +0 -641
  97. package/dist/index-CvQgEgUM.js +0 -641
  98. package/dist/index-D25KzR0I.js +0 -479
  99. package/dist/index-D4LWXVnG.js +0 -515
  100. package/dist/index-DCApv1oX.js +0 -641
  101. package/dist/index-DCBIjLHy.js +0 -515
  102. package/dist/index-DEYOivza.js +0 -641
  103. package/dist/index-DHH8Os_2.js +0 -189
  104. package/dist/index-DReodgBw.js +0 -4233
  105. package/dist/index-DTRJONCd.js +0 -515
  106. package/dist/index-DgffG7KK.js +0 -641
  107. package/dist/index-DjERNRXX.js +0 -515
  108. package/dist/index-DjXyzwL0.js +0 -479
  109. package/dist/index-DkOqM4e2.js +0 -3147
  110. package/dist/index-Ds8IV04t.js +0 -189
  111. package/dist/index-LSdsO2Ox.js +0 -479
  112. package/dist/index-UJixTdep.js +0 -479
  113. package/dist/index-WPRGF_GX.js +0 -189
  114. package/dist/index-WPWzllES.js +0 -641
  115. package/dist/index-Wl2Qg26t.js +0 -3147
  116. package/dist/index-dk0diNwi.js +0 -479
  117. package/dist/index-gBlRG4kk.js +0 -479
  118. package/dist/index-mVol7F2K.js +0 -479
  119. package/dist/index-xWU3J3OH.js +0 -641
  120. package/dist/index-zKJLxthI.js +0 -189
  121. package/dist/index.es.js +0 -95
  122. package/dist/index.html +0 -28
  123. package/dist/index.umd.js +0 -8
@@ -1,583 +1,583 @@
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, 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
- // 状态切换
136
- const handleStatusChange = (row: Role) => {
137
- Message.success(`已${row.status === Status.ENABLED ? '启用' : '禁用'}角色 ${row.name}`)
138
- }
139
-
140
- onMounted(() => {
141
- getRoleList()
142
- })
143
- </script>
144
-
145
- <template>
146
- <div class="role-page">
147
- <!-- 搜索栏 -->
148
- <div class="search-section">
149
- <Form :model="searchForm" inline class="search-form">
150
- <FormItem label="关键词">
151
- <Input
152
- v-model="searchForm.keyword"
153
- placeholder="角色名称/编码"
154
- clearable
155
- @keyup.enter="handleSearch"
156
- />
157
- </FormItem>
158
- <FormItem label="状态">
159
- <Select
160
- v-model="searchForm.status"
161
- :options="StatusOptions"
162
- placeholder="请选择"
163
- clearable
164
- />
165
- </FormItem>
166
- <FormItem>
167
- <Space>
168
- <Button type="primary" @click="handleSearch">
169
- <template #icon>
170
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
- <circle cx="11" cy="11" r="8"/>
172
- <path d="M21 21l-4.35-4.35"/>
173
- </svg>
174
- </template>
175
- 搜索
176
- </Button>
177
- <Button @click="handleReset">
178
- <template #icon>
179
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
180
- <path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/>
181
- <path d="M3 3v5h5"/>
182
- </svg>
183
- </template>
184
- 重置
185
- </Button>
186
- </Space>
187
- </FormItem>
188
- </Form>
189
- </div>
190
-
191
- <!-- 表格 -->
192
- <div class="table-section">
193
- <!-- 工具栏 -->
194
- <div class="table-toolbar">
195
- <div class="toolbar-left">
196
- <Button type="primary" @click="handleAdd">
197
- <template #icon>
198
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
199
- <line x1="12" y1="5" x2="12" y2="19"/>
200
- <line x1="5" y1="12" x2="19" y2="12"/>
201
- </svg>
202
- </template>
203
- 新增角色
204
- </Button>
205
- </div>
206
- <div class="toolbar-right">
207
- <span class="table-count">共 {{ total }} 条数据</span>
208
- </div>
209
- </div>
210
-
211
- <!-- 表格 -->
212
- <div class="table-wrapper">
213
- <table class="data-table">
214
- <thead>
215
- <tr>
216
- <th class="col-id">ID</th>
217
- <th class="col-name">角色名称</th>
218
- <th class="col-code">角色编码</th>
219
- <th class="col-desc">描述</th>
220
- <th class="col-status">状态</th>
221
- <th class="col-time">创建时间</th>
222
- <th class="col-actions">操作</th>
223
- </tr>
224
- </thead>
225
- <tbody>
226
- <tr v-if="loading">
227
- <td colspan="7" class="loading-cell">
228
- <div class="loading-content">
229
- <div class="loading-spinner"></div>
230
- <span>加载中...</span>
231
- </div>
232
- </td>
233
- </tr>
234
- <tr v-else-if="roleList.length === 0">
235
- <td colspan="7" class="empty-cell">
236
- <div class="empty-content">
237
- <svg viewBox="0 0 64 41" fill="none">
238
- <g transform="translate(0 1)">
239
- <ellipse fill="#f5f5f5" cx="32" cy="33" rx="32" ry="7"/>
240
- <g stroke="var(--color-text-placeholder)" stroke-width="2">
241
- <path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"/>
242
- <path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35H11.95C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="var(--color-fill)"/>
243
- </g>
244
- </g>
245
- </svg>
246
- <span>暂无数据</span>
247
- </div>
248
- </td>
249
- </tr>
250
- <tr v-else v-for="row in roleList" :key="row.id" class="data-row">
251
- <td class="col-id">
252
- <span class="id-badge">{{ row.id }}</span>
253
- </td>
254
- <td class="col-name">
255
- <span class="role-name">{{ row.name }}</span>
256
- </td>
257
- <td class="col-code">
258
- <Tag type="primary" size="small">{{ row.code }}</Tag>
259
- </td>
260
- <td class="col-desc">
261
- <span class="desc-text">{{ row.description }}</span>
262
- </td>
263
- <td class="col-status">
264
- <div class="status-switch">
265
- <Switch
266
- :model-value="row.status === Status.ENABLED"
267
- @update:model-value="row.status = $event ? Status.ENABLED : Status.DISABLED; handleStatusChange(row)"
268
- />
269
- <span class="status-text" :class="{ enabled: row.status === Status.ENABLED }">
270
- {{ row.status === Status.ENABLED ? '已启用' : '已禁用' }}
271
- </span>
272
- </div>
273
- </td>
274
- <td class="col-time">
275
- <span class="time-text">{{ row.createTime }}</span>
276
- </td>
277
- <td class="col-actions">
278
- <Space class="action-buttons">
279
- <Button type="primary" link size="small" @click="handleEdit(row)">
280
- <template #icon>
281
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
282
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
283
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
284
- </svg>
285
- </template>
286
- 编辑
287
- </Button>
288
- <Popconfirm title="确定删除该角色吗?" @confirm="handleDelete(row.id)">
289
- <Button type="danger" link size="small">
290
- <template #icon>
291
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
292
- <polyline points="3,6 5,6 21,6"/>
293
- <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
294
- <line x1="10" y1="11" x2="10" y2="17"/>
295
- <line x1="14" y1="11" x2="14" y2="17"/>
296
- </svg>
297
- </template>
298
- 删除
299
- </Button>
300
- </Popconfirm>
301
- </Space>
302
- </td>
303
- </tr>
304
- </tbody>
305
- </table>
306
- </div>
307
-
308
- <!-- 分页 -->
309
- <div class="pagination-wrapper">
310
- <Pagination
311
- v-model:current-page="currentPage"
312
- v-model:page-size="pageSize"
313
- :total="total"
314
- :page-sizes="[10, 20, 50, 100]"
315
- layout="total, sizes, prev, pager, next"
316
- @current-change="getRoleList"
317
- @size-change="getRoleList"
318
- />
319
- </div>
320
- </div>
321
-
322
- <!-- 编辑弹窗 -->
323
- <Modal v-model="modalVisible" :title="modalTitle" width="520px" class="role-modal">
324
- <Form ref="formRef" :model="formData" :rules="rules" label-width="80px" class="role-form">
325
- <FormItem label="角色名称" prop="name">
326
- <Input v-model="formData.name" placeholder="请输入角色名称" />
327
- </FormItem>
328
- <FormItem label="角色编码" prop="code">
329
- <Input v-model="formData.code" placeholder="请输入角色编码" />
330
- </FormItem>
331
- <FormItem label="描述">
332
- <Input v-model="formData.description" placeholder="请输入描述" />
333
- </FormItem>
334
- <FormItem label="状态">
335
- <div class="status-field">
336
- <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
337
- <span class="status-label">{{ formData.status === Status.ENABLED ? '启用' : '禁用' }}</span>
338
- </div>
339
- </FormItem>
340
- </Form>
341
- <template #footer>
342
- <Space>
343
- <Button @click="modalVisible = false">取消</Button>
344
- <Button type="primary" @click="handleSubmit">确定</Button>
345
- </Space>
346
- </template>
347
- </Modal>
348
- </div>
349
- </template>
350
-
351
- <style lang="scss" scoped>
352
- .role-page {
353
- padding: 24px;
354
- background: var(--bg-color-page);
355
- min-height: 100%;
356
- }
357
-
358
- // 搜索区域
359
- .search-section {
360
- background: var(--bg-color);
361
- border-radius: var(--border-radius-large);
362
- padding: 20px 24px;
363
- margin-bottom: 16px;
364
- box-shadow: var(--box-shadow-card);
365
-
366
- .search-form {
367
- display: flex;
368
- flex-wrap: wrap;
369
- gap: 16px;
370
-
371
- :deep(.x-form-item) {
372
- margin-bottom: 0;
373
- }
374
-
375
- :deep(.x-input),
376
- :deep(.x-select) {
377
- width: 200px;
378
- }
379
- }
380
- }
381
-
382
- // 表格区域
383
- .table-section {
384
- background: var(--bg-color);
385
- border-radius: var(--border-radius-large);
386
- box-shadow: var(--box-shadow-card);
387
- overflow: hidden;
388
- }
389
-
390
- .table-toolbar {
391
- display: flex;
392
- justify-content: space-between;
393
- align-items: center;
394
- padding: 16px 24px;
395
- border-bottom: 1px solid var(--color-border-lighter);
396
-
397
- .toolbar-left {
398
- display: flex;
399
- gap: 12px;
400
- }
401
-
402
- .table-count {
403
- font-size: 14px;
404
- color: var(--color-text-secondary);
405
- }
406
- }
407
-
408
- .table-wrapper {
409
- overflow-x: auto;
410
- }
411
-
412
- // 表格样式
413
- .data-table {
414
- width: 100%;
415
- border-collapse: collapse;
416
-
417
- th, td {
418
- padding: 14px 16px;
419
- text-align: left;
420
- border-bottom: 1px solid var(--color-border-lighter);
421
- }
422
-
423
- th {
424
- font-size: 14px;
425
- font-weight: 500;
426
- color: var(--color-text-secondary);
427
- background: var(--color-fill-light);
428
- white-space: nowrap;
429
- }
430
-
431
- .data-row {
432
- transition: background-color 0.2s;
433
-
434
- &:hover {
435
- background: var(--color-primary-light-6);
436
- }
437
- }
438
-
439
- td {
440
- vertical-align: middle;
441
- }
442
-
443
- .loading-cell,
444
- .empty-cell {
445
- padding: 60px 20px;
446
- }
447
-
448
- .loading-content,
449
- .empty-content {
450
- display: flex;
451
- flex-direction: column;
452
- align-items: center;
453
- gap: 16px;
454
- color: var(--color-text-placeholder);
455
-
456
- svg {
457
- width: 64px;
458
- height: 41px;
459
- }
460
- }
461
-
462
- .loading-spinner {
463
- width: 32px;
464
- height: 32px;
465
- border: 3px solid var(--color-border-lighter);
466
- border-top-color: var(--color-primary);
467
- border-radius: 50%;
468
- animation: spin 0.8s linear infinite;
469
- }
470
- }
471
-
472
- @keyframes spin {
473
- to { transform: rotate(360deg); }
474
- }
475
-
476
- // 列样式
477
- .col-id {
478
- width: 80px;
479
- }
480
-
481
- .id-badge {
482
- display: inline-block;
483
- min-width: 32px;
484
- padding: 4px 10px;
485
- background: var(--color-fill);
486
- border-radius: 12px;
487
- font-size: 12px;
488
- font-weight: 500;
489
- color: var(--color-text-secondary);
490
- text-align: center;
491
- }
492
-
493
- .col-name {
494
- min-width: 120px;
495
- }
496
-
497
- .role-name {
498
- font-size: 14px;
499
- font-weight: 500;
500
- color: var(--color-text-primary);
501
- }
502
-
503
- .col-code {
504
- min-width: 100px;
505
- }
506
-
507
- .col-desc {
508
- min-width: 150px;
509
- }
510
-
511
- .desc-text {
512
- font-size: 14px;
513
- color: var(--color-text-secondary);
514
- }
515
-
516
- .col-status {
517
- min-width: 120px;
518
- }
519
-
520
- .status-switch {
521
- display: flex;
522
- align-items: center;
523
- gap: 8px;
524
- }
525
-
526
- .status-text {
527
- font-size: 13px;
528
- color: var(--color-text-secondary);
529
-
530
- &.enabled {
531
- color: var(--color-success);
532
- }
533
- }
534
-
535
- .col-time {
536
- min-width: 160px;
537
- }
538
-
539
- .time-text {
540
- font-size: 13px;
541
- color: var(--color-text-secondary);
542
- }
543
-
544
- .col-actions {
545
- min-width: 140px;
546
- }
547
-
548
- .action-buttons {
549
- :deep(.x-button) {
550
- padding: 4px 8px;
551
-
552
- svg {
553
- width: 14px;
554
- height: 14px;
555
- margin-right: 4px;
556
- }
557
- }
558
- }
559
-
560
- // 分页
561
- .pagination-wrapper {
562
- display: flex;
563
- justify-content: flex-end;
564
- padding: 16px 24px;
565
- border-top: 1px solid var(--color-border-lighter);
566
- }
567
-
568
- // 弹窗表单
569
- .role-form {
570
- padding: 16px 0;
571
-
572
- .status-field {
573
- display: flex;
574
- align-items: center;
575
- gap: 12px;
576
- }
577
-
578
- .status-label {
579
- font-size: 14px;
580
- color: var(--color-text-secondary);
581
- }
582
- }
583
- </style>
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, 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
+ // 状态切换
136
+ const handleStatusChange = (row: Role) => {
137
+ Message.success(`已${row.status === Status.ENABLED ? '启用' : '禁用'}角色 ${row.name}`)
138
+ }
139
+
140
+ onMounted(() => {
141
+ getRoleList()
142
+ })
143
+ </script>
144
+
145
+ <template>
146
+ <div class="role-page">
147
+ <!-- 搜索栏 -->
148
+ <div class="search-section">
149
+ <Form :model="searchForm" inline class="search-form">
150
+ <FormItem label="关键词">
151
+ <Input
152
+ v-model="searchForm.keyword"
153
+ placeholder="角色名称/编码"
154
+ clearable
155
+ @keyup.enter="handleSearch"
156
+ />
157
+ </FormItem>
158
+ <FormItem label="状态">
159
+ <Select
160
+ v-model="searchForm.status"
161
+ :options="StatusOptions"
162
+ placeholder="请选择"
163
+ clearable
164
+ />
165
+ </FormItem>
166
+ <FormItem>
167
+ <Space>
168
+ <Button type="primary" @click="handleSearch">
169
+ <template #icon>
170
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
+ <circle cx="11" cy="11" r="8"/>
172
+ <path d="M21 21l-4.35-4.35"/>
173
+ </svg>
174
+ </template>
175
+ 搜索
176
+ </Button>
177
+ <Button @click="handleReset">
178
+ <template #icon>
179
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
180
+ <path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/>
181
+ <path d="M3 3v5h5"/>
182
+ </svg>
183
+ </template>
184
+ 重置
185
+ </Button>
186
+ </Space>
187
+ </FormItem>
188
+ </Form>
189
+ </div>
190
+
191
+ <!-- 表格 -->
192
+ <div class="table-section">
193
+ <!-- 工具栏 -->
194
+ <div class="table-toolbar">
195
+ <div class="toolbar-left">
196
+ <Button type="primary" @click="handleAdd">
197
+ <template #icon>
198
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
199
+ <line x1="12" y1="5" x2="12" y2="19"/>
200
+ <line x1="5" y1="12" x2="19" y2="12"/>
201
+ </svg>
202
+ </template>
203
+ 新增角色
204
+ </Button>
205
+ </div>
206
+ <div class="toolbar-right">
207
+ <span class="table-count">共 {{ total }} 条数据</span>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- 表格 -->
212
+ <div class="table-wrapper">
213
+ <table class="data-table">
214
+ <thead>
215
+ <tr>
216
+ <th class="col-id">ID</th>
217
+ <th class="col-name">角色名称</th>
218
+ <th class="col-code">角色编码</th>
219
+ <th class="col-desc">描述</th>
220
+ <th class="col-status">状态</th>
221
+ <th class="col-time">创建时间</th>
222
+ <th class="col-actions">操作</th>
223
+ </tr>
224
+ </thead>
225
+ <tbody>
226
+ <tr v-if="loading">
227
+ <td colspan="7" class="loading-cell">
228
+ <div class="loading-content">
229
+ <div class="loading-spinner"></div>
230
+ <span>加载中...</span>
231
+ </div>
232
+ </td>
233
+ </tr>
234
+ <tr v-else-if="roleList.length === 0">
235
+ <td colspan="7" class="empty-cell">
236
+ <div class="empty-content">
237
+ <svg viewBox="0 0 64 41" fill="none">
238
+ <g transform="translate(0 1)">
239
+ <ellipse fill="#f5f5f5" cx="32" cy="33" rx="32" ry="7"/>
240
+ <g stroke="var(--color-text-placeholder)" stroke-width="2">
241
+ <path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"/>
242
+ <path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35H11.95C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="var(--color-fill)"/>
243
+ </g>
244
+ </g>
245
+ </svg>
246
+ <span>暂无数据</span>
247
+ </div>
248
+ </td>
249
+ </tr>
250
+ <tr v-else v-for="row in roleList" :key="row.id" class="data-row">
251
+ <td class="col-id">
252
+ <span class="id-badge">{{ row.id }}</span>
253
+ </td>
254
+ <td class="col-name">
255
+ <span class="role-name">{{ row.name }}</span>
256
+ </td>
257
+ <td class="col-code">
258
+ <Tag type="primary" size="small">{{ row.code }}</Tag>
259
+ </td>
260
+ <td class="col-desc">
261
+ <span class="desc-text">{{ row.description }}</span>
262
+ </td>
263
+ <td class="col-status">
264
+ <div class="status-switch">
265
+ <Switch
266
+ :model-value="row.status === Status.ENABLED"
267
+ @update:model-value="row.status = $event ? Status.ENABLED : Status.DISABLED; handleStatusChange(row)"
268
+ />
269
+ <span class="status-text" :class="{ enabled: row.status === Status.ENABLED }">
270
+ {{ row.status === Status.ENABLED ? '已启用' : '已禁用' }}
271
+ </span>
272
+ </div>
273
+ </td>
274
+ <td class="col-time">
275
+ <span class="time-text">{{ row.createTime }}</span>
276
+ </td>
277
+ <td class="col-actions">
278
+ <Space class="action-buttons">
279
+ <Button type="primary" link size="small" @click="handleEdit(row)">
280
+ <template #icon>
281
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
282
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
283
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
284
+ </svg>
285
+ </template>
286
+ 编辑
287
+ </Button>
288
+ <Popconfirm title="确定删除该角色吗?" @confirm="handleDelete(row.id)">
289
+ <Button type="danger" link size="small">
290
+ <template #icon>
291
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
292
+ <polyline points="3,6 5,6 21,6"/>
293
+ <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
294
+ <line x1="10" y1="11" x2="10" y2="17"/>
295
+ <line x1="14" y1="11" x2="14" y2="17"/>
296
+ </svg>
297
+ </template>
298
+ 删除
299
+ </Button>
300
+ </Popconfirm>
301
+ </Space>
302
+ </td>
303
+ </tr>
304
+ </tbody>
305
+ </table>
306
+ </div>
307
+
308
+ <!-- 分页 -->
309
+ <div class="pagination-wrapper">
310
+ <Pagination
311
+ v-model:current-page="currentPage"
312
+ v-model:page-size="pageSize"
313
+ :total="total"
314
+ :page-sizes="[10, 20, 50, 100]"
315
+ layout="total, sizes, prev, pager, next"
316
+ @current-change="getRoleList"
317
+ @size-change="getRoleList"
318
+ />
319
+ </div>
320
+ </div>
321
+
322
+ <!-- 编辑弹窗 -->
323
+ <Modal v-model="modalVisible" :title="modalTitle" width="520px" class="role-modal">
324
+ <Form ref="formRef" :model="formData" :rules="rules" label-width="80px" class="role-form">
325
+ <FormItem label="角色名称" prop="name">
326
+ <Input v-model="formData.name" placeholder="请输入角色名称" />
327
+ </FormItem>
328
+ <FormItem label="角色编码" prop="code">
329
+ <Input v-model="formData.code" placeholder="请输入角色编码" />
330
+ </FormItem>
331
+ <FormItem label="描述">
332
+ <Input v-model="formData.description" placeholder="请输入描述" />
333
+ </FormItem>
334
+ <FormItem label="状态">
335
+ <div class="status-field">
336
+ <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
337
+ <span class="status-label">{{ formData.status === Status.ENABLED ? '启用' : '禁用' }}</span>
338
+ </div>
339
+ </FormItem>
340
+ </Form>
341
+ <template #footer>
342
+ <Space>
343
+ <Button @click="modalVisible = false">取消</Button>
344
+ <Button type="primary" @click="handleSubmit">确定</Button>
345
+ </Space>
346
+ </template>
347
+ </Modal>
348
+ </div>
349
+ </template>
350
+
351
+ <style lang="scss" scoped>
352
+ .role-page {
353
+ padding: 24px;
354
+ background: var(--bg-color-page);
355
+ min-height: 100%;
356
+ }
357
+
358
+ // 搜索区域
359
+ .search-section {
360
+ background: var(--bg-color);
361
+ border-radius: var(--border-radius-large);
362
+ padding: 20px 24px;
363
+ margin-bottom: 16px;
364
+ box-shadow: var(--box-shadow-card);
365
+
366
+ .search-form {
367
+ display: flex;
368
+ flex-wrap: wrap;
369
+ gap: 16px;
370
+
371
+ :deep(.x-form-item) {
372
+ margin-bottom: 0;
373
+ }
374
+
375
+ :deep(.x-input),
376
+ :deep(.x-select) {
377
+ width: 200px;
378
+ }
379
+ }
380
+ }
381
+
382
+ // 表格区域
383
+ .table-section {
384
+ background: var(--bg-color);
385
+ border-radius: var(--border-radius-large);
386
+ box-shadow: var(--box-shadow-card);
387
+ overflow: hidden;
388
+ }
389
+
390
+ .table-toolbar {
391
+ display: flex;
392
+ justify-content: space-between;
393
+ align-items: center;
394
+ padding: 16px 24px;
395
+ border-bottom: 1px solid var(--color-border-lighter);
396
+
397
+ .toolbar-left {
398
+ display: flex;
399
+ gap: 12px;
400
+ }
401
+
402
+ .table-count {
403
+ font-size: 14px;
404
+ color: var(--color-text-secondary);
405
+ }
406
+ }
407
+
408
+ .table-wrapper {
409
+ overflow-x: auto;
410
+ }
411
+
412
+ // 表格样式
413
+ .data-table {
414
+ width: 100%;
415
+ border-collapse: collapse;
416
+
417
+ th, td {
418
+ padding: 14px 16px;
419
+ text-align: left;
420
+ border-bottom: 1px solid var(--color-border-lighter);
421
+ }
422
+
423
+ th {
424
+ font-size: 14px;
425
+ font-weight: 500;
426
+ color: var(--color-text-secondary);
427
+ background: var(--color-fill-light);
428
+ white-space: nowrap;
429
+ }
430
+
431
+ .data-row {
432
+ transition: background-color 0.2s;
433
+
434
+ &:hover {
435
+ background: var(--color-primary-light-6);
436
+ }
437
+ }
438
+
439
+ td {
440
+ vertical-align: middle;
441
+ }
442
+
443
+ .loading-cell,
444
+ .empty-cell {
445
+ padding: 60px 20px;
446
+ }
447
+
448
+ .loading-content,
449
+ .empty-content {
450
+ display: flex;
451
+ flex-direction: column;
452
+ align-items: center;
453
+ gap: 16px;
454
+ color: var(--color-text-placeholder);
455
+
456
+ svg {
457
+ width: 64px;
458
+ height: 41px;
459
+ }
460
+ }
461
+
462
+ .loading-spinner {
463
+ width: 32px;
464
+ height: 32px;
465
+ border: 3px solid var(--color-border-lighter);
466
+ border-top-color: var(--color-primary);
467
+ border-radius: 50%;
468
+ animation: spin 0.8s linear infinite;
469
+ }
470
+ }
471
+
472
+ @keyframes spin {
473
+ to { transform: rotate(360deg); }
474
+ }
475
+
476
+ // 列样式
477
+ .col-id {
478
+ width: 80px;
479
+ }
480
+
481
+ .id-badge {
482
+ display: inline-block;
483
+ min-width: 32px;
484
+ padding: 4px 10px;
485
+ background: var(--color-fill);
486
+ border-radius: 12px;
487
+ font-size: 12px;
488
+ font-weight: 500;
489
+ color: var(--color-text-secondary);
490
+ text-align: center;
491
+ }
492
+
493
+ .col-name {
494
+ min-width: 120px;
495
+ }
496
+
497
+ .role-name {
498
+ font-size: 14px;
499
+ font-weight: 500;
500
+ color: var(--color-text-primary);
501
+ }
502
+
503
+ .col-code {
504
+ min-width: 100px;
505
+ }
506
+
507
+ .col-desc {
508
+ min-width: 150px;
509
+ }
510
+
511
+ .desc-text {
512
+ font-size: 14px;
513
+ color: var(--color-text-secondary);
514
+ }
515
+
516
+ .col-status {
517
+ min-width: 120px;
518
+ }
519
+
520
+ .status-switch {
521
+ display: flex;
522
+ align-items: center;
523
+ gap: 8px;
524
+ }
525
+
526
+ .status-text {
527
+ font-size: 13px;
528
+ color: var(--color-text-secondary);
529
+
530
+ &.enabled {
531
+ color: var(--color-success);
532
+ }
533
+ }
534
+
535
+ .col-time {
536
+ min-width: 160px;
537
+ }
538
+
539
+ .time-text {
540
+ font-size: 13px;
541
+ color: var(--color-text-secondary);
542
+ }
543
+
544
+ .col-actions {
545
+ min-width: 140px;
546
+ }
547
+
548
+ .action-buttons {
549
+ :deep(.x-button) {
550
+ padding: 4px 8px;
551
+
552
+ svg {
553
+ width: 14px;
554
+ height: 14px;
555
+ margin-right: 4px;
556
+ }
557
+ }
558
+ }
559
+
560
+ // 分页
561
+ .pagination-wrapper {
562
+ display: flex;
563
+ justify-content: flex-end;
564
+ padding: 16px 24px;
565
+ border-top: 1px solid var(--color-border-lighter);
566
+ }
567
+
568
+ // 弹窗表单
569
+ .role-form {
570
+ padding: 16px 0;
571
+
572
+ .status-field {
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 12px;
576
+ }
577
+
578
+ .status-label {
579
+ font-size: 14px;
580
+ color: var(--color-text-secondary);
581
+ }
582
+ }
583
+ </style>