create-simpleadmin-ui 2.0.0

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 (55) hide show
  1. package/README.md +53 -0
  2. package/bin/create-simpleadmin-ui.mjs +2 -0
  3. package/package.json +35 -0
  4. package/src/index.mjs +557 -0
  5. package/src/sync-template.mjs +64 -0
  6. package/template/.env.development +4 -0
  7. package/template/.env.production.example +4 -0
  8. package/template/.vscode/extensions.json +3 -0
  9. package/template/README.md +26 -0
  10. package/template/index.html +19 -0
  11. package/template/package.json +30 -0
  12. package/template/public/vite.svg +1 -0
  13. package/template/src/App.vue +3 -0
  14. package/template/src/components/AppBreadcrumb.vue +132 -0
  15. package/template/src/components/AppPage.vue +23 -0
  16. package/template/src/components/ChangePasswordDialog.vue +97 -0
  17. package/template/src/components/ProfileEditDialog.vue +142 -0
  18. package/template/src/components/RichTextEditor.vue +121 -0
  19. package/template/src/components/SidebarMenuItem.vue +56 -0
  20. package/template/src/components/TagsView.vue +172 -0
  21. package/template/src/constants/menuIcons.ts +205 -0
  22. package/template/src/directives/permission.ts +13 -0
  23. package/template/src/env.d.ts +22 -0
  24. package/template/src/layouts/MainLayout.vue +410 -0
  25. package/template/src/main.ts +27 -0
  26. package/template/src/router/index.ts +198 -0
  27. package/template/src/stores/tagsView.ts +161 -0
  28. package/template/src/stores/theme.ts +68 -0
  29. package/template/src/stores/user.ts +191 -0
  30. package/template/src/styles/global.css +314 -0
  31. package/template/src/utils/datetime.ts +34 -0
  32. package/template/src/utils/request.ts +324 -0
  33. package/template/src/utils/requestSign.ts +162 -0
  34. package/template/src/views/error/NotFound.vue +49 -0
  35. package/template/src/views/home/HomeView.vue +1177 -0
  36. package/template/src/views/login/LoginView.vue +576 -0
  37. package/template/src/views/monitor/loginlog/index.vue +366 -0
  38. package/template/src/views/monitor/online/index.vue +298 -0
  39. package/template/src/views/monitor/operlog/index.vue +492 -0
  40. package/template/src/views/system/config/index.vue +407 -0
  41. package/template/src/views/system/dept/index.vue +517 -0
  42. package/template/src/views/system/dict/index.vue +747 -0
  43. package/template/src/views/system/file/index.vue +1031 -0
  44. package/template/src/views/system/menu/index.vue +822 -0
  45. package/template/src/views/system/message/index.vue +422 -0
  46. package/template/src/views/system/notice/index.vue +578 -0
  47. package/template/src/views/system/openApp/index.vue +596 -0
  48. package/template/src/views/system/post/index.vue +495 -0
  49. package/template/src/views/system/role/index.vue +546 -0
  50. package/template/src/views/system/user/index.vue +373 -0
  51. package/template/src/vite-env.d.ts +1 -0
  52. package/template/tsconfig.app.json +30 -0
  53. package/template/tsconfig.json +7 -0
  54. package/template/tsconfig.node.json +24 -0
  55. package/template/vite.config.ts +22 -0
