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,373 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, reactive, ref } from 'vue'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
+ import { get, post, put, del, getSilent } from '@/utils/request'
5
+ import AppPage from '@/components/AppPage.vue'
6
+ import { formatDateTime } from '@/utils/datetime'
7
+
8
+ interface UserItem {
9
+ id: number
10
+ userName: string
11
+ nickName: string
12
+ phone?: string
13
+ status: number
14
+ deptId?: number | null
15
+ deptName?: string
16
+ createTime: string
17
+ }
18
+
19
+ interface DeptTreeNode {
20
+ id: number
21
+ label: string
22
+ children?: DeptTreeNode[]
23
+ }
24
+
25
+ /** 扁平下拉选项(岗位 / 角色)。 */
26
+ interface SelectOption {
27
+ id: number
28
+ label: string
29
+ }
30
+
31
+ interface UserForm {
32
+ id?: number
33
+ deptId?: number | null
34
+ userName: string
35
+ password: string
36
+ nickName: string
37
+ phone: string
38
+ email: string
39
+ status: number
40
+ sex: number
41
+ roleIds: number[]
42
+ postIds: number[]
43
+ }
44
+
45
+ const loading = ref(false)
46
+ const total = ref(0)
47
+ const list = ref<UserItem[]>([])
48
+ const deptTree = ref<DeptTreeNode[]>([])
49
+ const roleOptions = ref<SelectOption[]>([])
50
+ const postOptions = ref<SelectOption[]>([])
51
+ const query = reactive({ pageIndex: 1, pageSize: 10, keyword: '' })
52
+ const dialogVisible = ref(false)
53
+ const form = reactive<UserForm>({
54
+ id: undefined,
55
+ deptId: undefined,
56
+ userName: '',
57
+ password: '',
58
+ nickName: '',
59
+ phone: '',
60
+ email: '',
61
+ status: 0,
62
+ sex: 0,
63
+ roleIds: [],
64
+ postIds: [],
65
+ })
66
+
67
+ const treeSelectProps = {
68
+ label: 'label',
69
+ value: 'id',
70
+ children: 'children',
71
+ }
72
+
73
+ async function loadDepts() {
74
+ deptTree.value = (await getSilent<DeptTreeNode[]>('/depts/options')) || []
75
+ }
76
+
77
+ /** 加载角色、岗位下拉(用户表单用)。 */
78
+ async function loadFormOptions() {
79
+ const [roles, posts] = await Promise.all([
80
+ getSilent<SelectOption[]>('/roles/options'),
81
+ getSilent<SelectOption[]>('/posts/options'),
82
+ ])
83
+ roleOptions.value = roles || []
84
+ postOptions.value = posts || []
85
+ }
86
+
87
+ async function load() {
88
+ loading.value = true
89
+ try {
90
+ const data = await get<{ total: number; items: UserItem[] }>('/users', query)
91
+ total.value = data.total
92
+ list.value = data.items
93
+ } finally {
94
+ loading.value = false
95
+ }
96
+ }
97
+
98
+ function resetForm() {
99
+ Object.assign(form, {
100
+ id: undefined,
101
+ deptId: undefined,
102
+ userName: '',
103
+ password: 'admin123',
104
+ nickName: '',
105
+ phone: '',
106
+ email: '',
107
+ status: 0,
108
+ sex: 0,
109
+ roleIds: [],
110
+ postIds: [],
111
+ })
112
+ }
113
+
114
+ async function ensureFormLookups() {
115
+ const tasks: Promise<void>[] = []
116
+ if (!deptTree.value.length) tasks.push(loadDepts())
117
+ if (!roleOptions.value.length || !postOptions.value.length) tasks.push(loadFormOptions())
118
+ if (tasks.length) await Promise.all(tasks)
119
+ }
120
+
121
+ async function openCreate() {
122
+ resetForm()
123
+ await ensureFormLookups()
124
+ dialogVisible.value = true
125
+ }
126
+
127
+ async function openEdit(row: UserItem) {
128
+ await ensureFormLookups()
129
+ const detail = await get<UserForm & { id: number }>(`/users/${row.id}`)
130
+ Object.assign(form, {
131
+ id: detail.id,
132
+ deptId: detail.deptId ?? undefined,
133
+ userName: detail.userName,
134
+ password: '',
135
+ nickName: detail.nickName || '',
136
+ phone: detail.phone || '',
137
+ email: (detail as { email?: string }).email || '',
138
+ status: detail.status ?? 0,
139
+ sex: detail.sex ?? 0,
140
+ roleIds: detail.roleIds || [],
141
+ postIds: detail.postIds || [],
142
+ })
143
+ dialogVisible.value = true
144
+ }
145
+
146
+ async function save() {
147
+ if (!form.userName.trim()) {
148
+ ElMessage.warning('请填写账号')
149
+ return
150
+ }
151
+ if (!form.nickName.trim()) {
152
+ ElMessage.warning('请填写昵称')
153
+ return
154
+ }
155
+ if (!form.id && !form.password) {
156
+ ElMessage.warning('请填写密码')
157
+ return
158
+ }
159
+ const payload = {
160
+ id: form.id,
161
+ deptId: form.deptId ?? null,
162
+ userName: form.userName.trim(),
163
+ password: form.id ? undefined : form.password,
164
+ nickName: form.nickName.trim(),
165
+ phone: form.phone.trim() || null,
166
+ email: form.email.trim() || null,
167
+ status: form.status,
168
+ sex: form.sex,
169
+ roleIds: form.roleIds,
170
+ postIds: form.postIds,
171
+ }
172
+ if (form.id) await put('/users', payload)
173
+ else await post('/users', payload)
174
+ ElMessage.success('保存成功')
175
+ dialogVisible.value = false
176
+ load()
177
+ }
178
+
179
+ async function remove(row: UserItem) {
180
+ await ElMessageBox.confirm(`确认删除用户 ${row.userName}?`, '提示')
181
+ await del('/users', { ids: [row.id] })
182
+ ElMessage.success('已删除')
183
+ load()
184
+ }
185
+
186
+ /**
187
+ * 重置指定用户密码。
188
+ * @param row 列表行
189
+ */
190
+ async function resetPassword(row: UserItem) {
191
+ const { value } = await ElMessageBox.prompt(
192
+ `为账号「${row.userName}」设置新密码(至少 6 位)`,
193
+ '重置密码',
194
+ {
195
+ confirmButtonText: '确定',
196
+ cancelButtonText: '取消',
197
+ inputType: 'password',
198
+ inputPlaceholder: '请输入新密码',
199
+ inputValidator: (val) => {
200
+ if (!val || String(val).length < 6) return '密码至少 6 位'
201
+ return true
202
+ },
203
+ },
204
+ )
205
+ await put('/users/reset-password', { userId: row.id, password: value })
206
+ ElMessage.success('密码已重置')
207
+ }
208
+
209
+ onMounted(async () => {
210
+ await Promise.all([load(), loadDepts(), loadFormOptions()])
211
+ })
212
+ </script>
213
+
214
+ <template>
215
+ <AppPage title="用户管理" description="维护登录账号、部门归属与角色岗位">
216
+ <div class="toolbar">
217
+ <el-input v-model="query.keyword" placeholder="账号/昵称" clearable style="width: 220px" @keyup.enter="load" />
218
+ <el-button type="primary" @click="load">查询</el-button>
219
+ <el-button type="success" v-permission="'system:user:add'" @click="openCreate">新增</el-button>
220
+ </div>
221
+ <el-table :data="list" v-loading="loading" border stripe>
222
+ <el-table-column prop="userName" label="账号" min-width="110" />
223
+ <el-table-column prop="nickName" label="昵称" min-width="110" />
224
+ <el-table-column prop="deptName" label="部门" min-width="120" show-overflow-tooltip />
225
+ <el-table-column prop="phone" label="手机" min-width="120" show-overflow-tooltip>
226
+ <template #default="{ row }">
227
+ {{ row.phone || '—' }}
228
+ </template>
229
+ </el-table-column>
230
+ <el-table-column prop="status" label="状态" width="90">
231
+ <template #default="{ row }">
232
+ <el-tag :type="row.status === 0 ? 'success' : 'danger'">{{ row.status === 0 ? '正常' : '停用' }}</el-tag>
233
+ </template>
234
+ </el-table-column>
235
+ <el-table-column
236
+ prop="createTime"
237
+ label="创建时间"
238
+ width="170"
239
+ class-name="is-datetime"
240
+ show-overflow-tooltip
241
+ >
242
+ <template #default="{ row }">
243
+ <span class="datetime-cell">{{ formatDateTime(row.createTime) }}</span>
244
+ </template>
245
+ </el-table-column>
246
+ <el-table-column label="操作" width="300" fixed="right" align="center">
247
+ <template #default="{ row }">
248
+ <div class="row-actions">
249
+ <el-button
250
+ size="small"
251
+ type="primary"
252
+ v-permission="'system:user:edit'"
253
+ @click="openEdit(row)"
254
+ >
255
+ 编辑
256
+ </el-button>
257
+ <el-button
258
+ size="small"
259
+ type="warning"
260
+ v-permission="'system:user:resetPwd'"
261
+ @click="resetPassword(row)"
262
+ >
263
+ 重置密码
264
+ </el-button>
265
+ <el-button
266
+ size="small"
267
+ type="danger"
268
+ v-permission="'system:user:remove'"
269
+ @click="remove(row)"
270
+ >
271
+ 删除
272
+ </el-button>
273
+ </div>
274
+ </template>
275
+ </el-table-column>
276
+ </el-table>
277
+ <el-pagination
278
+ class="pager"
279
+ background
280
+ layout="total, prev, pager, next"
281
+ :total="total"
282
+ v-model:current-page="query.pageIndex"
283
+ v-model:page-size="query.pageSize"
284
+ @current-change="load"
285
+ />
286
+ <el-dialog
287
+ v-model="dialogVisible"
288
+ :title="form.id ? '编辑用户' : '新增用户'"
289
+ width="560px"
290
+ append-to-body
291
+ align-center
292
+ destroy-on-close
293
+ >
294
+ <el-form label-width="80px">
295
+ <el-form-item label="账号">
296
+ <el-input v-model="form.userName" :disabled="!!form.id" />
297
+ </el-form-item>
298
+ <el-form-item v-if="!form.id" label="密码">
299
+ <el-input v-model="form.password" type="password" show-password />
300
+ </el-form-item>
301
+ <el-form-item label="昵称">
302
+ <el-input v-model="form.nickName" />
303
+ </el-form-item>
304
+ <el-form-item label="部门">
305
+ <el-tree-select
306
+ v-model="form.deptId"
307
+ :data="deptTree"
308
+ :props="treeSelectProps"
309
+ check-strictly
310
+ clearable
311
+ filterable
312
+ placeholder="请选择部门"
313
+ style="width: 100%"
314
+ />
315
+ </el-form-item>
316
+ <el-form-item label="角色">
317
+ <el-select
318
+ v-model="form.roleIds"
319
+ multiple
320
+ filterable
321
+ clearable
322
+ collapse-tags
323
+ collapse-tags-tooltip
324
+ placeholder="请选择角色"
325
+ style="width: 100%"
326
+ >
327
+ <el-option
328
+ v-for="item in roleOptions"
329
+ :key="item.id"
330
+ :label="item.label"
331
+ :value="item.id"
332
+ />
333
+ </el-select>
334
+ </el-form-item>
335
+ <el-form-item label="岗位">
336
+ <el-select
337
+ v-model="form.postIds"
338
+ multiple
339
+ filterable
340
+ clearable
341
+ collapse-tags
342
+ collapse-tags-tooltip
343
+ placeholder="请选择岗位"
344
+ style="width: 100%"
345
+ >
346
+ <el-option
347
+ v-for="item in postOptions"
348
+ :key="item.id"
349
+ :label="item.label"
350
+ :value="item.id"
351
+ />
352
+ </el-select>
353
+ </el-form-item>
354
+ <el-form-item label="手机">
355
+ <el-input v-model="form.phone" />
356
+ </el-form-item>
357
+ <el-form-item label="邮箱">
358
+ <el-input v-model="form.email" />
359
+ </el-form-item>
360
+ <el-form-item label="状态">
361
+ <el-radio-group v-model="form.status">
362
+ <el-radio :value="0">正常</el-radio>
363
+ <el-radio :value="1">停用</el-radio>
364
+ </el-radio-group>
365
+ </el-form-item>
366
+ </el-form>
367
+ <template #footer>
368
+ <el-button @click="dialogVisible = false">取消</el-button>
369
+ <el-button type="primary" @click="save">确定</el-button>
370
+ </template>
371
+ </el-dialog>
372
+ </AppPage>
373
+ </template>
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "module": "ESNext",
7
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "Bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "preserve",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true,
24
+ "baseUrl": ".",
25
+ "paths": {
26
+ "@/*": ["src/*"]
27
+ }
28
+ },
29
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
30
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "Bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { fileURLToPath, URL } from 'node:url'
4
+
5
+ // Element Plus 已在 main.ts 全量注册,此处不再使用 unplugin(避免 Node/ESM 兼容问题)
6
+ export default defineConfig({
7
+ plugins: [vue()],
8
+ resolve: {
9
+ alias: {
10
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
11
+ },
12
+ },
13
+ server: {
14
+ port: 5173,
15
+ proxy: {
16
+ '/api': {
17
+ target: 'http://localhost:5080',
18
+ changeOrigin: true,
19
+ },
20
+ },
21
+ },
22
+ })