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,517 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref } from 'vue'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
+ import { Plus, Refresh, OfficeBuilding } from '@element-plus/icons-vue'
5
+ import { get, post, put, del } from '@/utils/request'
6
+ import AppPage from '@/components/AppPage.vue'
7
+
8
+ interface DeptNode {
9
+ id: number
10
+ parentId: number
11
+ deptName: string
12
+ deptCode?: string
13
+ orderNum: number
14
+ status: number
15
+ leader?: string
16
+ phone?: string
17
+ email?: string
18
+ children?: DeptNode[]
19
+ }
20
+
21
+ interface DeptForm {
22
+ id?: number
23
+ parentId: number
24
+ deptName: string
25
+ deptCode: string
26
+ orderNum: number
27
+ leader: string
28
+ phone: string
29
+ email: string
30
+ status: number
31
+ remark: string
32
+ }
33
+
34
+ const loading = ref(false)
35
+ const saving = ref(false)
36
+ const tree = ref<DeptNode[]>([])
37
+ const keyword = ref('')
38
+ const dialogVisible = ref(false)
39
+ const expandAll = ref(true)
40
+
41
+ const form = reactive<DeptForm>({
42
+ id: undefined,
43
+ parentId: 0,
44
+ deptName: '',
45
+ deptCode: '',
46
+ orderNum: 0,
47
+ leader: '',
48
+ phone: '',
49
+ email: '',
50
+ status: 0,
51
+ remark: '',
52
+ })
53
+
54
+ const filteredTree = computed(() => {
55
+ const q = keyword.value.trim().toLowerCase()
56
+ if (!q) return tree.value
57
+ const filter = (nodes: DeptNode[]): DeptNode[] => {
58
+ const out: DeptNode[] = []
59
+ for (const n of nodes) {
60
+ const kids = n.children?.length ? filter(n.children) : []
61
+ const hit =
62
+ n.deptName?.toLowerCase().includes(q) ||
63
+ n.deptCode?.toLowerCase().includes(q) ||
64
+ n.leader?.toLowerCase().includes(q) ||
65
+ n.phone?.toLowerCase().includes(q)
66
+ if (hit || kids.length) out.push({ ...n, children: kids })
67
+ }
68
+ return out
69
+ }
70
+ return filter(tree.value)
71
+ })
72
+
73
+ const parentOptions = computed(() => {
74
+ const opts: { id: number; label: string }[] = [{ id: 0, label: '顶级部门' }]
75
+ const walk = (nodes: DeptNode[], prefix = '') => {
76
+ for (const n of nodes) {
77
+ if (form.id && n.id === form.id) continue
78
+ opts.push({ id: n.id, label: `${prefix}${n.deptName}` })
79
+ if (n.children?.length) walk(n.children, `${prefix}${n.deptName} / `)
80
+ }
81
+ }
82
+ walk(tree.value)
83
+ return opts
84
+ })
85
+
86
+ const stats = computed(() => {
87
+ let total = 0
88
+ let normal = 0
89
+ let disabled = 0
90
+ let roots = 0
91
+ const walk = (nodes: DeptNode[], depth: number) => {
92
+ for (const n of nodes) {
93
+ total++
94
+ if (n.status === 0) normal++
95
+ else disabled++
96
+ if (depth === 0) roots++
97
+ if (n.children?.length) walk(n.children, depth + 1)
98
+ }
99
+ }
100
+ walk(tree.value, 0)
101
+ return { total, normal, disabled, roots }
102
+ })
103
+
104
+ async function load() {
105
+ loading.value = true
106
+ try {
107
+ tree.value = (await get<DeptNode[]>('/depts/tree')) || []
108
+ } finally {
109
+ loading.value = false
110
+ }
111
+ }
112
+
113
+ function resetForm(parentId = 0) {
114
+ Object.assign(form, {
115
+ id: undefined,
116
+ parentId,
117
+ deptName: '',
118
+ deptCode: '',
119
+ orderNum: 0,
120
+ leader: '',
121
+ phone: '',
122
+ email: '',
123
+ status: 0,
124
+ remark: '',
125
+ })
126
+ }
127
+
128
+ function openCreate(parentId = 0) {
129
+ resetForm(parentId)
130
+ dialogVisible.value = true
131
+ }
132
+
133
+ async function openEdit(row: DeptNode) {
134
+ const detail = await get<DeptForm>(`/depts/${row.id}`)
135
+ Object.assign(form, {
136
+ id: detail.id,
137
+ parentId: detail.parentId ?? 0,
138
+ deptName: detail.deptName || '',
139
+ deptCode: detail.deptCode || '',
140
+ orderNum: detail.orderNum ?? 0,
141
+ leader: detail.leader || '',
142
+ phone: detail.phone || '',
143
+ email: detail.email || '',
144
+ status: detail.status ?? 0,
145
+ remark: detail.remark || '',
146
+ })
147
+ dialogVisible.value = true
148
+ }
149
+
150
+ async function save() {
151
+ if (!form.deptName.trim()) {
152
+ ElMessage.warning('请填写部门名称')
153
+ return
154
+ }
155
+ saving.value = true
156
+ try {
157
+ const payload = {
158
+ id: form.id,
159
+ parentId: form.parentId,
160
+ deptName: form.deptName.trim(),
161
+ deptCode: form.deptCode.trim() || null,
162
+ orderNum: form.orderNum,
163
+ leader: form.leader.trim() || null,
164
+ phone: form.phone.trim() || null,
165
+ email: form.email.trim() || null,
166
+ status: form.status,
167
+ remark: form.remark.trim() || null,
168
+ }
169
+ if (form.id) await put('/depts', payload)
170
+ else await post('/depts', payload)
171
+ ElMessage.success('保存成功')
172
+ dialogVisible.value = false
173
+ load()
174
+ } finally {
175
+ saving.value = false
176
+ }
177
+ }
178
+
179
+ async function remove(row: DeptNode) {
180
+ if (row.children?.length) {
181
+ ElMessage.warning('请先删除下级部门')
182
+ return
183
+ }
184
+ await ElMessageBox.confirm(`确认删除部门「${row.deptName}」?`, '提示', { type: 'warning' })
185
+ await del('/depts', { ids: [row.id] })
186
+ ElMessage.success('已删除')
187
+ load()
188
+ }
189
+
190
+ onMounted(load)
191
+ </script>
192
+
193
+ <template>
194
+ <AppPage title="部门管理" description="维护组织架构树,支持层级新增与负责人信息">
195
+ <div class="dept-stats">
196
+ <div class="dept-stats__item">
197
+ <span class="dept-stats__num">{{ stats.total }}</span>
198
+ <span class="dept-stats__label">全部部门</span>
199
+ </div>
200
+ <div class="dept-stats__item dept-stats__item--root">
201
+ <span class="dept-stats__num">{{ stats.roots }}</span>
202
+ <span class="dept-stats__label">顶级节点</span>
203
+ </div>
204
+ <div class="dept-stats__item dept-stats__item--ok">
205
+ <span class="dept-stats__num">{{ stats.normal }}</span>
206
+ <span class="dept-stats__label">正常</span>
207
+ </div>
208
+ <div class="dept-stats__item dept-stats__item--off">
209
+ <span class="dept-stats__num">{{ stats.disabled }}</span>
210
+ <span class="dept-stats__label">停用</span>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="toolbar">
215
+ <el-input
216
+ v-model="keyword"
217
+ clearable
218
+ placeholder="搜索名称 / 编码 / 负责人 / 电话"
219
+ style="width: 280px"
220
+ />
221
+ <el-button type="primary" :icon="Refresh" @click="load">刷新</el-button>
222
+ <el-button @click="expandAll = !expandAll">{{ expandAll ? '全部折叠' : '全部展开' }}</el-button>
223
+ <div class="toolbar__spacer" />
224
+ <el-button type="success" :icon="Plus" v-permission="'system:dept:add'" @click="openCreate(0)">
225
+ 新增部门
226
+ </el-button>
227
+ </div>
228
+
229
+ <div class="dept-table-wrap" v-loading="loading">
230
+ <el-table
231
+ :data="filteredTree"
232
+ row-key="id"
233
+ border
234
+ stripe
235
+ :default-expand-all="expandAll"
236
+ :key="expandAll ? 'expand' : 'collapse'"
237
+ :tree-props="{ children: 'children' }"
238
+ class="dept-table"
239
+ >
240
+ <el-table-column label="部门名称" min-width="220" show-overflow-tooltip>
241
+ <template #default="{ row }">
242
+ <div class="name-cell">
243
+ <span class="name-cell__icon">
244
+ <el-icon :size="14"><OfficeBuilding /></el-icon>
245
+ </span>
246
+ <strong>{{ row.deptName }}</strong>
247
+ </div>
248
+ </template>
249
+ </el-table-column>
250
+
251
+ <el-table-column prop="deptCode" label="部门编码" min-width="120" show-overflow-tooltip>
252
+ <template #default="{ row }">
253
+ <code v-if="row.deptCode" class="mono">{{ row.deptCode }}</code>
254
+ <span v-else class="muted">—</span>
255
+ </template>
256
+ </el-table-column>
257
+
258
+ <el-table-column prop="orderNum" label="排序" width="72" align="center" />
259
+
260
+ <el-table-column prop="leader" label="负责人" min-width="100" show-overflow-tooltip>
261
+ <template #default="{ row }">
262
+ {{ row.leader || '—' }}
263
+ </template>
264
+ </el-table-column>
265
+
266
+ <el-table-column prop="phone" label="联系电话" min-width="120" show-overflow-tooltip>
267
+ <template #default="{ row }">
268
+ {{ row.phone || '—' }}
269
+ </template>
270
+ </el-table-column>
271
+
272
+ <el-table-column prop="email" label="邮箱" min-width="160" show-overflow-tooltip>
273
+ <template #default="{ row }">
274
+ {{ row.email || '—' }}
275
+ </template>
276
+ </el-table-column>
277
+
278
+ <el-table-column label="状态" width="88" align="center">
279
+ <template #default="{ row }">
280
+ <el-tag size="small" :type="row.status === 0 ? 'success' : 'danger'">
281
+ {{ row.status === 0 ? '正常' : '停用' }}
282
+ </el-tag>
283
+ </template>
284
+ </el-table-column>
285
+
286
+ <el-table-column label="操作" width="260" fixed="right" align="center">
287
+ <template #default="{ row }">
288
+ <div class="row-actions">
289
+ <el-button
290
+ size="small"
291
+ type="primary"
292
+ v-permission="'system:dept:add'"
293
+ @click="openCreate(row.id)"
294
+ >
295
+ 新增
296
+ </el-button>
297
+ <el-button
298
+ size="small"
299
+ type="primary"
300
+ v-permission="'system:dept:edit'"
301
+ @click="openEdit(row)"
302
+ >
303
+ 编辑
304
+ </el-button>
305
+ <el-button
306
+ size="small"
307
+ type="danger"
308
+ v-permission="'system:dept:remove'"
309
+ @click="remove(row)"
310
+ >
311
+ 删除
312
+ </el-button>
313
+ </div>
314
+ </template>
315
+ </el-table-column>
316
+ </el-table>
317
+ </div>
318
+
319
+ <el-dialog
320
+ v-model="dialogVisible"
321
+ :title="form.id ? '编辑部门' : '新增部门'"
322
+ width="560px"
323
+ class="dept-dialog"
324
+ append-to-body
325
+ align-center
326
+ destroy-on-close
327
+ >
328
+ <el-form label-width="96px" class="dept-form">
329
+ <el-form-item label="上级部门">
330
+ <el-select v-model="form.parentId" filterable style="width: 100%">
331
+ <el-option v-for="o in parentOptions" :key="o.id" :label="o.label" :value="o.id" />
332
+ </el-select>
333
+ </el-form-item>
334
+
335
+ <el-row :gutter="0">
336
+ <el-col :span="12" class="dept-form__col-l">
337
+ <el-form-item label="部门名称" required>
338
+ <el-input v-model="form.deptName" maxlength="64" placeholder="显示名称" />
339
+ </el-form-item>
340
+ </el-col>
341
+ <el-col :span="12" class="dept-form__col-r">
342
+ <el-form-item label="部门编码">
343
+ <el-input v-model="form.deptCode" maxlength="64" placeholder="可选编码" />
344
+ </el-form-item>
345
+ </el-col>
346
+ </el-row>
347
+
348
+ <el-row :gutter="0">
349
+ <el-col :span="12" class="dept-form__col-l">
350
+ <el-form-item label="显示排序">
351
+ <el-input-number v-model="form.orderNum" :min="0" controls-position="right" style="width: 100%" />
352
+ </el-form-item>
353
+ </el-col>
354
+ <el-col :span="12" class="dept-form__col-r">
355
+ <el-form-item label="状态">
356
+ <el-radio-group v-model="form.status">
357
+ <el-radio :value="0">正常</el-radio>
358
+ <el-radio :value="1">停用</el-radio>
359
+ </el-radio-group>
360
+ </el-form-item>
361
+ </el-col>
362
+ </el-row>
363
+
364
+ <el-form-item label="负责人">
365
+ <el-input v-model="form.leader" maxlength="64" placeholder="负责人姓名" />
366
+ </el-form-item>
367
+
368
+ <el-row :gutter="0">
369
+ <el-col :span="12" class="dept-form__col-l">
370
+ <el-form-item label="联系电话">
371
+ <el-input v-model="form.phone" maxlength="20" placeholder="手机或座机" />
372
+ </el-form-item>
373
+ </el-col>
374
+ <el-col :span="12" class="dept-form__col-r">
375
+ <el-form-item label="邮箱">
376
+ <el-input v-model="form.email" maxlength="128" placeholder="可选" />
377
+ </el-form-item>
378
+ </el-col>
379
+ </el-row>
380
+
381
+ <el-form-item label="备注">
382
+ <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="200" />
383
+ </el-form-item>
384
+ </el-form>
385
+
386
+ <template #footer>
387
+ <el-button @click="dialogVisible = false">取消</el-button>
388
+ <el-button type="primary" :loading="saving" @click="save">保存</el-button>
389
+ </template>
390
+ </el-dialog>
391
+ </AppPage>
392
+ </template>
393
+
394
+ <style scoped lang="scss">
395
+ .dept-stats {
396
+ display: grid;
397
+ grid-template-columns: repeat(4, minmax(0, 1fr));
398
+ gap: 12px;
399
+ margin-bottom: 14px;
400
+ }
401
+
402
+ .dept-stats__item {
403
+ display: flex;
404
+ flex-direction: column;
405
+ gap: 4px;
406
+ padding: 14px 16px;
407
+ border-radius: 14px;
408
+ border: 1px solid var(--sa-border);
409
+ background: var(--sa-panel);
410
+ box-shadow: var(--sa-shadow);
411
+ }
412
+
413
+ .dept-stats__num {
414
+ font-size: 22px;
415
+ font-weight: 800;
416
+ letter-spacing: -0.03em;
417
+ color: var(--sa-ink);
418
+ font-variant-numeric: tabular-nums;
419
+ }
420
+
421
+ .dept-stats__label {
422
+ font-size: 12px;
423
+ color: var(--sa-ink-muted);
424
+ }
425
+
426
+ .dept-stats__item--root .dept-stats__num { color: #0369a1; }
427
+ .dept-stats__item--ok .dept-stats__num { color: var(--sa-accent); }
428
+ .dept-stats__item--off .dept-stats__num { color: #be123c; }
429
+
430
+ .toolbar {
431
+ display: flex;
432
+ flex-wrap: wrap;
433
+ gap: 10px;
434
+ align-items: center;
435
+ margin-bottom: 14px;
436
+ padding: 12px 14px;
437
+ background: var(--sa-toolbar-bg);
438
+ border: 1px solid var(--sa-border);
439
+ border-radius: var(--sa-radius-sm);
440
+ }
441
+
442
+ .toolbar__spacer {
443
+ flex: 1;
444
+ }
445
+
446
+ .dept-table-wrap {
447
+ border: 1px solid var(--sa-border);
448
+ border-radius: 14px;
449
+ overflow: hidden;
450
+ background: var(--sa-panel);
451
+ box-shadow: var(--sa-shadow);
452
+ }
453
+
454
+ .name-cell {
455
+ display: inline-flex;
456
+ align-items: center;
457
+ gap: 8px;
458
+ min-width: 0;
459
+ }
460
+
461
+ .name-cell__icon {
462
+ width: 26px;
463
+ height: 26px;
464
+ border-radius: 8px;
465
+ display: grid;
466
+ place-items: center;
467
+ flex-shrink: 0;
468
+ background: var(--sa-accent-soft);
469
+ color: var(--sa-accent);
470
+ }
471
+
472
+ .name-cell strong {
473
+ font-size: 13px;
474
+ font-weight: 650;
475
+ }
476
+
477
+ .mono {
478
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
479
+ font-size: 12px;
480
+ color: var(--sa-ink-secondary);
481
+ background: var(--sa-toolbar-bg);
482
+ padding: 1px 6px;
483
+ border-radius: 6px;
484
+ }
485
+
486
+ .muted {
487
+ color: var(--sa-ink-muted);
488
+ }
489
+
490
+ .dept-form__col-l {
491
+ padding-right: 8px;
492
+ }
493
+ .dept-form__col-r {
494
+ padding-left: 8px;
495
+ }
496
+
497
+ @media (max-width: 900px) {
498
+ .dept-stats {
499
+ grid-template-columns: repeat(2, minmax(0, 1fr));
500
+ }
501
+ }
502
+ </style>
503
+
504
+ <style>
505
+ .dept-dialog.el-dialog {
506
+ width: min(560px, calc(100vw - 32px)) !important;
507
+ max-height: 88vh;
508
+ overflow: hidden;
509
+ display: flex;
510
+ flex-direction: column;
511
+ }
512
+ .dept-dialog .el-dialog__body {
513
+ overflow-x: hidden;
514
+ overflow-y: auto;
515
+ max-height: calc(88vh - 140px);
516
+ }
517
+ </style>