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,492 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, reactive, ref } from 'vue'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
+ import { Refresh, Search, Document, View, Delete } from '@element-plus/icons-vue'
5
+ import { get, del } from '@/utils/request'
6
+ import AppPage from '@/components/AppPage.vue'
7
+ import { formatDateTime } from '@/utils/datetime'
8
+
9
+ interface OperLogItem {
10
+ id: number
11
+ title?: string
12
+ businessType: number
13
+ operName?: string
14
+ operUrl?: string
15
+ operIp?: string
16
+ status: number
17
+ operTime?: string
18
+ costTime: number
19
+ }
20
+
21
+ interface OperLogDetail extends OperLogItem {
22
+ method?: string
23
+ requestMethod?: string
24
+ operatorType?: number
25
+ deptName?: string
26
+ operLocation?: string
27
+ operParam?: string
28
+ jsonResult?: string
29
+ errorMsg?: string
30
+ }
31
+
32
+ const loading = ref(false)
33
+ const detailLoading = ref(false)
34
+ const total = ref(0)
35
+ const list = ref<OperLogItem[]>([])
36
+ const detailVisible = ref(false)
37
+ const current = ref<OperLogDetail | null>(null)
38
+
39
+ const query = reactive({
40
+ pageIndex: 1,
41
+ pageSize: 10,
42
+ keyword: '',
43
+ status: undefined as number | undefined,
44
+ businessType: undefined as number | undefined,
45
+ })
46
+
47
+ const stats = reactive({ total: 0, ok: 0, fail: 0 })
48
+
49
+ const bizLabels: Record<number, string> = {
50
+ 0: '其它',
51
+ 1: '新增',
52
+ 2: '修改',
53
+ 3: '删除',
54
+ 4: '授权',
55
+ 5: '导出',
56
+ 6: '导入',
57
+ 7: '强退',
58
+ 8: '生成代码',
59
+ 9: '清空',
60
+ }
61
+
62
+ function bizLabel(t: number) {
63
+ return bizLabels[t] ?? `类型${t}`
64
+ }
65
+
66
+ function bizTagType(t: number) {
67
+ if (t === 1) return 'success'
68
+ if (t === 2) return 'warning'
69
+ if (t === 3 || t === 7 || t === 9) return 'danger'
70
+ if (t === 4) return 'primary'
71
+ return 'info'
72
+ }
73
+
74
+ async function loadStats(pageTotal?: number) {
75
+ const base = { pageIndex: 1, pageSize: 1, keyword: query.keyword || undefined }
76
+ const unfiltered =
77
+ (query.status === undefined || query.status === null) &&
78
+ (query.businessType === undefined || query.businessType === null)
79
+ try {
80
+ const tasks: Promise<void>[] = []
81
+ if (unfiltered && pageTotal != null) {
82
+ stats.total = pageTotal
83
+ } else {
84
+ tasks.push(
85
+ get<{ total: number }>('/oper-logs', base).then((all) => {
86
+ stats.total = all.total
87
+ }),
88
+ )
89
+ }
90
+ tasks.push(
91
+ get<{ total: number }>('/oper-logs', { ...base, status: 0 }).then((r) => {
92
+ stats.ok = r.total
93
+ }),
94
+ get<{ total: number }>('/oper-logs', { ...base, status: 1 }).then((r) => {
95
+ stats.fail = r.total
96
+ }),
97
+ )
98
+ await Promise.all(tasks)
99
+ } catch {
100
+ stats.total = total.value
101
+ }
102
+ }
103
+
104
+ async function load() {
105
+ loading.value = true
106
+ try {
107
+ const params: Record<string, unknown> = {
108
+ pageIndex: query.pageIndex,
109
+ pageSize: query.pageSize,
110
+ keyword: query.keyword || undefined,
111
+ }
112
+ if (query.status !== undefined && query.status !== null) params.status = query.status
113
+ if (query.businessType !== undefined && query.businessType !== null) {
114
+ params.businessType = query.businessType
115
+ }
116
+ const data = await get<{ total: number; items: OperLogItem[] }>('/oper-logs', params)
117
+ total.value = data.total
118
+ list.value = data.items || []
119
+ await loadStats(data.total)
120
+ } finally {
121
+ loading.value = false
122
+ }
123
+ }
124
+
125
+ function search() {
126
+ query.pageIndex = 1
127
+ load()
128
+ }
129
+
130
+ function resetQuery() {
131
+ query.keyword = ''
132
+ query.status = undefined
133
+ query.businessType = undefined
134
+ query.pageIndex = 1
135
+ load()
136
+ }
137
+
138
+ function formatPrettyJson(raw?: string | null) {
139
+ if (!raw || !raw.trim()) return ''
140
+ try {
141
+ return JSON.stringify(JSON.parse(raw), null, 2)
142
+ } catch {
143
+ return raw
144
+ }
145
+ }
146
+
147
+ async function openDetail(row: OperLogItem) {
148
+ detailVisible.value = true
149
+ detailLoading.value = true
150
+ current.value = null
151
+ try {
152
+ current.value = await get<OperLogDetail>(`/oper-logs/${row.id}`)
153
+ } catch {
154
+ detailVisible.value = false
155
+ } finally {
156
+ detailLoading.value = false
157
+ }
158
+ }
159
+
160
+ async function remove(row: OperLogItem) {
161
+ await ElMessageBox.confirm(`确认删除该操作日志?`, '提示', { type: 'warning' })
162
+ await del('/oper-logs', { ids: [row.id] })
163
+ ElMessage.success('已删除')
164
+ load()
165
+ }
166
+
167
+ onMounted(load)
168
+ </script>
169
+
170
+ <template>
171
+ <AppPage title="操作日志" description="审计后台关键操作与接口调用记录">
172
+ <div class="mon-stats">
173
+ <div class="mon-stats__item">
174
+ <span class="mon-stats__num">{{ stats.total }}</span>
175
+ <span class="mon-stats__label">全部记录</span>
176
+ </div>
177
+ <div class="mon-stats__item mon-stats__item--ok">
178
+ <span class="mon-stats__num">{{ stats.ok }}</span>
179
+ <span class="mon-stats__label">成功</span>
180
+ </div>
181
+ <div class="mon-stats__item mon-stats__item--fail">
182
+ <span class="mon-stats__num">{{ stats.fail }}</span>
183
+ <span class="mon-stats__label">失败</span>
184
+ </div>
185
+ <div class="mon-stats__item mon-stats__item--hit">
186
+ <span class="mon-stats__num">{{ total }}</span>
187
+ <span class="mon-stats__label">当前筛选</span>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="toolbar">
192
+ <el-input
193
+ v-model="query.keyword"
194
+ clearable
195
+ placeholder="搜索模块 / 操作人"
196
+ style="width: 200px"
197
+ @keyup.enter="search"
198
+ />
199
+ <el-select
200
+ v-model="query.businessType"
201
+ clearable
202
+ placeholder="业务类型"
203
+ style="width: 130px"
204
+ @change="search"
205
+ >
206
+ <el-option v-for="(label, key) in bizLabels" :key="key" :label="label" :value="Number(key)" />
207
+ </el-select>
208
+ <el-select v-model="query.status" clearable placeholder="状态" style="width: 110px" @change="search">
209
+ <el-option label="成功" :value="0" />
210
+ <el-option label="失败" :value="1" />
211
+ </el-select>
212
+ <el-button type="primary" :icon="Search" @click="search">查询</el-button>
213
+ <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
214
+ </div>
215
+
216
+ <div class="table-wrap" v-loading="loading">
217
+ <el-table :data="list" border stripe empty-text="暂无操作日志">
218
+ <el-table-column label="模块" min-width="140" show-overflow-tooltip>
219
+ <template #default="{ row }">
220
+ <div class="name-cell">
221
+ <span class="name-cell__icon">
222
+ <el-icon :size="14"><Document /></el-icon>
223
+ </span>
224
+ <strong>{{ row.title || '—' }}</strong>
225
+ </div>
226
+ </template>
227
+ </el-table-column>
228
+ <el-table-column label="业务类型" width="100" align="center">
229
+ <template #default="{ row }">
230
+ <el-tag size="small" :type="bizTagType(row.businessType)">{{ bizLabel(row.businessType) }}</el-tag>
231
+ </template>
232
+ </el-table-column>
233
+ <el-table-column prop="operName" label="操作人" width="110" show-overflow-tooltip />
234
+ <el-table-column prop="operIp" label="IP" width="130" show-overflow-tooltip>
235
+ <template #default="{ row }">
236
+ <code class="mono">{{ row.operIp || '—' }}</code>
237
+ </template>
238
+ </el-table-column>
239
+ <el-table-column prop="operUrl" label="请求地址" min-width="180" show-overflow-tooltip>
240
+ <template #default="{ row }">
241
+ <code class="mono url">{{ row.operUrl || '—' }}</code>
242
+ </template>
243
+ </el-table-column>
244
+ <el-table-column label="状态" width="80" align="center">
245
+ <template #default="{ row }">
246
+ <el-tag size="small" :type="row.status === 0 ? 'success' : 'danger'">
247
+ {{ row.status === 0 ? '成功' : '失败' }}
248
+ </el-tag>
249
+ </template>
250
+ </el-table-column>
251
+ <el-table-column label="耗时" width="90" align="right">
252
+ <template #default="{ row }">
253
+ <span class="muted">{{ row.costTime ?? 0 }} ms</span>
254
+ </template>
255
+ </el-table-column>
256
+ <el-table-column label="时间" width="170" class-name="is-datetime" show-overflow-tooltip>
257
+ <template #default="{ row }">
258
+ <span class="datetime-cell">{{ formatDateTime(row.operTime) }}</span>
259
+ </template>
260
+ </el-table-column>
261
+ <el-table-column label="操作" width="180" fixed="right" align="center">
262
+ <template #default="{ row }">
263
+ <div class="row-actions">
264
+ <el-button size="small" type="primary" :icon="View" @click="openDetail(row)">详情</el-button>
265
+ <el-button
266
+ size="small"
267
+ type="danger"
268
+ :icon="Delete"
269
+ v-permission="'monitor:operlog:remove'"
270
+ @click="remove(row)"
271
+ >
272
+ 删除
273
+ </el-button>
274
+ </div>
275
+ </template>
276
+ </el-table-column>
277
+ </el-table>
278
+ </div>
279
+
280
+ <div class="pager">
281
+ <el-pagination
282
+ background
283
+ layout="total, sizes, prev, pager, next"
284
+ :total="total"
285
+ v-model:current-page="query.pageIndex"
286
+ v-model:page-size="query.pageSize"
287
+ :page-sizes="[10, 20, 50]"
288
+ @current-change="load"
289
+ @size-change="
290
+ () => {
291
+ query.pageIndex = 1
292
+ load()
293
+ }
294
+ "
295
+ />
296
+ </div>
297
+
298
+ <el-dialog
299
+ v-model="detailVisible"
300
+ title="操作日志详情"
301
+ width="720px"
302
+ class="operlog-detail-dialog"
303
+ append-to-body
304
+ align-center
305
+ destroy-on-close
306
+ >
307
+ <div v-loading="detailLoading">
308
+ <template v-if="current">
309
+ <el-descriptions :column="2" border>
310
+ <el-descriptions-item label="模块" :span="2">{{ current.title || '—' }}</el-descriptions-item>
311
+ <el-descriptions-item label="业务类型">
312
+ <el-tag size="small" :type="bizTagType(current.businessType)">
313
+ {{ bizLabel(current.businessType) }}
314
+ </el-tag>
315
+ </el-descriptions-item>
316
+ <el-descriptions-item label="状态">
317
+ <el-tag size="small" :type="current.status === 0 ? 'success' : 'danger'">
318
+ {{ current.status === 0 ? '成功' : '失败' }}
319
+ </el-tag>
320
+ </el-descriptions-item>
321
+ <el-descriptions-item label="操作人">{{ current.operName || '—' }}</el-descriptions-item>
322
+ <el-descriptions-item label="部门">{{ current.deptName || '—' }}</el-descriptions-item>
323
+ <el-descriptions-item label="请求方式">{{ current.requestMethod || '—' }}</el-descriptions-item>
324
+ <el-descriptions-item label="耗时">{{ current.costTime ?? 0 }} ms</el-descriptions-item>
325
+ <el-descriptions-item label="IP">{{ current.operIp || '—' }}</el-descriptions-item>
326
+ <el-descriptions-item label="地点">{{ current.operLocation || '—' }}</el-descriptions-item>
327
+ <el-descriptions-item label="请求地址" :span="2">{{ current.operUrl || '—' }}</el-descriptions-item>
328
+ <el-descriptions-item label="方法" :span="2">{{ current.method || '—' }}</el-descriptions-item>
329
+ <el-descriptions-item label="时间" :span="2">{{ formatDateTime(current.operTime) }}</el-descriptions-item>
330
+ <el-descriptions-item v-if="current.errorMsg" label="错误信息" :span="2">
331
+ <span class="fail-msg">{{ current.errorMsg }}</span>
332
+ </el-descriptions-item>
333
+ </el-descriptions>
334
+
335
+ <div class="detail-block">
336
+ <div class="detail-block__title">请求参数</div>
337
+ <pre class="detail-block__code">{{ formatPrettyJson(current.operParam) || '(无)' }}</pre>
338
+ </div>
339
+ <div class="detail-block">
340
+ <div class="detail-block__title">返回结果</div>
341
+ <pre class="detail-block__code">{{ formatPrettyJson(current.jsonResult) || '(无)' }}</pre>
342
+ </div>
343
+ </template>
344
+ </div>
345
+ <template #footer>
346
+ <el-button @click="detailVisible = false">关闭</el-button>
347
+ </template>
348
+ </el-dialog>
349
+ </AppPage>
350
+ </template>
351
+
352
+ <style scoped lang="scss">
353
+ .mon-stats {
354
+ display: grid;
355
+ grid-template-columns: repeat(4, minmax(0, 1fr));
356
+ gap: 12px;
357
+ margin-bottom: 14px;
358
+ }
359
+ .mon-stats__item {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 4px;
363
+ padding: 14px 16px;
364
+ border-radius: 14px;
365
+ border: 1px solid var(--sa-border);
366
+ background: var(--sa-panel);
367
+ box-shadow: var(--sa-shadow);
368
+ }
369
+ .mon-stats__num {
370
+ font-size: 22px;
371
+ font-weight: 800;
372
+ letter-spacing: -0.03em;
373
+ color: var(--sa-ink);
374
+ font-variant-numeric: tabular-nums;
375
+ }
376
+ .mon-stats__label {
377
+ font-size: 12px;
378
+ color: var(--sa-ink-muted);
379
+ }
380
+ .mon-stats__item--ok .mon-stats__num {
381
+ color: var(--sa-accent);
382
+ }
383
+ .mon-stats__item--fail .mon-stats__num {
384
+ color: #be123c;
385
+ }
386
+ .mon-stats__item--hit .mon-stats__num {
387
+ color: #0369a1;
388
+ }
389
+ .toolbar {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ gap: 10px;
393
+ align-items: center;
394
+ margin-bottom: 14px;
395
+ padding: 12px 14px;
396
+ background: var(--sa-toolbar-bg);
397
+ border: 1px solid var(--sa-border);
398
+ border-radius: var(--sa-radius-sm);
399
+ }
400
+ .table-wrap {
401
+ border: 1px solid var(--sa-border);
402
+ border-radius: 14px;
403
+ overflow: hidden;
404
+ background: var(--sa-panel);
405
+ box-shadow: var(--sa-shadow);
406
+ }
407
+ .name-cell {
408
+ display: inline-flex;
409
+ align-items: center;
410
+ gap: 8px;
411
+ }
412
+ .name-cell__icon {
413
+ width: 26px;
414
+ height: 26px;
415
+ border-radius: 8px;
416
+ display: grid;
417
+ place-items: center;
418
+ background: var(--sa-accent-soft);
419
+ color: var(--sa-accent);
420
+ }
421
+ .name-cell strong {
422
+ font-size: 13px;
423
+ font-weight: 650;
424
+ }
425
+ .mono {
426
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
427
+ font-size: 12px;
428
+ color: var(--sa-ink-secondary);
429
+ background: var(--sa-toolbar-bg);
430
+ padding: 1px 6px;
431
+ border-radius: 6px;
432
+ }
433
+ .mono.url {
434
+ word-break: break-all;
435
+ }
436
+ .muted {
437
+ color: var(--sa-ink-muted);
438
+ font-size: 12px;
439
+ }
440
+ .pager {
441
+ display: flex;
442
+ justify-content: flex-end;
443
+ margin-top: 14px;
444
+ }
445
+ .fail-msg {
446
+ color: #be123c;
447
+ }
448
+ .detail-block {
449
+ margin-top: 14px;
450
+ }
451
+ .detail-block__title {
452
+ font-size: 13px;
453
+ font-weight: 650;
454
+ color: var(--sa-ink);
455
+ margin-bottom: 8px;
456
+ }
457
+ .detail-block__code {
458
+ margin: 0;
459
+ padding: 12px 14px;
460
+ max-height: 220px;
461
+ overflow: auto;
462
+ border-radius: 10px;
463
+ border: 1px solid var(--sa-border);
464
+ background: #0f172a;
465
+ color: #e2e8f0;
466
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
467
+ font-size: 12px;
468
+ line-height: 1.55;
469
+ white-space: pre-wrap;
470
+ word-break: break-word;
471
+ }
472
+ @media (max-width: 900px) {
473
+ .mon-stats {
474
+ grid-template-columns: repeat(2, minmax(0, 1fr));
475
+ }
476
+ }
477
+ </style>
478
+
479
+ <style>
480
+ .operlog-detail-dialog.el-dialog {
481
+ width: min(720px, calc(100vw - 32px)) !important;
482
+ max-height: 90vh;
483
+ overflow: hidden;
484
+ display: flex;
485
+ flex-direction: column;
486
+ }
487
+ .operlog-detail-dialog .el-dialog__body {
488
+ overflow-x: hidden;
489
+ overflow-y: auto;
490
+ max-height: calc(90vh - 140px);
491
+ }
492
+ </style>