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,422 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, reactive, ref } from 'vue'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
+ import { Refresh, Search, Message, CircleCheck } from '@element-plus/icons-vue'
5
+ import { get, put, del } from '@/utils/request'
6
+ import AppPage from '@/components/AppPage.vue'
7
+ import { formatDateTime } from '@/utils/datetime'
8
+
9
+ interface MessageItem {
10
+ id: number
11
+ title: string
12
+ content?: string
13
+ msgType: number
14
+ isRead: boolean
15
+ readTime?: string
16
+ createTime: string
17
+ }
18
+
19
+ const loading = ref(false)
20
+ const total = ref(0)
21
+ const list = ref<MessageItem[]>([])
22
+ const detailVisible = ref(false)
23
+ const current = ref<MessageItem | null>(null)
24
+
25
+ const query = reactive({
26
+ pageIndex: 1,
27
+ pageSize: 10,
28
+ keyword: '',
29
+ isRead: undefined as boolean | undefined,
30
+ })
31
+
32
+ const stats = reactive({ total: 0, unread: 0, read: 0 })
33
+
34
+ function msgTypeLabel(t: number) {
35
+ return t === 2 ? '公告' : '系统'
36
+ }
37
+
38
+ async function loadStats(pageTotal?: number) {
39
+ const base = { pageIndex: 1, pageSize: 1, keyword: query.keyword || undefined }
40
+ const unfiltered = query.isRead === undefined || query.isRead === null
41
+ try {
42
+ const tasks: Promise<void>[] = []
43
+ if (unfiltered && pageTotal != null) {
44
+ stats.total = pageTotal
45
+ } else {
46
+ tasks.push(
47
+ get<{ total: number }>('/messages', base).then((all) => {
48
+ stats.total = all.total
49
+ }),
50
+ )
51
+ }
52
+ tasks.push(
53
+ get<{ total: number }>('/messages', { ...base, isRead: false }).then((r) => {
54
+ stats.unread = r.total
55
+ }),
56
+ get<{ total: number }>('/messages', { ...base, isRead: true }).then((r) => {
57
+ stats.read = r.total
58
+ }),
59
+ )
60
+ await Promise.all(tasks)
61
+ } catch {
62
+ stats.total = total.value
63
+ }
64
+ }
65
+
66
+ async function load() {
67
+ loading.value = true
68
+ try {
69
+ const params: Record<string, unknown> = {
70
+ pageIndex: query.pageIndex,
71
+ pageSize: query.pageSize,
72
+ keyword: query.keyword || undefined,
73
+ }
74
+ if (query.isRead !== undefined && query.isRead !== null) params.isRead = query.isRead
75
+ const data = await get<{ total: number; items: MessageItem[] }>('/messages', params)
76
+ total.value = data.total
77
+ list.value = data.items || []
78
+ await loadStats(data.total)
79
+ } finally {
80
+ loading.value = false
81
+ }
82
+ }
83
+
84
+ function search() {
85
+ query.pageIndex = 1
86
+ load()
87
+ }
88
+
89
+ function resetQuery() {
90
+ query.keyword = ''
91
+ query.isRead = undefined
92
+ query.pageIndex = 1
93
+ load()
94
+ }
95
+
96
+ async function openDetail(row: MessageItem) {
97
+ current.value = { ...row }
98
+ detailVisible.value = true
99
+ if (!row.isRead) {
100
+ try {
101
+ await put('/messages/read', { ids: [row.id] })
102
+ row.isRead = true
103
+ row.readTime = new Date().toISOString()
104
+ current.value = { ...row }
105
+ loadStats()
106
+ } catch {
107
+ /* 查看仍可用 */
108
+ }
109
+ }
110
+ }
111
+
112
+ async function markRead(row: MessageItem) {
113
+ await put('/messages/read', { ids: [row.id] })
114
+ ElMessage.success('已标记已读')
115
+ load()
116
+ }
117
+
118
+ async function markAllRead() {
119
+ await ElMessageBox.confirm('确认将全部未读消息标记为已读?', '提示', { type: 'info' })
120
+ await put('/messages/read-all')
121
+ ElMessage.success('已全部标记已读')
122
+ load()
123
+ }
124
+
125
+ async function remove(row: MessageItem) {
126
+ await ElMessageBox.confirm(`确认删除消息「${row.title}」?`, '提示', { type: 'warning' })
127
+ await del('/messages', { ids: [row.id] })
128
+ ElMessage.success('已删除')
129
+ if (current.value?.id === row.id) detailVisible.value = false
130
+ load()
131
+ }
132
+
133
+ onMounted(load)
134
+ </script>
135
+
136
+ <template>
137
+ <AppPage title="消息中心" description="站内消息查阅、已读标记与清理">
138
+ <div class="msg-stats">
139
+ <div class="msg-stats__item">
140
+ <span class="msg-stats__num">{{ stats.total }}</span>
141
+ <span class="msg-stats__label">全部消息</span>
142
+ </div>
143
+ <div class="msg-stats__item msg-stats__item--unread">
144
+ <span class="msg-stats__num">{{ stats.unread }}</span>
145
+ <span class="msg-stats__label">未读</span>
146
+ </div>
147
+ <div class="msg-stats__item msg-stats__item--ok">
148
+ <span class="msg-stats__num">{{ stats.read }}</span>
149
+ <span class="msg-stats__label">已读</span>
150
+ </div>
151
+ <div class="msg-stats__item msg-stats__item--hit">
152
+ <span class="msg-stats__num">{{ total }}</span>
153
+ <span class="msg-stats__label">当前筛选</span>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="toolbar">
158
+ <el-input
159
+ v-model="query.keyword"
160
+ clearable
161
+ placeholder="搜索标题"
162
+ style="width: 200px"
163
+ @keyup.enter="search"
164
+ />
165
+ <el-select v-model="query.isRead" clearable placeholder="已读状态" style="width: 120px" @change="search">
166
+ <el-option label="未读" :value="false" />
167
+ <el-option label="已读" :value="true" />
168
+ </el-select>
169
+ <el-button type="primary" :icon="Search" @click="search">查询</el-button>
170
+ <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
171
+ <div class="toolbar__spacer" />
172
+ <el-button type="primary" plain :icon="CircleCheck" :disabled="!stats.unread" @click="markAllRead">
173
+ 全部已读
174
+ </el-button>
175
+ </div>
176
+
177
+ <div class="table-wrap" v-loading="loading">
178
+ <el-table :data="list" border stripe empty-text="暂无消息" @row-click="openDetail">
179
+ <el-table-column label="标题" min-width="220" show-overflow-tooltip>
180
+ <template #default="{ row }">
181
+ <div class="name-cell" :class="{ 'is-unread': !row.isRead }">
182
+ <span class="name-cell__icon">
183
+ <el-icon :size="14"><Message /></el-icon>
184
+ </span>
185
+ <strong>{{ row.title }}</strong>
186
+ <span v-if="!row.isRead" class="dot" />
187
+ </div>
188
+ </template>
189
+ </el-table-column>
190
+ <el-table-column label="类型" width="88" align="center">
191
+ <template #default="{ row }">
192
+ <el-tag size="small" :type="row.msgType === 2 ? 'warning' : 'info'">
193
+ {{ msgTypeLabel(row.msgType) }}
194
+ </el-tag>
195
+ </template>
196
+ </el-table-column>
197
+ <el-table-column label="状态" width="88" align="center">
198
+ <template #default="{ row }">
199
+ <el-tag size="small" :type="row.isRead ? 'success' : 'danger'">
200
+ {{ row.isRead ? '已读' : '未读' }}
201
+ </el-tag>
202
+ </template>
203
+ </el-table-column>
204
+ <el-table-column label="时间" width="170" class-name="is-datetime" show-overflow-tooltip>
205
+ <template #default="{ row }">
206
+ <span class="datetime-cell">{{ formatDateTime(row.createTime) }}</span>
207
+ </template>
208
+ </el-table-column>
209
+ <el-table-column label="操作" width="280" fixed="right" align="center">
210
+ <template #default="{ row }">
211
+ <div class="row-actions">
212
+ <el-button size="small" type="primary" @click.stop="openDetail(row)">查看</el-button>
213
+ <el-button
214
+ v-if="!row.isRead"
215
+ size="small"
216
+ type="success"
217
+ @click.stop="markRead(row)"
218
+ >
219
+ 已读
220
+ </el-button>
221
+ <el-button size="small" type="danger" @click.stop="remove(row)">删除</el-button>
222
+ </div>
223
+ </template>
224
+ </el-table-column>
225
+ </el-table>
226
+ </div>
227
+
228
+ <div class="pager">
229
+ <el-pagination
230
+ background
231
+ layout="total, sizes, prev, pager, next"
232
+ :total="total"
233
+ v-model:current-page="query.pageIndex"
234
+ v-model:page-size="query.pageSize"
235
+ :page-sizes="[10, 20, 50]"
236
+ @current-change="load"
237
+ @size-change="
238
+ () => {
239
+ query.pageIndex = 1
240
+ load()
241
+ }
242
+ "
243
+ />
244
+ </div>
245
+
246
+ <el-dialog
247
+ v-model="detailVisible"
248
+ title="消息详情"
249
+ width="560px"
250
+ class="msg-dialog"
251
+ append-to-body
252
+ align-center
253
+ destroy-on-close
254
+ >
255
+ <template v-if="current">
256
+ <div class="detail-head">
257
+ <h3>{{ current.title }}</h3>
258
+ <div class="detail-meta">
259
+ <el-tag size="small" :type="current.msgType === 2 ? 'warning' : 'info'">
260
+ {{ msgTypeLabel(current.msgType) }}
261
+ </el-tag>
262
+ <el-tag size="small" :type="current.isRead ? 'success' : 'danger'">
263
+ {{ current.isRead ? '已读' : '未读' }}
264
+ </el-tag>
265
+ <span class="muted">{{ formatDateTime(current.createTime) }}</span>
266
+ </div>
267
+ </div>
268
+ <div class="detail-body">{{ current.content || '(无正文)' }}</div>
269
+ </template>
270
+ <template #footer>
271
+ <el-button @click="detailVisible = false">关闭</el-button>
272
+ <el-button v-if="current" type="danger" @click="remove(current)">删除</el-button>
273
+ </template>
274
+ </el-dialog>
275
+ </AppPage>
276
+ </template>
277
+
278
+ <style scoped lang="scss">
279
+ .msg-stats {
280
+ display: grid;
281
+ grid-template-columns: repeat(4, minmax(0, 1fr));
282
+ gap: 12px;
283
+ margin-bottom: 14px;
284
+ }
285
+ .msg-stats__item {
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: 4px;
289
+ padding: 14px 16px;
290
+ border-radius: 14px;
291
+ border: 1px solid var(--sa-border);
292
+ background: var(--sa-panel);
293
+ box-shadow: var(--sa-shadow);
294
+ }
295
+ .msg-stats__num {
296
+ font-size: 22px;
297
+ font-weight: 800;
298
+ letter-spacing: -0.03em;
299
+ color: var(--sa-ink);
300
+ font-variant-numeric: tabular-nums;
301
+ }
302
+ .msg-stats__label {
303
+ font-size: 12px;
304
+ color: var(--sa-ink-muted);
305
+ }
306
+ .msg-stats__item--unread .msg-stats__num {
307
+ color: #be123c;
308
+ }
309
+ .msg-stats__item--ok .msg-stats__num {
310
+ color: var(--sa-accent);
311
+ }
312
+ .msg-stats__item--hit .msg-stats__num {
313
+ color: #0369a1;
314
+ }
315
+
316
+ .toolbar {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ gap: 10px;
320
+ align-items: center;
321
+ margin-bottom: 14px;
322
+ padding: 12px 14px;
323
+ background: var(--sa-toolbar-bg);
324
+ border: 1px solid var(--sa-border);
325
+ border-radius: var(--sa-radius-sm);
326
+ }
327
+ .toolbar__spacer {
328
+ flex: 1;
329
+ }
330
+ .table-wrap {
331
+ border: 1px solid var(--sa-border);
332
+ border-radius: 14px;
333
+ overflow: hidden;
334
+ background: var(--sa-panel);
335
+ box-shadow: var(--sa-shadow);
336
+ :deep(.el-table__row) {
337
+ cursor: pointer;
338
+ }
339
+ }
340
+ .name-cell {
341
+ display: inline-flex;
342
+ align-items: center;
343
+ gap: 8px;
344
+ position: relative;
345
+ }
346
+ .name-cell.is-unread strong {
347
+ font-weight: 700;
348
+ }
349
+ .name-cell__icon {
350
+ width: 26px;
351
+ height: 26px;
352
+ border-radius: 8px;
353
+ display: grid;
354
+ place-items: center;
355
+ background: var(--sa-accent-soft);
356
+ color: var(--sa-accent);
357
+ }
358
+ .name-cell strong {
359
+ font-size: 13px;
360
+ font-weight: 550;
361
+ }
362
+ .dot {
363
+ width: 7px;
364
+ height: 7px;
365
+ border-radius: 50%;
366
+ background: #e11d48;
367
+ flex-shrink: 0;
368
+ }
369
+ .muted {
370
+ color: var(--sa-ink-muted);
371
+ font-size: 12px;
372
+ }
373
+ .pager {
374
+ display: flex;
375
+ justify-content: flex-end;
376
+ margin-top: 14px;
377
+ }
378
+ .detail-head h3 {
379
+ margin: 0 0 10px;
380
+ font-size: 16px;
381
+ font-weight: 700;
382
+ color: var(--sa-ink);
383
+ }
384
+ .detail-meta {
385
+ display: flex;
386
+ flex-wrap: wrap;
387
+ gap: 8px;
388
+ align-items: center;
389
+ margin-bottom: 16px;
390
+ }
391
+ .detail-body {
392
+ white-space: pre-wrap;
393
+ line-height: 1.7;
394
+ font-size: 14px;
395
+ color: var(--sa-ink-secondary);
396
+ padding: 14px 16px;
397
+ border-radius: 12px;
398
+ background: var(--sa-toolbar-bg);
399
+ border: 1px solid var(--sa-border);
400
+ min-height: 120px;
401
+ }
402
+ @media (max-width: 900px) {
403
+ .msg-stats {
404
+ grid-template-columns: repeat(2, minmax(0, 1fr));
405
+ }
406
+ }
407
+ </style>
408
+
409
+ <style>
410
+ .msg-dialog.el-dialog {
411
+ width: min(560px, calc(100vw - 32px)) !important;
412
+ max-height: 88vh;
413
+ overflow: hidden;
414
+ display: flex;
415
+ flex-direction: column;
416
+ }
417
+ .msg-dialog .el-dialog__body {
418
+ overflow-x: hidden;
419
+ overflow-y: auto;
420
+ max-height: calc(88vh - 140px);
421
+ }
422
+ </style>