@@ -0,0 +1,747 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref } from 'vue'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
+ import { Plus, Refresh, Search, CollectionTag } from '@element-plus/icons-vue'
5
+ import { get, post, put, del } from '@/utils/request'
6
+ import AppPage from '@/components/AppPage.vue'
7
+
8
+ interface DictTypeItem {
9
+ id: number
10
+ dictName: string
11
+ dictType: string
12
+ status: number
13
+ remark?: string
14
+ }
15
+
16
+ interface DictDataItem {
17
+ id: number
18
+ dictTypeId: number
19
+ dictType: string
20
+ dictLabel: string
21
+ dictValue: string
22
+ orderNum: number
23
+ status: number
24
+ isDefault: boolean
25
+ }
26
+
27
+ interface TypeForm {
28
+ id?: number
29
+ dictName: string
30
+ dictType: string
31
+ status: number
32
+ remark: string
33
+ }
34
+
35
+ interface DataForm {
36
+ id?: number
37
+ dictTypeId: number
38
+ dictType: string
39
+ dictLabel: string
40
+ dictValue: string
41
+ orderNum: number
42
+ cssClass: string
43
+ listClass: string
44
+ isDefault: boolean
45
+ status: number
46
+ remark: string
47
+ }
48
+
49
+ const typeLoading = ref(false)
50
+ const dataLoading = ref(false)
51
+ const saving = ref(false)
52
+ const typeTotal = ref(0)
53
+ const dataTotal = ref(0)
54
+ const types = ref<DictTypeItem[]>([])
55
+ const datas = ref<DictDataItem[]>([])
56
+ const selectedType = ref<DictTypeItem | null>(null)
57
+
58
+ const typeQuery = reactive({ pageIndex: 1, pageSize: 10, keyword: '', status: undefined as number | undefined })
59
+ const dataQuery = reactive({ pageIndex: 1, pageSize: 10, keyword: '' })
60
+
61
+ const typeDialog = ref(false)
62
+ const dataDialog = ref(false)
63
+
64
+ const typeForm = reactive<TypeForm>({
65
+ id: undefined,
66
+ dictName: '',
67
+ dictType: '',
68
+ status: 0,
69
+ remark: '',
70
+ })
71
+
72
+ const dataForm = reactive<DataForm>({
73
+ id: undefined,
74
+ dictTypeId: 0,
75
+ dictType: '',
76
+ dictLabel: '',
77
+ dictValue: '',
78
+ orderNum: 0,
79
+ cssClass: '',
80
+ listClass: '',
81
+ isDefault: false,
82
+ status: 0,
83
+ remark: '',
84
+ })
85
+
86
+ const typeStats = computed(() => {
87
+ let normal = 0
88
+ let disabled = 0
89
+ for (const t of types.value) {
90
+ if (t.status === 0) normal++
91
+ else disabled++
92
+ }
93
+ return { normal, disabled }
94
+ })
95
+
96
+ async function loadTypes() {
97
+ typeLoading.value = true
98
+ try {
99
+ const params: Record<string, unknown> = {
100
+ pageIndex: typeQuery.pageIndex,
101
+ pageSize: typeQuery.pageSize,
102
+ keyword: typeQuery.keyword || undefined,
103
+ }
104
+ if (typeQuery.status !== undefined && typeQuery.status !== null) params.status = typeQuery.status
105
+ const data = await get<{ total: number; items: DictTypeItem[] }>('/dict-types', params)
106
+ typeTotal.value = data.total
107
+ types.value = data.items || []
108
+ if (selectedType.value) {
109
+ const still = types.value.find((t) => t.id === selectedType.value!.id)
110
+ if (still) selectedType.value = still
111
+ else {
112
+ selectedType.value = null
113
+ datas.value = []
114
+ dataTotal.value = 0
115
+ }
116
+ }
117
+ } finally {
118
+ typeLoading.value = false
119
+ }
120
+ }
121
+
122
+ async function loadDatas() {
123
+ if (!selectedType.value) {
124
+ datas.value = []
125
+ dataTotal.value = 0
126
+ return
127
+ }
128
+ dataLoading.value = true
129
+ try {
130
+ const data = await get<{ total: number; items: DictDataItem[] }>('/dict-data', {
131
+ pageIndex: dataQuery.pageIndex,
132
+ pageSize: dataQuery.pageSize,
133
+ dictTypeId: selectedType.value.id,
134
+ keyword: dataQuery.keyword || undefined,
135
+ })
136
+ dataTotal.value = data.total
137
+ datas.value = data.items || []
138
+ } finally {
139
+ dataLoading.value = false
140
+ }
141
+ }
142
+
143
+ function searchTypes() {
144
+ typeQuery.pageIndex = 1
145
+ loadTypes()
146
+ }
147
+
148
+ function resetTypeQuery() {
149
+ typeQuery.keyword = ''
150
+ typeQuery.status = undefined
151
+ typeQuery.pageIndex = 1
152
+ loadTypes()
153
+ }
154
+
155
+ function selectType(row: DictTypeItem) {
156
+ selectedType.value = row
157
+ dataQuery.pageIndex = 1
158
+ dataQuery.keyword = ''
159
+ loadDatas()
160
+ }
161
+
162
+ function openCreateType() {
163
+ Object.assign(typeForm, { id: undefined, dictName: '', dictType: '', status: 0, remark: '' })
164
+ typeDialog.value = true
165
+ }
166
+
167
+ async function openEditType(row: DictTypeItem) {
168
+ const detail = await get<TypeForm>(`/dict-types/${row.id}`)
169
+ Object.assign(typeForm, {
170
+ id: detail.id,
171
+ dictName: detail.dictName || '',
172
+ dictType: detail.dictType || '',
173
+ status: detail.status ?? 0,
174
+ remark: detail.remark || '',
175
+ })
176
+ typeDialog.value = true
177
+ }
178
+
179
+ async function saveType() {
180
+ if (!typeForm.dictName.trim() || !typeForm.dictType.trim()) {
181
+ ElMessage.warning('请填写字典名称和类型')
182
+ return
183
+ }
184
+ saving.value = true
185
+ try {
186
+ const payload = {
187
+ id: typeForm.id,
188
+ dictName: typeForm.dictName.trim(),
189
+ dictType: typeForm.dictType.trim(),
190
+ status: typeForm.status,
191
+ remark: typeForm.remark.trim() || null,
192
+ }
193
+ if (typeForm.id) await put('/dict-types', payload)
194
+ else await post('/dict-types', payload)
195
+ ElMessage.success('保存成功')
196
+ typeDialog.value = false
197
+ loadTypes()
198
+ } finally {
199
+ saving.value = false
200
+ }
201
+ }
202
+
203
+ async function removeType(row: DictTypeItem) {
204
+ await ElMessageBox.confirm(`确认删除字典类型「${row.dictName}」?其下字典数据需自行确认。`, '提示', {
205
+ type: 'warning',
206
+ })
207
+ await del('/dict-types', { ids: [row.id] })
208
+ ElMessage.success('已删除')
209
+ if (selectedType.value?.id === row.id) {
210
+ selectedType.value = null
211
+ datas.value = []
212
+ dataTotal.value = 0
213
+ }
214
+ loadTypes()
215
+ }
216
+
217
+ function openCreateData() {
218
+ if (!selectedType.value) {
219
+ ElMessage.warning('请先选择字典类型')
220
+ return
221
+ }
222
+ Object.assign(dataForm, {
223
+ id: undefined,
224
+ dictTypeId: selectedType.value.id,
225
+ dictType: selectedType.value.dictType,
226
+ dictLabel: '',
227
+ dictValue: '',
228
+ orderNum: 0,
229
+ cssClass: '',
230
+ listClass: '',
231
+ isDefault: false,
232
+ status: 0,
233
+ remark: '',
234
+ })
235
+ dataDialog.value = true
236
+ }
237
+
238
+ async function openEditData(row: DictDataItem) {
239
+ const detail = await get<DataForm>(`/dict-data/${row.id}`)
240
+ Object.assign(dataForm, {
241
+ id: detail.id,
242
+ dictTypeId: detail.dictTypeId,
243
+ dictType: detail.dictType || '',
244
+ dictLabel: detail.dictLabel || '',
245
+ dictValue: detail.dictValue || '',
246
+ orderNum: detail.orderNum ?? 0,
247
+ cssClass: detail.cssClass || '',
248
+ listClass: detail.listClass || '',
249
+ isDefault: detail.isDefault ?? false,
250
+ status: detail.status ?? 0,
251
+ remark: detail.remark || '',
252
+ })
253
+ dataDialog.value = true
254
+ }
255
+
256
+ async function saveData() {
257
+ if (!dataForm.dictLabel.trim() || !dataForm.dictValue.trim()) {
258
+ ElMessage.warning('请填写标签和键值')
259
+ return
260
+ }
261
+ saving.value = true
262
+ try {
263
+ const payload = {
264
+ id: dataForm.id,
265
+ dictTypeId: dataForm.dictTypeId,
266
+ dictType: dataForm.dictType,
267
+ dictLabel: dataForm.dictLabel.trim(),
268
+ dictValue: dataForm.dictValue.trim(),
269
+ orderNum: dataForm.orderNum,
270
+ cssClass: dataForm.cssClass.trim() || null,
271
+ listClass: dataForm.listClass.trim() || null,
272
+ isDefault: dataForm.isDefault,
273
+ status: dataForm.status,
274
+ remark: dataForm.remark.trim() || null,
275
+ }
276
+ if (dataForm.id) await put('/dict-data', payload)
277
+ else await post('/dict-data', payload)
278
+ ElMessage.success('保存成功')
279
+ dataDialog.value = false
280
+ loadDatas()
281
+ } finally {
282
+ saving.value = false
283
+ }
284
+ }
285
+
286
+ async function removeData(row: DictDataItem) {
287
+ await ElMessageBox.confirm(`确认删除字典项「${row.dictLabel}」?`, '提示', { type: 'warning' })
288
+ await del('/dict-data', { ids: [row.id] })
289
+ ElMessage.success('已删除')
290
+ loadDatas()
291
+ }
292
+
293
+ onMounted(loadTypes)
294
+ </script>
295
+
296
+ <template>
297
+ <AppPage title="字典管理" description="维护业务字典类型及枚举项">
298
+ <div class="dict-stats">
299
+ <div class="dict-stats__item">
300
+ <span class="dict-stats__num">{{ typeTotal }}</span>
301
+ <span class="dict-stats__label">字典类型</span>
302
+ </div>
303
+ <div class="dict-stats__item dict-stats__item--ok">
304
+ <span class="dict-stats__num">{{ typeStats.normal }}</span>
305
+ <span class="dict-stats__label">本页正常</span>
306
+ </div>
307
+ <div class="dict-stats__item dict-stats__item--off">
308
+ <span class="dict-stats__num">{{ typeStats.disabled }}</span>
309
+ <span class="dict-stats__label">本页停用</span>
310
+ </div>
311
+ <div class="dict-stats__item dict-stats__item--hit">
312
+ <span class="dict-stats__num">{{ selectedType ? dataTotal : '—' }}</span>
313
+ <span class="dict-stats__label">选中类型数据</span>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="dict-layout">
318
+ <section class="dict-pane">
319
+ <div class="toolbar">
320
+ <el-input
321
+ v-model="typeQuery.keyword"
322
+ clearable
323
+ placeholder="搜索名称 / 类型"
324
+ style="width: 160px"
325
+ @keyup.enter="searchTypes"
326
+ />
327
+ <el-select
328
+ v-model="typeQuery.status"
329
+ clearable
330
+ placeholder="状态"
331
+ style="width: 100px"
332
+ @change="searchTypes"
333
+ >
334
+ <el-option label="正常" :value="0" />
335
+ <el-option label="停用" :value="1" />
336
+ </el-select>
337
+ <el-button type="primary" :icon="Search" @click="searchTypes" />
338
+ <el-button :icon="Refresh" @click="resetTypeQuery" />
339
+ <div class="toolbar__spacer" />
340
+ <el-button type="success" :icon="Plus" v-permission="'system:dict:add'" @click="openCreateType">
341
+ 新增类型
342
+ </el-button>
343
+ </div>
344
+
345
+ <div class="table-wrap" v-loading="typeLoading">
346
+ <el-table
347
+ :data="types"
348
+ border
349
+ stripe
350
+ highlight-current-row
351
+ empty-text="暂无字典类型"
352
+ @row-click="selectType"
353
+ >
354
+ <el-table-column label="字典名称" min-width="120" show-overflow-tooltip>
355
+ <template #default="{ row }">
356
+ <div class="name-cell">
357
+ <span class="name-cell__icon">
358
+ <el-icon :size="14"><CollectionTag /></el-icon>
359
+ </span>
360
+ <strong>{{ row.dictName }}</strong>
361
+ </div>
362
+ </template>
363
+ </el-table-column>
364
+ <el-table-column label="类型" min-width="110" show-overflow-tooltip>
365
+ <template #default="{ row }">
366
+ <code class="mono">{{ row.dictType }}</code>
367
+ </template>
368
+ </el-table-column>
369
+ <el-table-column label="状态" width="72" align="center">
370
+ <template #default="{ row }">
371
+ <el-tag size="small" :type="row.status === 0 ? 'success' : 'danger'">
372
+ {{ row.status === 0 ? '正常' : '停用' }}
373
+ </el-tag>
374
+ </template>
375
+ </el-table-column>
376
+ <el-table-column label="操作" width="180" align="center" fixed="right">
377
+ <template #default="{ row }">
378
+ <div class="row-actions">
379
+ <el-button
380
+ size="small"
381
+ type="primary"
382
+ v-permission="'system:dict:edit'"
383
+ @click.stop="openEditType(row)"
384
+ >
385
+ 编辑
386
+ </el-button>
387
+ <el-button
388
+ size="small"
389
+ type="danger"
390
+ v-permission="'system:dict:remove'"
391
+ @click.stop="removeType(row)"
392
+ >
393
+ 删除
394
+ </el-button>
395
+ </div>
396
+ </template>
397
+ </el-table-column>
398
+ </el-table>
399
+ </div>
400
+ <div class="pager">
401
+ <el-pagination
402
+ small
403
+ background
404
+ layout="total, prev, pager, next"
405
+ :total="typeTotal"
406
+ v-model:current-page="typeQuery.pageIndex"
407
+ v-model:page-size="typeQuery.pageSize"
408
+ @current-change="loadTypes"
409
+ />
410
+ </div>
411
+ </section>
412
+
413
+ <section class="dict-pane">
414
+ <div class="toolbar">
415
+ <span class="pane-title">
416
+ 字典数据
417
+ <template v-if="selectedType">
418
+ · <code class="mono">{{ selectedType.dictType }}</code>
419
+ </template>
420
+ <span v-else class="muted">(请选择左侧类型)</span>
421
+ </span>
422
+ <el-input
423
+ v-model="dataQuery.keyword"
424
+ clearable
425
+ placeholder="搜索标签"
426
+ style="width: 140px"
427
+ :disabled="!selectedType"
428
+ @keyup.enter="loadDatas"
429
+ />
430
+ <el-button type="primary" :icon="Search" :disabled="!selectedType" @click="loadDatas" />
431
+ <div class="toolbar__spacer" />
432
+ <el-button
433
+ type="success"
434
+ :icon="Plus"
435
+ :disabled="!selectedType"
436
+ v-permission="'system:dict:add'"
437
+ @click="openCreateData"
438
+ >
439
+ 新增数据
440
+ </el-button>
441
+ </div>
442
+
443
+ <div class="table-wrap" v-loading="dataLoading">
444
+ <el-table :data="datas" border stripe empty-text="暂无字典数据">
445
+ <el-table-column prop="dictLabel" label="标签" min-width="120" show-overflow-tooltip />
446
+ <el-table-column label="键值" min-width="100" show-overflow-tooltip>
447
+ <template #default="{ row }">
448
+ <code class="mono">{{ row.dictValue }}</code>
449
+ </template>
450
+ </el-table-column>
451
+ <el-table-column prop="orderNum" label="排序" width="64" align="center" />
452
+ <el-table-column label="默认" width="64" align="center">
453
+ <template #default="{ row }">
454
+ <el-tag size="small" :type="row.isDefault ? 'warning' : 'info'">
455
+ {{ row.isDefault ? '是' : '否' }}
456
+ </el-tag>
457
+ </template>
458
+ </el-table-column>
459
+ <el-table-column label="状态" width="72" align="center">
460
+ <template #default="{ row }">
461
+ <el-tag size="small" :type="row.status === 0 ? 'success' : 'danger'">
462
+ {{ row.status === 0 ? '正常' : '停用' }}
463
+ </el-tag>
464
+ </template>
465
+ </el-table-column>
466
+ <el-table-column label="操作" width="180" align="center" fixed="right">
467
+ <template #default="{ row }">
468
+ <div class="row-actions">
469
+ <el-button
470
+ size="small"
471
+ type="primary"
472
+ v-permission="'system:dict:edit'"
473
+ @click="openEditData(row)"
474
+ >
475
+ 编辑
476
+ </el-button>
477
+ <el-button
478
+ size="small"
479
+ type="danger"
480
+ v-permission="'system:dict:remove'"
481
+ @click="removeData(row)"
482
+ >
483
+ 删除
484
+ </el-button>
485
+ </div>
486
+ </template>
487
+ </el-table-column>
488
+ </el-table>
489
+ </div>
490
+ <div class="pager">
491
+ <el-pagination
492
+ small
493
+ background
494
+ layout="total, prev, pager, next"
495
+ :total="dataTotal"
496
+ v-model:current-page="dataQuery.pageIndex"
497
+ v-model:page-size="dataQuery.pageSize"
498
+ @current-change="loadDatas"
499
+ />
500
+ </div>
501
+ </section>
502
+ </div>
503
+
504
+ <el-dialog
505
+ v-model="typeDialog"
506
+ :title="typeForm.id ? '编辑字典类型' : '新增字典类型'"
507
+ width="520px"
508
+ class="sa-dialog"
509
+ append-to-body
510
+ align-center
511
+ destroy-on-close
512
+ >
513
+ <el-form label-width="96px">
514
+ <el-form-item label="字典名称" required>
515
+ <el-input v-model="typeForm.dictName" maxlength="64" placeholder="如:用户性别" />
516
+ </el-form-item>
517
+ <el-form-item label="字典类型" required>
518
+ <el-input v-model="typeForm.dictType" maxlength="64" placeholder="如:sys_user_sex" />
519
+ </el-form-item>
520
+ <el-form-item label="状态">
521
+ <el-radio-group v-model="typeForm.status">
522
+ <el-radio :value="0">正常</el-radio>
523
+ <el-radio :value="1">停用</el-radio>
524
+ </el-radio-group>
525
+ </el-form-item>
526
+ <el-form-item label="备注">
527
+ <el-input v-model="typeForm.remark" type="textarea" :rows="2" maxlength="200" />
528
+ </el-form-item>
529
+ </el-form>
530
+ <template #footer>
531
+ <el-button @click="typeDialog = false">取消</el-button>
532
+ <el-button type="primary" :loading="saving" @click="saveType">保存</el-button>
533
+ </template>
534
+ </el-dialog>
535
+
536
+ <el-dialog
537
+ v-model="dataDialog"
538
+ :title="dataForm.id ? '编辑字典数据' : '新增字典数据'"
539
+ width="560px"
540
+ class="sa-dialog"
541
+ append-to-body
542
+ align-center
543
+ destroy-on-close
544
+ >
545
+ <el-form label-width="96px">
546
+ <el-form-item label="字典类型">
547
+ <el-input :model-value="dataForm.dictType" disabled />
548
+ </el-form-item>
549
+ <el-row>
550
+ <el-col :span="12" class="col-l">
551
+ <el-form-item label="数据标签" required>
552
+ <el-input v-model="dataForm.dictLabel" maxlength="64" />
553
+ </el-form-item>
554
+ </el-col>
555
+ <el-col :span="12" class="col-r">
556
+ <el-form-item label="数据键值" required>
557
+ <el-input v-model="dataForm.dictValue" maxlength="64" />
558
+ </el-form-item>
559
+ </el-col>
560
+ </el-row>
561
+ <el-row>
562
+ <el-col :span="12" class="col-l">
563
+ <el-form-item label="显示排序">
564
+ <el-input-number v-model="dataForm.orderNum" :min="0" controls-position="right" style="width: 100%" />
565
+ </el-form-item>
566
+ </el-col>
567
+ <el-col :span="12" class="col-r">
568
+ <el-form-item label="状态">
569
+ <el-radio-group v-model="dataForm.status">
570
+ <el-radio :value="0">正常</el-radio>
571
+ <el-radio :value="1">停用</el-radio>
572
+ </el-radio-group>
573
+ </el-form-item>
574
+ </el-col>
575
+ </el-row>
576
+ <el-form-item label="是否默认">
577
+ <el-switch v-model="dataForm.isDefault" />
578
+ </el-form-item>
579
+ <el-row>
580
+ <el-col :span="12" class="col-l">
581
+ <el-form-item label="样式属性">
582
+ <el-input v-model="dataForm.cssClass" maxlength="64" placeholder="可选" />
583
+ </el-form-item>
584
+ </el-col>
585
+ <el-col :span="12" class="col-r">
586
+ <el-form-item label="回显样式">
587
+ <el-input v-model="dataForm.listClass" maxlength="64" placeholder="如 success" />
588
+ </el-form-item>
589
+ </el-col>
590
+ </el-row>
591
+ <el-form-item label="备注">
592
+ <el-input v-model="dataForm.remark" type="textarea" :rows="2" maxlength="200" />
593
+ </el-form-item>
594
+ </el-form>
595
+ <template #footer>
596
+ <el-button @click="dataDialog = false">取消</el-button>
597
+ <el-button type="primary" :loading="saving" @click="saveData">保存</el-button>
598
+ </template>
599
+ </el-dialog>
600
+ </AppPage>
601
+ </template>
602
+
603
+ <style scoped lang="scss">
604
+ .dict-stats {
605
+ display: grid;
606
+ grid-template-columns: repeat(4, minmax(0, 1fr));
607
+ gap: 12px;
608
+ margin-bottom: 14px;
609
+ }
610
+ .dict-stats__item {
611
+ display: flex;
612
+ flex-direction: column;
613
+ gap: 4px;
614
+ padding: 14px 16px;
615
+ border-radius: 14px;
616
+ border: 1px solid var(--sa-border);
617
+ background: var(--sa-panel);
618
+ box-shadow: var(--sa-shadow);
619
+ }
620
+ .dict-stats__num {
621
+ font-size: 22px;
622
+ font-weight: 800;
623
+ letter-spacing: -0.03em;
624
+ color: var(--sa-ink);
625
+ font-variant-numeric: tabular-nums;
626
+ }
627
+ .dict-stats__label {
628
+ font-size: 12px;
629
+ color: var(--sa-ink-muted);
630
+ }
631
+ .dict-stats__item--ok .dict-stats__num {
632
+ color: var(--sa-accent);
633
+ }
634
+ .dict-stats__item--off .dict-stats__num {
635
+ color: #be123c;
636
+ }
637
+ .dict-stats__item--hit .dict-stats__num {
638
+ color: #0369a1;
639
+ }
640
+
641
+ .dict-layout {
642
+ display: grid;
643
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
644
+ gap: 14px;
645
+ align-items: start;
646
+ }
647
+
648
+ .dict-pane {
649
+ min-width: 0;
650
+ }
651
+
652
+ .toolbar {
653
+ display: flex;
654
+ flex-wrap: wrap;
655
+ gap: 8px;
656
+ align-items: center;
657
+ margin-bottom: 12px;
658
+ padding: 10px 12px;
659
+ background: var(--sa-toolbar-bg);
660
+ border: 1px solid var(--sa-border);
661
+ border-radius: var(--sa-radius-sm);
662
+ }
663
+ .toolbar__spacer {
664
+ flex: 1;
665
+ }
666
+ .pane-title {
667
+ font-size: 13px;
668
+ font-weight: 650;
669
+ color: var(--sa-ink);
670
+ margin-right: 4px;
671
+ }
672
+
673
+ .table-wrap {
674
+ border: 1px solid var(--sa-border);
675
+ border-radius: 14px;
676
+ overflow: hidden;
677
+ background: var(--sa-panel);
678
+ box-shadow: var(--sa-shadow);
679
+ }
680
+
681
+ .name-cell {
682
+ display: inline-flex;
683
+ align-items: center;
684
+ gap: 8px;
685
+ }
686
+ .name-cell__icon {
687
+ width: 26px;
688
+ height: 26px;
689
+ border-radius: 8px;
690
+ display: grid;
691
+ place-items: center;
692
+ flex-shrink: 0;
693
+ background: var(--sa-accent-soft);
694
+ color: var(--sa-accent);
695
+ }
696
+ .name-cell strong {
697
+ font-size: 13px;
698
+ font-weight: 650;
699
+ }
700
+ .mono {
701
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
702
+ font-size: 12px;
703
+ color: var(--sa-ink-secondary);
704
+ background: var(--sa-toolbar-bg);
705
+ padding: 1px 6px;
706
+ border-radius: 6px;
707
+ }
708
+ .muted {
709
+ color: var(--sa-ink-muted);
710
+ font-weight: 400;
711
+ }
712
+ .pager {
713
+ display: flex;
714
+ justify-content: flex-end;
715
+ margin-top: 10px;
716
+ }
717
+ .col-l {
718
+ padding-right: 8px;
719
+ }
720
+ .col-r {
721
+ padding-left: 8px;
722
+ }
723
+
724
+ @media (max-width: 1100px) {
725
+ .dict-layout {
726
+ grid-template-columns: 1fr;
727
+ }
728
+ .dict-stats {
729
+ grid-template-columns: repeat(2, minmax(0, 1fr));
730
+ }
731
+ }
732
+ </style>
733
+
734
+ <style>
735
+ .sa-dialog.el-dialog {
736
+ width: min(560px, calc(100vw - 32px)) !important;
737
+ max-height: 88vh;
738
+ overflow: hidden;
739
+ display: flex;
740
+ flex-direction: column;
741
+ }
742
+ .sa-dialog .el-dialog__body {
743
+ overflow-x: hidden;
744
+ overflow-y: auto;
745
+ max-height: calc(88vh - 140px);
746
+ }
747
+ </style>