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,161 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { RouteLocationNormalizedLoaded } from 'vue-router'
4
+
5
+ /** 已打开的页签项 */
6
+ export interface TagView {
7
+ /** 路由 path(不含 query) */
8
+ path: string
9
+ /** 完整路径(含 query) */
10
+ fullPath: string
11
+ /** 展示标题 */
12
+ title: string
13
+ /** 路由 name(与组件 name / keep-alive include 对齐) */
14
+ name?: string | symbol | null
15
+ /** 固定页签(如首页),不可关闭 */
16
+ affix?: boolean
17
+ }
18
+
19
+ const HOME_PATH = '/index'
20
+
21
+ /**
22
+ * 多页签:记录已打开页面,支持关闭(首页固定不可关)。
23
+ */
24
+ export const useTagsViewStore = defineStore('tagsView', () => {
25
+ const visited = ref<TagView[]>([
26
+ { path: HOME_PATH, fullPath: HOME_PATH, title: '首页', name: 'Home', affix: true },
27
+ ])
28
+
29
+ /**
30
+ * keep-alive 缓存的组件名列表(独立 ref,仅在集合变化时更新,避免无意义的新数组触发重挂载)。
31
+ */
32
+ const cachedNames = ref<string[]>(['Home'])
33
+
34
+ /**
35
+ * 按当前 visited 同步缓存名(内容不变则不写 ref)。
36
+ */
37
+ function syncCachedNames() {
38
+ const next = visited.value
39
+ .map((t) => (typeof t.name === 'string' ? t.name : ''))
40
+ .filter(Boolean)
41
+ const prev = cachedNames.value
42
+ if (prev.length === next.length && prev.every((n, i) => n === next[i])) {
43
+ return
44
+ }
45
+ cachedNames.value = next
46
+ }
47
+
48
+ /**
49
+ * 根据当前路由登记页签(已存在且内容相同则跳过,避免触发 keep-alive 重挂载)。
50
+ * @param route 当前路由
51
+ */
52
+ function addView(route: RouteLocationNormalizedLoaded) {
53
+ if (route.meta?.public) return
54
+ if (route.name === 'NotFound' || route.path.startsWith('/login')) return
55
+
56
+ const path = route.path
57
+ const title = (route.meta?.title as string) || (typeof route.name === 'string' ? route.name : path)
58
+ const affix = Boolean(route.meta?.affix) || path === HOME_PATH
59
+ const name = route.name
60
+ const existing = visited.value.find((t) => t.path === path)
61
+ if (existing) {
62
+ if (
63
+ existing.fullPath === route.fullPath &&
64
+ existing.title === title &&
65
+ existing.name === name &&
66
+ existing.affix === affix
67
+ ) {
68
+ return
69
+ }
70
+ existing.fullPath = route.fullPath
71
+ existing.title = title
72
+ existing.name = name
73
+ existing.affix = affix
74
+ syncCachedNames()
75
+ return
76
+ }
77
+
78
+ visited.value.push({
79
+ path,
80
+ fullPath: route.fullPath,
81
+ title,
82
+ name,
83
+ affix,
84
+ })
85
+ syncCachedNames()
86
+ }
87
+
88
+ /**
89
+ * 关闭指定 path 的页签。
90
+ * @param path 路由 path
91
+ * @returns 关闭后应跳转的 fullPath;若无需跳转返回 null
92
+ */
93
+ function closeView(path: string): string | null {
94
+ const idx = visited.value.findIndex((t) => t.path === path)
95
+ if (idx < 0) return null
96
+ const tag = visited.value[idx]
97
+ if (tag.affix) return null
98
+
99
+ visited.value.splice(idx, 1)
100
+ if (visited.value.length === 0) {
101
+ visited.value.push({
102
+ path: HOME_PATH,
103
+ fullPath: HOME_PATH,
104
+ title: '首页',
105
+ name: 'Home',
106
+ affix: true,
107
+ })
108
+ syncCachedNames()
109
+ return HOME_PATH
110
+ }
111
+ syncCachedNames()
112
+ const next = visited.value[idx] || visited.value[idx - 1]
113
+ return next?.fullPath ?? HOME_PATH
114
+ }
115
+
116
+ /**
117
+ * 关闭除固定页与当前页外的全部页签。
118
+ * @param currentPath 保留的当前 path
119
+ */
120
+ function closeOthers(currentPath: string) {
121
+ visited.value = visited.value.filter((t) => t.affix || t.path === currentPath)
122
+ syncCachedNames()
123
+ }
124
+
125
+ /**
126
+ * 关闭全部非固定页签。
127
+ * @returns 应跳转的 fullPath
128
+ */
129
+ function closeAll(): string {
130
+ visited.value = visited.value.filter((t) => t.affix)
131
+ if (!visited.value.length) {
132
+ visited.value.push({
133
+ path: HOME_PATH,
134
+ fullPath: HOME_PATH,
135
+ title: '首页',
136
+ name: 'Home',
137
+ affix: true,
138
+ })
139
+ }
140
+ syncCachedNames()
141
+ return visited.value[0]?.fullPath || HOME_PATH
142
+ }
143
+
144
+ /** 登出等场景重置为仅首页 */
145
+ function reset() {
146
+ visited.value = [
147
+ { path: HOME_PATH, fullPath: HOME_PATH, title: '首页', name: 'Home', affix: true },
148
+ ]
149
+ cachedNames.value = ['Home']
150
+ }
151
+
152
+ return {
153
+ visited,
154
+ cachedNames,
155
+ addView,
156
+ closeView,
157
+ closeOthers,
158
+ closeAll,
159
+ reset,
160
+ }
161
+ })
@@ -0,0 +1,68 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+
4
+ /** 可选主题标识 */
5
+ export type ThemeId = 'teal' | 'ocean' | 'forest' | 'amber' | 'slate' | 'dark'
6
+
7
+ /** 主题选项(用于右上角切换) */
8
+ export interface ThemeOption {
9
+ id: ThemeId
10
+ label: string
11
+ /** 色板预览色 */
12
+ swatch: string
13
+ }
14
+
15
+ const STORAGE_KEY = 'sa_theme'
16
+
17
+ /** 内置主题列表 */
18
+ export const THEME_OPTIONS: ThemeOption[] = [
19
+ { id: 'teal', label: '青绿', swatch: '#0f766e' },
20
+ { id: 'ocean', label: '海蓝', swatch: '#0369a1' },
21
+ { id: 'forest', label: '森绿', swatch: '#15803d' },
22
+ { id: 'amber', label: '琥珀', swatch: '#b45309' },
23
+ { id: 'slate', label: '岩灰', swatch: '#475569' },
24
+ { id: 'dark', label: '暗色', swatch: '#0f172a' },
25
+ ]
26
+
27
+ /**
28
+ * 将主题应用到 document(CSS 变量由 html[data-theme] 驱动)。
29
+ * @param id 主题标识
30
+ */
31
+ export function applyTheme(id: ThemeId) {
32
+ document.documentElement.setAttribute('data-theme', id)
33
+ }
34
+
35
+ /**
36
+ * 从本地存储读取主题,非法值回退青绿。
37
+ */
38
+ function readStoredTheme(): ThemeId {
39
+ const raw = localStorage.getItem(STORAGE_KEY) as ThemeId | null
40
+ if (raw && THEME_OPTIONS.some((t) => t.id === raw)) return raw
41
+ return 'teal'
42
+ }
43
+
44
+ /**
45
+ * 界面主题状态。
46
+ */
47
+ export const useThemeStore = defineStore('theme', () => {
48
+ const themeId = ref<ThemeId>(readStoredTheme())
49
+
50
+ const current = computed(() => THEME_OPTIONS.find((t) => t.id === themeId.value) || THEME_OPTIONS[0])
51
+
52
+ /**
53
+ * 切换并持久化主题。
54
+ * @param id 目标主题
55
+ */
56
+ function setTheme(id: ThemeId) {
57
+ themeId.value = id
58
+ localStorage.setItem(STORAGE_KEY, id)
59
+ applyTheme(id)
60
+ }
61
+
62
+ /** 应用当前主题(应用启动时调用)。 */
63
+ function init() {
64
+ applyTheme(themeId.value)
65
+ }
66
+
67
+ return { themeId, current, setTheme, init }
68
+ })
@@ -0,0 +1,191 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import { get, post } from '@/utils/request'
4
+
5
+ export interface AuthUserInfo {
6
+ userId: number
7
+ userName: string
8
+ nickName: string
9
+ avatar?: string
10
+ permissions: string[]
11
+ roles: string[]
12
+ /** 上次成功登录时间(不含本次) */
13
+ lastLoginTime?: string | null
14
+ /** 上次登录 IP */
15
+ lastLoginIp?: string | null
16
+ }
17
+
18
+ export interface SidebarRouter {
19
+ name: string
20
+ path: string
21
+ component?: string | null
22
+ meta: { title: string; icon?: string; hidden?: boolean }
23
+ children?: SidebarRouter[]
24
+ }
25
+
26
+ export interface CaptchaResult {
27
+ captchaId: string
28
+ imageBase64: string
29
+ expireSeconds: number
30
+ devCode?: string | null
31
+ }
32
+
33
+ export interface SendSmsCodeResult {
34
+ expireSeconds: number
35
+ intervalSeconds: number
36
+ devCode?: string | null
37
+ }
38
+
39
+ export interface LoginOptions {
40
+ /** 是否启用手机号短信登录 */
41
+ enableSmsLogin: boolean
42
+ }
43
+
44
+ export interface TokenPairResult {
45
+ accessToken: string
46
+ expiresIn: number
47
+ refreshToken: string
48
+ refreshExpiresIn: number
49
+ user: AuthUserInfo
50
+ }
51
+
52
+ export interface PasswordLoginPayload {
53
+ loginType: 'password'
54
+ userName: string
55
+ password: string
56
+ captchaId: string
57
+ captchaCode: string
58
+ }
59
+
60
+ export interface SmsLoginPayload {
61
+ loginType: 'sms'
62
+ phone: string
63
+ smsCode: string
64
+ }
65
+
66
+ export type LoginPayload = PasswordLoginPayload | SmsLoginPayload
67
+
68
+ const TOKEN_KEY = 'sa_token'
69
+ const REFRESH_KEY = 'sa_refresh_token'
70
+
71
+ export const useUserStore = defineStore('user', () => {
72
+ const token = ref<string>(localStorage.getItem(TOKEN_KEY) || '')
73
+ const refreshToken = ref<string>(localStorage.getItem(REFRESH_KEY) || '')
74
+ const userInfo = ref<AuthUserInfo | null>(null)
75
+ const routersLoaded = ref(false)
76
+ /** 侧栏菜单树(来自 /auth/routers) */
77
+ const menus = ref<SidebarRouter[]>([])
78
+
79
+ function setToken(value: string) {
80
+ token.value = value
81
+ if (value) localStorage.setItem(TOKEN_KEY, value)
82
+ else localStorage.removeItem(TOKEN_KEY)
83
+ }
84
+
85
+ function setRefreshToken(value: string) {
86
+ refreshToken.value = value
87
+ if (value) localStorage.setItem(REFRESH_KEY, value)
88
+ else localStorage.removeItem(REFRESH_KEY)
89
+ }
90
+
91
+ function applyTokenPair(data: TokenPairResult) {
92
+ setToken(data.accessToken)
93
+ setRefreshToken(data.refreshToken || '')
94
+ if (data.user) userInfo.value = data.user
95
+ }
96
+
97
+ function setMenus(list: SidebarRouter[]) {
98
+ menus.value = list || []
99
+ }
100
+
101
+ function logoutLocal() {
102
+ token.value = ''
103
+ refreshToken.value = ''
104
+ userInfo.value = null
105
+ routersLoaded.value = false
106
+ menus.value = []
107
+ localStorage.removeItem(TOKEN_KEY)
108
+ localStorage.removeItem(REFRESH_KEY)
109
+ }
110
+
111
+ async function fetchCaptcha() {
112
+ return get<CaptchaResult>('/auth/captcha')
113
+ }
114
+
115
+ async function fetchLoginOptions() {
116
+ return get<LoginOptions>('/auth/login-options')
117
+ }
118
+
119
+ async function sendSmsCode(phone: string) {
120
+ return post<SendSmsCodeResult>('/auth/sms-code', { phone })
121
+ }
122
+
123
+ async function login(payload: LoginPayload) {
124
+ const data = await post<TokenPairResult>('/auth/login', payload)
125
+ if (!data?.accessToken) {
126
+ throw new Error('登录响应缺少 accessToken')
127
+ }
128
+ applyTokenPair(data)
129
+ return data
130
+ }
131
+
132
+ /**
133
+ * 用 refreshToken 换新令牌对(供请求拦截器单飞调用)。
134
+ */
135
+ async function refreshSession() {
136
+ const current = refreshToken.value
137
+ if (!current) {
138
+ throw new Error('无刷新令牌')
139
+ }
140
+ const data = await post<TokenPairResult>(
141
+ '/auth/refresh',
142
+ { refreshToken: current },
143
+ { silent: true, skipAuthRefresh: true },
144
+ )
145
+ if (!data?.accessToken || !data.refreshToken) {
146
+ throw new Error('刷新响应缺少令牌')
147
+ }
148
+ applyTokenPair(data)
149
+ return data
150
+ }
151
+
152
+ async function fetchInfo() {
153
+ userInfo.value = await get<AuthUserInfo>('/auth/info')
154
+ return userInfo.value
155
+ }
156
+
157
+ async function logout() {
158
+ try {
159
+ await post('/auth/logout')
160
+ } finally {
161
+ logoutLocal()
162
+ }
163
+ }
164
+
165
+ function hasPermission(code?: string) {
166
+ if (!code) return true
167
+ const perms = userInfo.value?.permissions || []
168
+ if (userInfo.value?.roles?.includes('super_admin')) return true
169
+ return perms.includes(code) || perms.includes('*')
170
+ }
171
+
172
+ return {
173
+ token,
174
+ refreshToken,
175
+ userInfo,
176
+ routersLoaded,
177
+ menus,
178
+ setToken,
179
+ setRefreshToken,
180
+ setMenus,
181
+ logoutLocal,
182
+ fetchCaptcha,
183
+ fetchLoginOptions,
184
+ sendSmsCode,
185
+ login,
186
+ refreshSession,
187
+ fetchInfo,
188
+ logout,
189
+ hasPermission,
190
+ }
191
+ })