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,407 @@
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, Tools } from '@element-plus/icons-vue'
5
+ import { get, post, put, del } from '@/utils/request'
6
+ import AppPage from '@/components/AppPage.vue'
7
+
8
+ interface ConfigItem {
9
+ id: number
10
+ configName: string
11
+ configKey: string
12
+ configValue: string
13
+ configType: string
14
+ remark?: string
15
+ }
16
+
17
+ interface ConfigForm {
18
+ id?: number
19
+ configName: string
20
+ configKey: string
21
+ configValue: string
22
+ configType: string
23
+ remark: string
24
+ }
25
+
26
+ const loading = ref(false)
27
+ const saving = ref(false)
28
+ const total = ref(0)
29
+ const list = ref<ConfigItem[]>([])
30
+ const dialogVisible = ref(false)
31
+
32
+ const query = reactive({ pageIndex: 1, pageSize: 10, keyword: '' })
33
+
34
+ const form = reactive<ConfigForm>({
35
+ id: undefined,
36
+ configName: '',
37
+ configKey: '',
38
+ configValue: '',
39
+ configType: 'N',
40
+ remark: '',
41
+ })
42
+
43
+ const pageStats = computed(() => {
44
+ let builtin = 0
45
+ let custom = 0
46
+ for (const row of list.value) {
47
+ if (row.configType === 'Y') builtin++
48
+ else custom++
49
+ }
50
+ return { builtin, custom }
51
+ })
52
+
53
+ async function load() {
54
+ loading.value = true
55
+ try {
56
+ const data = await get<{ total: number; items: ConfigItem[] }>('/configs', {
57
+ pageIndex: query.pageIndex,
58
+ pageSize: query.pageSize,
59
+ keyword: query.keyword || undefined,
60
+ })
61
+ total.value = data.total
62
+ list.value = data.items || []
63
+ } finally {
64
+ loading.value = false
65
+ }
66
+ }
67
+
68
+ function search() {
69
+ query.pageIndex = 1
70
+ load()
71
+ }
72
+
73
+ function resetQuery() {
74
+ query.keyword = ''
75
+ query.pageIndex = 1
76
+ load()
77
+ }
78
+
79
+ function openCreate() {
80
+ Object.assign(form, {
81
+ id: undefined,
82
+ configName: '',
83
+ configKey: '',
84
+ configValue: '',
85
+ configType: 'N',
86
+ remark: '',
87
+ })
88
+ dialogVisible.value = true
89
+ }
90
+
91
+ async function openEdit(row: ConfigItem) {
92
+ const detail = await get<ConfigForm>(`/configs/${row.id}`)
93
+ Object.assign(form, {
94
+ id: detail.id,
95
+ configName: detail.configName || '',
96
+ configKey: detail.configKey || '',
97
+ configValue: detail.configValue || '',
98
+ configType: detail.configType || 'N',
99
+ remark: detail.remark || '',
100
+ })
101
+ dialogVisible.value = true
102
+ }
103
+
104
+ async function save() {
105
+ if (!form.configName.trim() || !form.configKey.trim()) {
106
+ ElMessage.warning('请填写参数名称和键名')
107
+ return
108
+ }
109
+ saving.value = true
110
+ try {
111
+ const payload = {
112
+ id: form.id,
113
+ configName: form.configName.trim(),
114
+ configKey: form.configKey.trim(),
115
+ configValue: form.configValue,
116
+ configType: form.configType || 'N',
117
+ remark: form.remark.trim() || null,
118
+ }
119
+ if (form.id) await put('/configs', payload)
120
+ else await post('/configs', payload)
121
+ ElMessage.success('保存成功')
122
+ dialogVisible.value = false
123
+ load()
124
+ } finally {
125
+ saving.value = false
126
+ }
127
+ }
128
+
129
+ async function remove(row: ConfigItem) {
130
+ if (row.configType === 'Y') {
131
+ ElMessage.warning('系统内置参数不可删除')
132
+ return
133
+ }
134
+ await ElMessageBox.confirm(`确认删除参数「${row.configName}」?`, '提示', { type: 'warning' })
135
+ await del('/configs', { ids: [row.id] })
136
+ ElMessage.success('已删除')
137
+ load()
138
+ }
139
+
140
+ onMounted(load)
141
+ </script>
142
+
143
+ <template>
144
+ <AppPage title="参数设置" description="系统运行参数与开关配置">
145
+ <div class="cfg-stats">
146
+ <div class="cfg-stats__item">
147
+ <span class="cfg-stats__num">{{ total }}</span>
148
+ <span class="cfg-stats__label">全部参数</span>
149
+ </div>
150
+ <div class="cfg-stats__item cfg-stats__item--ok">
151
+ <span class="cfg-stats__num">{{ pageStats.builtin }}</span>
152
+ <span class="cfg-stats__label">本页内置</span>
153
+ </div>
154
+ <div class="cfg-stats__item cfg-stats__item--hit">
155
+ <span class="cfg-stats__num">{{ pageStats.custom }}</span>
156
+ <span class="cfg-stats__label">本页自定义</span>
157
+ </div>
158
+ <div class="cfg-stats__item">
159
+ <span class="cfg-stats__num">{{ list.length }}</span>
160
+ <span class="cfg-stats__label">当前页条数</span>
161
+ </div>
162
+ </div>
163
+
164
+ <div class="toolbar">
165
+ <el-input
166
+ v-model="query.keyword"
167
+ clearable
168
+ placeholder="搜索名称 / 键名"
169
+ style="width: 240px"
170
+ @keyup.enter="search"
171
+ />
172
+ <el-button type="primary" :icon="Search" @click="search">查询</el-button>
173
+ <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
174
+ <div class="toolbar__spacer" />
175
+ <el-button type="success" :icon="Plus" v-permission="'system:config:add'" @click="openCreate">
176
+ 新增参数
177
+ </el-button>
178
+ </div>
179
+
180
+ <div class="table-wrap" v-loading="loading">
181
+ <el-table :data="list" border stripe empty-text="暂无参数">
182
+ <el-table-column label="参数名称" min-width="160" show-overflow-tooltip>
183
+ <template #default="{ row }">
184
+ <div class="name-cell">
185
+ <span class="name-cell__icon">
186
+ <el-icon :size="14"><Tools /></el-icon>
187
+ </span>
188
+ <strong>{{ row.configName }}</strong>
189
+ </div>
190
+ </template>
191
+ </el-table-column>
192
+ <el-table-column label="参数键名" min-width="160" show-overflow-tooltip>
193
+ <template #default="{ row }">
194
+ <code class="mono">{{ row.configKey }}</code>
195
+ </template>
196
+ </el-table-column>
197
+ <el-table-column prop="configValue" label="参数键值" min-width="180" show-overflow-tooltip />
198
+ <el-table-column label="内置" width="88" align="center">
199
+ <template #default="{ row }">
200
+ <el-tag size="small" :type="row.configType === 'Y' ? 'warning' : 'info'">
201
+ {{ row.configType === 'Y' ? '是' : '否' }}
202
+ </el-tag>
203
+ </template>
204
+ </el-table-column>
205
+ <el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip>
206
+ <template #default="{ row }">
207
+ <span v-if="row.remark">{{ row.remark }}</span>
208
+ <span v-else class="muted">—</span>
209
+ </template>
210
+ </el-table-column>
211
+ <el-table-column label="操作" width="180" fixed="right" align="center">
212
+ <template #default="{ row }">
213
+ <div class="row-actions">
214
+ <el-button
215
+ size="small"
216
+ type="primary"
217
+ v-permission="'system:config:edit'"
218
+ @click="openEdit(row)"
219
+ >
220
+ 编辑
221
+ </el-button>
222
+ <el-button
223
+ size="small"
224
+ type="danger"
225
+ :disabled="row.configType === 'Y'"
226
+ v-permission="'system:config:remove'"
227
+ @click="remove(row)"
228
+ >
229
+ 删除
230
+ </el-button>
231
+ </div>
232
+ </template>
233
+ </el-table-column>
234
+ </el-table>
235
+ </div>
236
+
237
+ <div class="pager">
238
+ <el-pagination
239
+ background
240
+ layout="total, sizes, prev, pager, next"
241
+ :total="total"
242
+ v-model:current-page="query.pageIndex"
243
+ v-model:page-size="query.pageSize"
244
+ :page-sizes="[10, 20, 50]"
245
+ @current-change="load"
246
+ @size-change="
247
+ () => {
248
+ query.pageIndex = 1
249
+ load()
250
+ }
251
+ "
252
+ />
253
+ </div>
254
+
255
+ <el-dialog
256
+ v-model="dialogVisible"
257
+ :title="form.id ? '编辑参数' : '新增参数'"
258
+ width="560px"
259
+ class="sa-dialog"
260
+ append-to-body
261
+ align-center
262
+ destroy-on-close
263
+ >
264
+ <el-form label-width="96px">
265
+ <el-form-item label="参数名称" required>
266
+ <el-input v-model="form.configName" maxlength="64" placeholder="显示名称" />
267
+ </el-form-item>
268
+ <el-form-item label="参数键名" required>
269
+ <el-input
270
+ v-model="form.configKey"
271
+ maxlength="128"
272
+ placeholder="唯一键,如 sys.user.initPassword"
273
+ :disabled="form.configType === 'Y' && !!form.id"
274
+ />
275
+ </el-form-item>
276
+ <el-form-item label="参数键值" required>
277
+ <el-input v-model="form.configValue" type="textarea" :rows="3" placeholder="配置值" />
278
+ </el-form-item>
279
+ <el-form-item label="系统内置">
280
+ <el-radio-group v-model="form.configType" :disabled="!!form.id && form.configType === 'Y'">
281
+ <el-radio value="Y">是</el-radio>
282
+ <el-radio value="N">否</el-radio>
283
+ </el-radio-group>
284
+ </el-form-item>
285
+ <el-form-item label="备注">
286
+ <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="200" />
287
+ </el-form-item>
288
+ </el-form>
289
+ <template #footer>
290
+ <el-button @click="dialogVisible = false">取消</el-button>
291
+ <el-button type="primary" :loading="saving" @click="save">保存</el-button>
292
+ </template>
293
+ </el-dialog>
294
+ </AppPage>
295
+ </template>
296
+
297
+ <style scoped lang="scss">
298
+ .cfg-stats {
299
+ display: grid;
300
+ grid-template-columns: repeat(4, minmax(0, 1fr));
301
+ gap: 12px;
302
+ margin-bottom: 14px;
303
+ }
304
+ .cfg-stats__item {
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: 4px;
308
+ padding: 14px 16px;
309
+ border-radius: 14px;
310
+ border: 1px solid var(--sa-border);
311
+ background: var(--sa-panel);
312
+ box-shadow: var(--sa-shadow);
313
+ }
314
+ .cfg-stats__num {
315
+ font-size: 22px;
316
+ font-weight: 800;
317
+ letter-spacing: -0.03em;
318
+ color: var(--sa-ink);
319
+ font-variant-numeric: tabular-nums;
320
+ }
321
+ .cfg-stats__label {
322
+ font-size: 12px;
323
+ color: var(--sa-ink-muted);
324
+ }
325
+ .cfg-stats__item--ok .cfg-stats__num {
326
+ color: #b45309;
327
+ }
328
+ .cfg-stats__item--hit .cfg-stats__num {
329
+ color: #0369a1;
330
+ }
331
+
332
+ .toolbar {
333
+ display: flex;
334
+ flex-wrap: wrap;
335
+ gap: 10px;
336
+ align-items: center;
337
+ margin-bottom: 14px;
338
+ padding: 12px 14px;
339
+ background: var(--sa-toolbar-bg);
340
+ border: 1px solid var(--sa-border);
341
+ border-radius: var(--sa-radius-sm);
342
+ }
343
+ .toolbar__spacer {
344
+ flex: 1;
345
+ }
346
+ .table-wrap {
347
+ border: 1px solid var(--sa-border);
348
+ border-radius: 14px;
349
+ overflow: hidden;
350
+ background: var(--sa-panel);
351
+ box-shadow: var(--sa-shadow);
352
+ }
353
+ .name-cell {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ gap: 8px;
357
+ }
358
+ .name-cell__icon {
359
+ width: 26px;
360
+ height: 26px;
361
+ border-radius: 8px;
362
+ display: grid;
363
+ place-items: center;
364
+ background: var(--sa-accent-soft);
365
+ color: var(--sa-accent);
366
+ }
367
+ .name-cell strong {
368
+ font-size: 13px;
369
+ font-weight: 650;
370
+ }
371
+ .mono {
372
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
373
+ font-size: 12px;
374
+ color: var(--sa-ink-secondary);
375
+ background: var(--sa-toolbar-bg);
376
+ padding: 1px 6px;
377
+ border-radius: 6px;
378
+ }
379
+ .muted {
380
+ color: var(--sa-ink-muted);
381
+ }
382
+ .pager {
383
+ display: flex;
384
+ justify-content: flex-end;
385
+ margin-top: 14px;
386
+ }
387
+ @media (max-width: 900px) {
388
+ .cfg-stats {
389
+ grid-template-columns: repeat(2, minmax(0, 1fr));
390
+ }
391
+ }
392
+ </style>
393
+
394
+ <style>
395
+ .sa-dialog.el-dialog {
396
+ width: min(560px, calc(100vw - 32px)) !important;
397
+ max-height: 88vh;
398
+ overflow: hidden;
399
+ display: flex;
400
+ flex-direction: column;
401
+ }
402
+ .sa-dialog .el-dialog__body {
403
+ overflow-x: hidden;
404
+ overflow-y: auto;
405
+ max-height: calc(88vh - 140px);
406
+ }
407
+ </style>