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,172 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { Close } from '@element-plus/icons-vue'
5
+ import { useTagsViewStore, type TagView } from '@/stores/tagsView'
6
+
7
+ const route = useRoute()
8
+ const router = useRouter()
9
+ const tagsStore = useTagsViewStore()
10
+
11
+ function syncTag() {
12
+ tagsStore.addView(route)
13
+ }
14
+
15
+ onMounted(syncTag)
16
+ watch(() => route.fullPath, syncTag)
17
+
18
+ function isActive(tag: TagView) {
19
+ return tag.path === route.path
20
+ }
21
+
22
+ function openTag(tag: TagView) {
23
+ if (tag.path !== route.path) {
24
+ router.push(tag.fullPath || tag.path)
25
+ }
26
+ }
27
+
28
+ function onClose(e: Event, tag: TagView) {
29
+ e.stopPropagation()
30
+ if (tag.affix) return
31
+ const wasActive = isActive(tag)
32
+ const next = tagsStore.closeView(tag.path)
33
+ if (wasActive && next) {
34
+ router.push(next)
35
+ }
36
+ }
37
+
38
+ function onContext(cmd: string, tag: TagView) {
39
+ if (cmd === 'close' && !tag.affix) {
40
+ onClose(new Event('click'), tag)
41
+ } else if (cmd === 'others') {
42
+ tagsStore.closeOthers(tag.path)
43
+ if (route.path !== tag.path) {
44
+ router.push(tag.fullPath || tag.path)
45
+ }
46
+ } else if (cmd === 'all') {
47
+ const next = tagsStore.closeAll()
48
+ if (route.path !== next) {
49
+ router.push(next)
50
+ }
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <div class="tags-view">
57
+ <el-scrollbar class="tags-view__scroll">
58
+ <div class="tags-view__list">
59
+ <el-dropdown
60
+ v-for="tag in tagsStore.visited"
61
+ :key="tag.path"
62
+ trigger="contextmenu"
63
+ @command="(cmd: string) => onContext(cmd, tag)"
64
+ >
65
+ <button
66
+ type="button"
67
+ class="tags-view__item"
68
+ :class="{ 'is-active': isActive(tag) }"
69
+ @click="openTag(tag)"
70
+ >
71
+ <span class="tags-view__title">{{ tag.title }}</span>
72
+ <span
73
+ v-if="!tag.affix"
74
+ class="tags-view__close"
75
+ title="关闭"
76
+ @click="onClose($event, tag)"
77
+ >
78
+ <el-icon :size="12"><Close /></el-icon>
79
+ </span>
80
+ </button>
81
+ <template #dropdown>
82
+ <el-dropdown-menu>
83
+ <el-dropdown-item v-if="!tag.affix" command="close">关闭</el-dropdown-item>
84
+ <el-dropdown-item command="others">关闭其他</el-dropdown-item>
85
+ <el-dropdown-item command="all">关闭全部</el-dropdown-item>
86
+ </el-dropdown-menu>
87
+ </template>
88
+ </el-dropdown>
89
+ </div>
90
+ </el-scrollbar>
91
+ </div>
92
+ </template>
93
+
94
+ <style scoped lang="scss">
95
+ .tags-view {
96
+ flex-shrink: 0;
97
+ background: var(--sa-panel);
98
+ border-bottom: 1px solid var(--sa-border);
99
+ padding: 0 12px;
100
+ }
101
+
102
+ .tags-view__scroll {
103
+ white-space: nowrap;
104
+ height: 40px;
105
+ :deep(.el-scrollbar__wrap) {
106
+ overflow-y: hidden;
107
+ }
108
+ :deep(.el-scrollbar__view) {
109
+ height: 100%;
110
+ }
111
+ }
112
+
113
+ .tags-view__list {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ gap: 6px;
117
+ height: 40px;
118
+ padding: 4px 0;
119
+ }
120
+
121
+ .tags-view__item {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: 6px;
125
+ max-width: 180px;
126
+ height: 28px;
127
+ padding: 0 10px;
128
+ border: 1px solid var(--sa-border);
129
+ border-radius: 8px;
130
+ background: var(--sa-toolbar-bg);
131
+ color: var(--sa-ink-secondary);
132
+ font-size: 12px;
133
+ font-weight: 600;
134
+ cursor: pointer;
135
+ transition: border-color 0.15s, color 0.15s, background 0.15s, box-shadow 0.15s;
136
+ outline: none;
137
+
138
+ &:hover {
139
+ border-color: color-mix(in srgb, var(--sa-accent) 45%, var(--sa-border));
140
+ color: var(--sa-accent);
141
+ }
142
+
143
+ &.is-active {
144
+ color: var(--sa-accent);
145
+ border-color: color-mix(in srgb, var(--sa-accent) 55%, var(--sa-border));
146
+ background: var(--sa-accent-soft);
147
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--sa-accent) 12%, transparent);
148
+ }
149
+ }
150
+
151
+ .tags-view__title {
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ white-space: nowrap;
155
+ }
156
+
157
+ .tags-view__close {
158
+ display: grid;
159
+ place-items: center;
160
+ width: 16px;
161
+ height: 16px;
162
+ border-radius: 4px;
163
+ flex-shrink: 0;
164
+ color: var(--sa-ink-muted);
165
+ transition: background 0.15s, color 0.15s;
166
+
167
+ &:hover {
168
+ background: color-mix(in srgb, var(--el-color-danger) 18%, transparent);
169
+ color: var(--el-color-danger);
170
+ }
171
+ }
172
+ </style>
@@ -0,0 +1,205 @@
1
+ import type { Component } from 'vue'
2
+ import * as ElementPlusIcons from '@element-plus/icons-vue'
3
+
4
+ /**
5
+ * 菜单/侧栏常用 Element Plus 图标(组件名)。
6
+ */
7
+ export const MENU_ICON_OPTIONS: string[] = [
8
+ 'HomeFilled',
9
+ 'House',
10
+ 'Setting',
11
+ 'Monitor',
12
+ 'User',
13
+ 'UserFilled',
14
+ 'Avatar',
15
+ 'Menu',
16
+ 'Grid',
17
+ 'List',
18
+ 'Document',
19
+ 'Folder',
20
+ 'FolderOpened',
21
+ 'Files',
22
+ 'Bell',
23
+ 'Notification',
24
+ 'Message',
25
+ 'ChatDotRound',
26
+ 'Key',
27
+ 'Lock',
28
+ 'Unlock',
29
+ 'Search',
30
+ 'Edit',
31
+ 'EditPen',
32
+ 'Delete',
33
+ 'Plus',
34
+ 'Minus',
35
+ 'Check',
36
+ 'Close',
37
+ 'View',
38
+ 'Hide',
39
+ 'Download',
40
+ 'Upload',
41
+ 'Picture',
42
+ 'Camera',
43
+ 'Location',
44
+ 'OfficeBuilding',
45
+ 'School',
46
+ 'Briefcase',
47
+ 'Postcard',
48
+ 'Collection',
49
+ 'CollectionTag',
50
+ 'Tickets',
51
+ 'Ticket',
52
+ 'Calendar',
53
+ 'Clock',
54
+ 'Timer',
55
+ 'AlarmClock',
56
+ 'Connection',
57
+ 'Link',
58
+ 'Share',
59
+ 'Promotion',
60
+ 'Flag',
61
+ 'Star',
62
+ 'StarFilled',
63
+ 'Medal',
64
+ 'Trophy',
65
+ 'DataAnalysis',
66
+ 'DataBoard',
67
+ 'DataLine',
68
+ 'TrendCharts',
69
+ 'PieChart',
70
+ 'Histogram',
71
+ 'Odometer',
72
+ 'Cpu',
73
+ 'Platform',
74
+ 'Cellphone',
75
+ 'Iphone',
76
+ 'Headset',
77
+ 'Microphone',
78
+ 'VideoCamera',
79
+ 'Film',
80
+ 'Reading',
81
+ 'Notebook',
82
+ 'Memo',
83
+ 'Management',
84
+ 'Operation',
85
+ 'Tools',
86
+ 'SetUp',
87
+ 'Switch',
88
+ 'SwitchButton',
89
+ 'Refresh',
90
+ 'Filter',
91
+ 'Sort',
92
+ 'Rank',
93
+ 'Guide',
94
+ 'Compass',
95
+ 'MapLocation',
96
+ 'Place',
97
+ 'Van',
98
+ 'Ship',
99
+ 'Shop',
100
+ 'ShoppingCart',
101
+ 'Goods',
102
+ 'Wallet',
103
+ 'Money',
104
+ 'CreditCard',
105
+ 'Coin',
106
+ 'Present',
107
+ 'Box',
108
+ 'TakeawayBox',
109
+ 'CoffeeCup',
110
+ 'Food',
111
+ 'Bowl',
112
+ 'KnifeFork',
113
+ 'Sunny',
114
+ 'Moon',
115
+ 'Cloudy',
116
+ 'Umbrella',
117
+ 'WindPower',
118
+ 'Lightning',
119
+ 'FirstAidKit',
120
+ 'Help',
121
+ 'InfoFilled',
122
+ 'Warning',
123
+ 'WarnTriangleFilled',
124
+ 'QuestionFilled',
125
+ 'CircleCheck',
126
+ 'SuccessFilled',
127
+ 'Failed',
128
+ 'Remove',
129
+ 'CircleClose',
130
+ ]
131
+
132
+ /** 旧版/别名图标 → Element Plus 组件名 */
133
+ const ICON_ALIAS: Record<string, string> = {
134
+ home: 'HomeFilled',
135
+ system: 'Setting',
136
+ monitor: 'Monitor',
137
+ user: 'User',
138
+ peoples: 'Avatar',
139
+ 'tree-table': 'Menu',
140
+ tree: 'OfficeBuilding',
141
+ post: 'Postcard',
142
+ dict: 'CollectionTag',
143
+ edit: 'Tools',
144
+ megaphone: 'Notification',
145
+ message: 'Message',
146
+ file: 'FolderOpened',
147
+ form: 'Document',
148
+ logininfor: 'Key',
149
+ }
150
+
151
+ const iconComponentMap = new Map<string, Component>()
152
+ for (const name of MENU_ICON_OPTIONS) {
153
+ const comp = (ElementPlusIcons as Record<string, Component>)[name]
154
+ if (comp) iconComponentMap.set(name, comp)
155
+ }
156
+ for (const target of Object.values(ICON_ALIAS)) {
157
+ if (iconComponentMap.has(target)) continue
158
+ const comp = (ElementPlusIcons as Record<string, Component>)[target]
159
+ if (comp) iconComponentMap.set(target, comp)
160
+ }
161
+
162
+ /**
163
+ * 合并当前已选图标,避免历史数据不在预置列表中时无法回显。
164
+ * @param current 当前图标名
165
+ */
166
+ export function resolveMenuIconOptions(current?: string | null): string[] {
167
+ const name = (current || '').trim()
168
+ if (!name || MENU_ICON_OPTIONS.includes(name)) return MENU_ICON_OPTIONS
169
+ return [name, ...MENU_ICON_OPTIONS]
170
+ }
171
+
172
+ /**
173
+ * 将菜单 Icon 字段解析为 Element Plus 图标组件名。
174
+ * @param icon 数据库或路由 meta 中的图标
175
+ * @param fallback 找不到时的兜底图标
176
+ */
177
+ export function resolveMenuIconName(icon?: string | null, fallback = 'Menu'): string {
178
+ const raw = (icon || '').trim()
179
+ if (!raw) return fallback
180
+ if (ICON_ALIAS[raw]) return ICON_ALIAS[raw]
181
+ if (/^[A-Z]/.test(raw)) return raw
182
+ const lower = raw.toLowerCase()
183
+ if (ICON_ALIAS[lower]) return ICON_ALIAS[lower]
184
+ const pascal = raw
185
+ .split(/[-_]/)
186
+ .filter(Boolean)
187
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
188
+ .join('')
189
+ return pascal || fallback
190
+ }
191
+
192
+ /**
193
+ * 解析为 Vue 图标组件(按需映射,无需全局注册全部 icons)。
194
+ * @param icon 菜单图标名
195
+ * @param fallback 兜底图标名
196
+ */
197
+ export function resolveMenuIcon(icon?: string | null, fallback = 'Menu'): Component {
198
+ const name = resolveMenuIconName(icon, fallback)
199
+ return (
200
+ iconComponentMap.get(name) ||
201
+ (ElementPlusIcons as Record<string, Component>)[name] ||
202
+ (ElementPlusIcons as Record<string, Component>)[fallback] ||
203
+ ElementPlusIcons.Menu
204
+ )
205
+ }
@@ -0,0 +1,13 @@
1
+ import type { Directive } from 'vue'
2
+ import { useUserStore } from '@/stores/user'
3
+
4
+ /** 按钮级权限指令:v-permission="'system:user:add'" */
5
+ export const permission: Directive<HTMLElement, string> = {
6
+ mounted(el, binding) {
7
+ const store = useUserStore()
8
+ const code = binding.value
9
+ if (!store.hasPermission(code)) {
10
+ el.parentNode?.removeChild(el)
11
+ }
12
+ },
13
+ }
@@ -0,0 +1,22 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_BASE_URL: string
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv
9
+ }
10
+
11
+ declare module '*.vue' {
12
+ import type { DefineComponent } from 'vue'
13
+ const component: DefineComponent<object, object, unknown>
14
+ export default component
15
+ }
16
+
17
+ /** wangEditor Vue3 封装:package exports 未正确暴露类型,在此补声明 */
18
+ declare module '@wangeditor/editor-for-vue' {
19
+ import type { DefineComponent } from 'vue'
20
+ export const Editor: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
21
+ export const Toolbar: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
22
+ }