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