chore: init sysmgr-admin (Vue3 scaffold)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-10 14:03:19 +08:00
commit 9730b9fd5e
48 changed files with 14973 additions and 0 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

3
.env Normal file
View File

@ -0,0 +1,3 @@
VITE_APP_NAME=demo_default
VITE_NUMBER_DEMO=3000
VITE_BOOLEAN_DEMO=true

1
.env.dev Normal file
View File

@ -0,0 +1 @@
VITE_APP_NAME=demo_dev

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22.23.1

10
.oxlintrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
"env": {
"browser": true
},
"categories": {
"correctness": "error"
}
}

6
.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

9
.vscode/extensions.json vendored Executable file
View File

@ -0,0 +1,9 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}

67
README.md Normal file
View File

@ -0,0 +1,67 @@
# sysmgr-admin Vue3 全家桶通用模板项目
- 程序员优雅哥 youyacoder agarwood 沉香系列。
- 整合了 TypeScript、Vue Router、Pinia、UnoCSS、SCSS、ESLint、Prettier、unplugin-vue-router、vite-plugin-vue-layouts、unplugin-auto-import、unplugin-vue-components、vueuse、vue-i18n。
- 支持多环境配置:dev、uat、prod。
- 最简代码模版,开箱即用,支持自动路由、全局布局、自动导入、组件自动注册。
## 分支说明
- main: 最简模板代码
- axios:集成 Axios,封装了网络请求
## 基于此模板创建新项目
可以通用 -b 指定要拉取的分支
1)拉取最简模板(main 分支)
```shell
git clone git@github.com:HeroCloudy/agarwood-vue3-app-template.git --depth 1 -b main your-project-name
```
2)拉取集成axios模板(axios 分支)
```shell
git clone git@github.com:HeroCloudy/agarwood-vue3-app-template.git --depth 1 -b axios your-project-name
```
3)拉取集成 Element Plus 的模板(element-plus 分支)
git clone git@github.com:HeroCloudy/wumeng-vue3-app-template.git --depth 1 -b element-plus your-project-name
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm build
```
### Lint with [ESLint](https://eslint.org/)
```sh
pnpm lint
```

318
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,318 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createDisposableDirective: typeof import('@vueuse/core').createDisposableDirective
const createEventHook: typeof import('@vueuse/core').createEventHook
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createPinia: typeof import('pinia').createPinia
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
const extendRef: typeof import('@vueuse/core').extendRef
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDefined: typeof import('@vueuse/core').isDefined
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onClickOutside: typeof import('@vueuse/core').onClickOutside
const onDeactivated: typeof import('vue').onDeactivated
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
const onLongPress: typeof import('@vueuse/core').onLongPress
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAnimate: typeof import('@vueuse/core').useAnimate
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useCountdown: typeof import('@vueuse/core').useCountdown
const useCounter: typeof import('@vueuse/core').useCounter
const useCssModule: typeof import('vue').useCssModule
const useCssSupports: typeof import('@vueuse/core').useCssSupports
const useCssVar: typeof import('@vueuse/core').useCssVar
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDropZone: typeof import('@vueuse/core').useDropZone
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useRafFn: typeof import('@vueuse/core').useRafFn
const useRefHistory: typeof import('@vueuse/core').useRefHistory
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
const useScriptTag: typeof import('@vueuse/core').useScriptTag
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
const useTimeout: typeof import('@vueuse/core').useTimeout
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

26
components.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Icon: typeof import('./src/components/icon/icon.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
// For TSX support
declare global {
const Icon: typeof import('./src/components/icon/icon.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
}

23
env.d.ts vendored Executable file
View File

@ -0,0 +1,23 @@
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
/// <reference types="vite-plugin-vue-layouts/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
readonly VITE_NUMBER_DEMO: number
readonly VITE_BOOLEAN_DEMO: boolean
}
declare module 'virtual:svg-icons-register' {
const component: any
export default component
}
declare module 'virtual:svg-icons-names' {
const iconsNames: string[]
export default iconsNames
}

33
eslint.config.ts Normal file
View File

@ -0,0 +1,33 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from 'eslint-config-prettier/flat'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{vue,ts,mts,tsx}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
skipFormatting,
{
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

75
package.json Normal file
View File

@ -0,0 +1,75 @@
{
"name": "sysmgr-admin",
"version": "0.0.0",
"packageManager": "pnpm@12.10.1",
"description": "程序员优雅哥 agarwood 系列 vue3 模板项目",
"private": true,
"type": "module",
"scripts": {
"dev:dev": "vite --mode dev",
"dev:uat": "vite --mode uat",
"build:dev": "run-p type-check \"build-only {@} --mode dev\" --",
"build:uat": "run-p type-check \"build-only {@} --mode uat\" --",
"build:prod": "run-p type-check \"build-only {@} --mode prod\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"@iconify/vue": "^5.0.1",
"@vueuse/core": "^14.3.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.38",
"vue-i18n": "^11.4.6",
"vue-router": "^5.0.6"
},
"devDependencies": {
"@iconify/json": "^2.2.487",
"@intlify/unplugin-vue-i18n": "^11.2.4",
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.60.0",
"eslint-plugin-vue": "~10.8.0",
"jiti": "^2.6.1",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.60.0",
"prettier": "3.8.4",
"rollup-plugin-visualizer": "^7.0.1",
"sass": "^1.101.0",
"terser": "^5.48.0",
"typescript": "~6.0.0",
"unocss": "^66.7.2",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0",
"unplugin-vue-router": "^0.19.2",
"vite": "^8.0.16",
"vite-plugin-compression2": "^2.5.3",
"vite-plugin-image-optimizer": "^2.0.3",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^8.1.3",
"vite-plugin-vue-layouts": "^0.11.0",
"vue-tsc": "^3.2.8"
},
"engines": {
"node": "^22.23.1"
},
"keywords": [
"vue3"
],
"author": {
"name": "程序员优雅哥 youyacoder",
"email": "hero_cloudy@126.com"
}
}

8377
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
allowBuilds:
'@parcel/watcher': false
core-js: false
esbuild: false

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

5
src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<router-view />
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="M329.6 382.933333C323.2 389.333333 320 395.733333 320 405.333333s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6c6.4-6.4 9.6-16 9.6-22.4s-3.2-16-9.6-22.4c-12.8-12.8-32-12.8-44.8 0zM649.6 382.933333C643.2 389.333333 640 395.733333 640 405.333333s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6c6.4-6.4 9.6-16 9.6-22.4s-3.2-16-9.6-22.4c-12.8-12.8-32-12.8-44.8 0z"/>
<path d="M512 938.666667c-235.264 0-426.666667-191.402667-426.666667-426.666667s191.402667-426.666667 426.666667-426.666667 426.666667 191.402667 426.666667 426.666667-191.402667 426.666667-426.666667 426.666667z m0-789.333334C312.021333 149.333333 149.333333 312.021333 149.333333 512c0 199.978667 162.688 362.666667 362.666667 362.666667 199.978667 0 362.666667-162.688 362.666667-362.666667 0-199.978667-162.688-362.666667-362.666667-362.666667z"/>
<path d="M512 768c-99.989333 0-181.333333-81.344-181.333333-181.333333a32 32 0 1 1 64 0c0 64.693333 52.64 117.333333 117.333333 117.333333s117.333333-52.64 117.333333-117.333333a32 32 0 1 1 64 0c0 99.989333-81.344 181.333333-181.333333 181.333333z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
body {
margin: 0;
background-color: var(--wm-bg-color-page);
color: var(--wm-color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

View File

@ -0,0 +1,3 @@
// 导入变量文件
@use "./settings";
@use "./base";

View File

@ -0,0 +1,73 @@
@use "sass:color";
// 品牌与状态色
$wm-color-primary: #1C9399;
$wm-color-success: #3DBE7D;
$wm-color-warning: #FFB74D;
$wm-color-danger: #F1524C;
$wm-color-info: #5E7CE0;
// 中性色 - 文字
$wm-color-text-primary: #303133;
$wm-color-text-regular: #606266;
$wm-color-text-secondary: #909399;
$wm-color-text-placeholder: #A8ABB2;
$wm-color-text-disabled: #C0C4CC;
// 中性色 - 边框
$wm-border-color-base: #DCDFE6;
$wm-border-color-light: #E4E7ED;
// 中性色 - 背景
$wm-bg-color-page: #F5F7FA;
$wm-bg-color-base: #FFFFFF;
// 默认主题 白天模式
:root {
// 品牌/状态色及其色阶
@each $type, $color in (primary, $wm-color-primary),
(success, $wm-color-success),
(danger, $wm-color-danger),
(warning, $wm-color-warning),
(info, $wm-color-info) {
--wm-color-#{$type}: #{$color};
// 生成浅色变体 (混合白色)
@each $i in (1, 2, 3, 5, 7, 8, 9) {
--wm-color-#{$type}-light-#{$i}: #{color.mix(#fff, $color, $i * 10%)};
}
// 生成深色变体 (混合黑色)
@each $i in (1, 2) {
--wm-color-#{$type}-dark-#{$i}: #{color.mix(#000, $color, $i * 10%)};
}
}
// 文字
--wm-color-text-primary: #{$wm-color-text-primary};
--wm-color-text-regular: #{$wm-color-text-regular};
--wm-color-text-secondary: #{$wm-color-text-secondary};
--wm-color-text-placeholder: #{$wm-color-text-placeholder};
// 边框
--wm-border-color-base: #{$wm-border-color-base};
--wm-border-color-light: #{$wm-border-color-light};
// 背景
--wm-bg-color-base: #{$wm-bg-color-base};
--wm-bg-color-page: #{$wm-bg-color-page};
}
// 深色主题覆盖
[data-theme="dark"] {
--wm-color-text-primary: rgba(255, 255, 255, 0.95);
--wm-color-text-regular: rgba(255, 255, 255, 0.75);
--wm-color-text-secondary: rgba(255, 255, 255, 0.55);
--wm-color-text-placeholder: rgba(255, 255, 255, 0.35);
--wm-bg-color-page: #121212;
--wm-bg-color-base: #1a1a1a;
--wm-border-color-base: rgba(255, 255, 255, 0.12);
}

View File

@ -0,0 +1,37 @@
// 间距系统 (以8px为基准)
$wm-spacing-base: 8px;
:root {
--wm-spacing-xs: #{$wm-spacing-base * 0.5}; // 4px
--wm-spacing-sm: #{$wm-spacing-base * 1}; // 8px
--wm-spacing-md: #{$wm-spacing-base * 1.5}; // 12px
--wm-spacing-lg: #{$wm-spacing-base * 2}; // 16px
--wm-spacing-xl: #{$wm-spacing-base * 3}; // 24px
--wm-spacing-2xl: #{$wm-spacing-base * 4}; // 32px
}
// ===== 字体系统 =====
:root {
--wm-font-size-xs: 12px;
--wm-font-size-sm: 14px;
--wm-font-size-base: 16px;
--wm-font-size-lg: 18px;
--wm-font-size-xl: 20px;
--wm-font-size-2xl: 24px;
--wm-font-size-3xl: 30px;
--wm-font-weight-normal: 400;
--wm-font-weight-medium: 500;
--wm-font-weight-semibold: 600;
--wm-line-height-tight: 1.25;
--wm-line-height-normal: 1.5;
}
// ===== 圆角 =====
:root {
--wm-border-radius-sm: 4px;
--wm-border-radius-base: 6px;
--wm-border-radius-lg: 8px;
--wm-border-radius-full: 9999px;
}

View File

@ -0,0 +1,2 @@
@use "./color";
@use "./size";

View File

@ -0,0 +1,75 @@
<!--
@name: icon.vue
@description:
@author: 程序员优雅哥 youyacoder
@time: 2026/2/11 15:45
-->
<template>
<div v-if="type === 'uno'" :class="icon" />
<iconify-icon v-else-if="type === 'iconify'" :icon="icon" class="icon" :aria-hidden="false" />
<svg v-else-if="type === 'svg'" class="svg">
<use :href="svgSymbolId" fill="currentColor" />
</svg>
<i v-else-if="type === 'iconfont'" :class="iconfontClassName" />
<span v-else>Unsupported type: {{ type }}</span>
</template>
<script setup lang="ts">
import { onBeforeMount, computed } from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue'
defineOptions({ name: 'Icon' })
const props = withDefaults(
defineProps<{
/**
* Icon 图标类型:
* - uno:UnoCSS Icon,对应的 icon 属性:https://icones.js.org/
* - iconify: Iconify 图标,对应的 icon 属性: https://icon-sets.iconify.design/
* - svg:本地 SVG 图标
* - iconfont:IconFont 图标
*/
type?: 'uno' | 'svg' | 'iconify' | 'iconfont'
icon: string
prefix?: string
fontFamily?: string
fontUrl?: string
}>(),
{
type: 'iconify',
prefix: 'icon',
fontFamily: 'iconfont',
},
)
onBeforeMount(() => {
if (props.type === 'iconfont' && props.fontUrl) {
const existingLink = document.querySelector(`link[href="${props.fontUrl}"]`)
if (!existingLink) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = props.fontUrl
document.head.appendChild(link)
}
}
})
const svgSymbolId = computed(() => `#${props.prefix}-${props.icon}`)
const iconfontClassName = computed(() => `${props.fontFamily} ${props.prefix}-${props.icon}`)
</script>
<style scoped lang="scss">
.icon {
@apply inline-block align-mid text-xl;
}
.svg {
@apply inline-block align-mid;
width: 1rem;
height: 1rem;
}
</style>

38
src/i18n/index.ts Normal file
View File

@ -0,0 +1,38 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
/**
* 获取浏览器的语言
*/
const getBrowserLanguage = () => {
const browserLang = navigator.language || (navigator as any).userLanguage
return ['zh-CN', 'en-US'].includes(browserLang) ? browserLang : 'zh-CN'
}
const currentLang = localStorage.getItem('locale') || getBrowserLanguage()
// 创建 i18n 实例
const i18n = createI18n({
legacy: false, // 使用 Composition API
locale: currentLang,
fallbackLocale: 'zh-CN', // fallback 语言
messages: {},
})
/**
* 动态加载语言包
*/
export async function loadLanguage(lang: string) {
return import(`./locales/${lang}.ts`).then((messages) => {
i18n.global.setLocaleMessage(lang, messages.default)
localStorage.setItem('locale', lang)
return lang
})
}
loadLanguage(currentLang)
export const installI18n = (app: App) => {
app.use(i18n)
}
export default i18n

View File

@ -0,0 +1,9 @@
export default {
common: {
welcome: 'Welcome, {name}!',
},
home: {
title: 'Welcome to Vue3 Common Template',
description: 'This is a feature-rich, easy-to-extend Vue3 common template project',
},
}

View File

@ -0,0 +1,4 @@
export default {
title: 'Demo Page',
info: 'This is a demo page',
}

View File

@ -0,0 +1,4 @@
export default {
title: '示例页面',
info: '这是一个示例页面',
}

10
src/i18n/locales/zh-CN.ts Normal file
View File

@ -0,0 +1,10 @@
export default {
common: {
hello: '你好',
welcome: '欢迎,{name}!',
},
home: {
title: '欢迎使用 Vue3 通用模板',
description: '这是一个功能完善、易于扩展的 Vue3 通用模板项目',
},
}

23
src/layouts/default.vue Normal file
View File

@ -0,0 +1,23 @@
<!--
@name: default.vue.vue
@description:
@author: 程序员优雅哥 youyacoder
@time: 2026/2/11 16:04
-->
<template>
<div>
<header>
<router-link to="/" class="item">home</router-link>
<router-link to="/demo" class="item">demo</router-link>
</header>
<router-view></router-view>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.item {
@apply line-height-loose mr-3 text-gray-400 mb-5;
}
</style>

27
src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import { createApp } from 'vue'
import App from './App.vue'
import { installRouter } from './router'
import { installStore } from '@/stores'
import { installAssets } from '@/plugins/assets'
import { Env } from '@/utils/env.ts'
import { installI18n } from '@/i18n'
const app = createApp(App)
installRouter(app)
installStore(app)
installI18n(app)
installAssets()
// 获取当前模式
const currentMode = import.meta.env.MODE
console.log('当前模式:', currentMode) // 输出: development, uat 或 production
console.log('VITE_APP_NAME: ', Env.get('VITE_APP_NAME'))
console.log('VITE_NUMBER_DEMO: ', Env.getNumber('VITE_NUMBER_DEMO'))
console.log('VITE_BOOLEAN_DEMO: ', Env.getBoolean('VITE_BOOLEAN_DEMO'))
console.log('isDev', Env.isDev)
console.log('isUat', Env.isUat)
console.log('isProd', Env.isProd)
app.mount('#app')

95
src/pages/demo.vue Normal file
View File

@ -0,0 +1,95 @@
<!--
@name: demo.vue
@description: Demo测试页面
@author: 程序员优雅哥 youyacoder
@time: 2026/2/11 13:59
-->
<template>
<div class="demo">
<div>
<div class="text-2xl text-danger">{{ $t('demo.title') }}</div>
<div>{{ $t('demo.info') }}</div>
</div>
<h2>Pinia 测试</h2>
<h3>计数器: {{ counter }}</h3>
<button @click="demoStore.increment()">点击+1</button>
<div>
<div class="demo-text">Hello world</div>
<div class="text-xs text-primary">测试文字</div>
<div class="text-sm text-success">测试文字</div>
<div class="text-md text-info">测试文字</div>
<div class="text-base text-warning">测试文字</div>
<div class="text-lg text-danger">测试文字</div>
<div class="text-xl text-text-primary">测试文字</div>
<div class="text-2xl text-text-regular">测试文字</div>
<div class="text-3xl text-text-secondary">测试文字</div>
<button class="mt-xl px-md py-sm border rounded-base" @click="toggleTheme">
切换深色/浅色模式
</button>
</div>
<div>
<div>UnoCSS Icon</div>
<!-- A basic anchor icon from Phosphor icons -->
<div class="i-ph-anchor-simple-thin" />
<!-- An orange alarm from Material Design Icons -->
<i class="i-mdi-alarm text-orange-400" />
<!-- A large Vue logo -->
<div class="i-logos-vue text-3xl" />
<!-- Sun in light mode, Moon in dark mode, from Carbon -->
<button class="i-carbon-sun dark:i-carbon-moon" />
<!-- Twemoji of laugh, turns to tear on hovering -->
<div
class="i-twemoji-grinning-face-with-smiling-eyes hover:i-twemoji-face-with-tears-of-joy"
/>
</div>
<div>
<div>SVG 图标</div>
<svg aria-hidden="true" class="svg">
<use href="#icon-demo" fill="red" />
</svg>
</div>
<div>
<div>图标组件测试</div>
<icon type="iconify" icon="mdi:user" class="size-8 text-primary" />
<icon type="uno" icon="i-mdi:user" class="size-8 text-primary" />
<icon type="svg" icon="demo" class="text-primary size-8!" />
<icon
type="iconfont"
font-url="//at.alicdn.com/t/c/font_3457715_h47rhjpli3n.css"
icon="mobile-alt"
class="text-primary text-2xl!"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useDemoStore } from '@/stores/modules/demo'
// 初始化 Store 实例
const demoStore = useDemoStore()
// 使用 storeToRefs 解构状态,保持响应性
const { counter } = storeToRefs(demoStore)
const toggleTheme = () => {
const html = document.documentElement
const currentTheme = html.getAttribute('data-theme')
html.setAttribute('data-theme', currentTheme === 'dark' ? 'light' : 'dark')
}
definePage({
meta: {
locales: ['demo'],
},
})
</script>
<style scoped lang="scss">
.demo-text {
@apply text-xl text-primary bg-success-light-7;
}
</style>

61
src/pages/index.vue Normal file
View File

@ -0,0 +1,61 @@
<!--
@name: index.vue
@description: 首页
@author: 程序员优雅哥 youyacoder
@time: 2026/2/11 15:58
-->
<template>
<div class="flex gap-2">
<button
v-for="lang in languages"
:key="lang.value"
:class="{ active: currentLocale === lang.value }"
@click="onChangeLanguage(lang.value)"
>
{{ lang.label }}
</button>
</div>
<div>{{ $t('common.welcome', { name: '优雅哥' }) }}</div>
<div>{{ $t('home.title') }}</div>
<hr />
<h1 class="index">index page</h1>
<div>{{ appName }}</div>
<div>{{ msg }}</div>
<div>isDark: {{ isDark }}</div>
<button @click="toggleDark()">Toggle</button>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { loadLanguage } from '@/i18n'
const { t, locale } = useI18n()
console.log(t('common.welcome', { name: '优雅哥' }))
const currentLocale = locale
// 支持的语言
const languages = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
]
const onChangeLanguage = async (lang: string) => {
// 加载语言包
await loadLanguage(lang)
// 设置当前语言
locale.value = lang
}
const msg = ref('Hello world')
const isDark = useDark({
selector: 'html',
attribute: 'data-theme',
valueDark: 'dark',
valueLight: 'light',
})
const toggleDark = useToggle(isDark)
const appName = import.meta.env?.VITE_APP_NAME ?? ''
</script>
<style scoped lang="scss"></style>

8
src/plugins/assets.ts Normal file
View File

@ -0,0 +1,8 @@
import '@/assets/scss/index.scss'
import 'virtual:uno.css' // 引入 UnoCSS
import 'virtual:svg-icons-register'
/**
* 空方法,通过调用该方法,导入上面 import 中的样式和资源
*/
export const installAssets = () => {}

56
src/router/index.ts Normal file
View File

@ -0,0 +1,56 @@
import {
createRouter,
createWebHistory,
type RouteLocationNormalized,
type RouteRecordRaw,
} from 'vue-router'
import type { App } from 'vue'
import { routes } from 'vue-router/auto-routes'
import { setupLayouts } from 'virtual:generated-layouts'
import i18n, { loadLanguage } from '@/i18n'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// routes
routes: setupLayouts(routes as RouteRecordRaw[]),
})
async function loadRouteLocales(to: RouteLocationNormalized) {
const currentLang = localStorage.getItem('locale') || 'zh-CN'
// 确保通用语言已加载
await loadLanguage(currentLang)
// 加载路由所需的语言模块
if (to.meta.locales) {
for (const locale of to.meta.locales as string[]) {
try {
await import(`@/i18n/locales/modules/${currentLang}/${locale}.ts`).then((messages) => {
// 合并到当前语言的消息中
i18n.global.mergeLocaleMessage(currentLang, {
[locale]: messages.default,
})
})
} catch (e) {
console.warn(`Failed to load locale module: ${locale}`, e)
}
}
}
}
// 路由前置守卫
router.beforeEach(async (to, from, next) => {
await loadRouteLocales(to)
next()
})
/**
* 安装路由
* @param app Vue 应用实例
* 采用模块化方法注册路由,提高代码可维护性
*/
export const installRouter = (app: App) => {
app.use(router)
}
export default router

21
src/stores/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { App } from 'vue'
// 创建 Pinia 实例
const pinia = createPinia()
// 添加持久化插件
// 该插件会自动将状态保存到 localStorage,默认 key 为 store 名称
pinia.use(piniaPluginPersistedstate)
/**
* 安装 Pinia
* @param app Vue 应用实例
* 采用模块化方法注册 Pinia,与路由注册保持一致的风格
*/
export const installStore = (app: App) => {
app.use(pinia)
}
export default pinia

View File

@ -0,0 +1,28 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
/**
* 测试用的 Pinia Store
* 使用组合式 API 风格编写,与 Vue3 的组合式 API 保持一致
*/
export const useDemoStore = defineStore(
'demoStore', // Store 唯一标识
() => {
// 状态定义(类似 Vue 的 ref)
const counter = ref<number>(0)
// Action 定义(修改状态的方法)
const increment = () => {
counter.value++
}
// 返回需要暴露的状态和方法
return {
counter,
increment,
}
},
{
persist: true, // 开启状态持久化
},
)

53
src/utils/env.ts Normal file
View File

@ -0,0 +1,53 @@
export class Env {
/**
* 获取环境变量
* @param key 环境变量名
* @param defaultValue 默认值
*/
static get<T>(key: keyof ImportMetaEnv, defaultValue?: T): T | string {
const value = import.meta.env[key]
return value ?? (defaultValue as T)
}
/**
* 获取数字类型的环境变量
*/
static getNumber(key: keyof ImportMetaEnv, defaultValue?: number): number {
const value = import.meta.env[key]
if (value === undefined) {
return defaultValue as number
}
return Number(value)
}
/**
* 获取布尔类型的环境变量
*/
static getBoolean(key: keyof ImportMetaEnv, defaultValue?: boolean): boolean {
const value = import.meta.env[key]
if (value === undefined) {
return defaultValue as boolean
}
return value === 'true' || value === '1'
}
/** 获取当前环境 */
static get env(): 'dev' | 'uat' | 'prod' {
return this.get('VITE_ENV', 'dev') as 'dev' | 'uat' | 'prod'
}
/** 是否为开发环境 */
static get isDev(): boolean {
return this.env === 'dev'
}
/** 是否为UAT环境 */
static get isUat(): boolean {
return this.env === 'uat'
}
/** 是否为生产环境 */
static get isProd(): boolean {
return this.env === 'prod'
}
}

4950
stats.html Normal file

File diff suppressed because one or more lines are too long

19
tsconfig.app.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue",
"typed-router.d.ts",
"auto-imports.d.ts",
"components.d.ts"
],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "preserve",
"moduleResolution": "bundler",
"types": ["node"]
}
}

77
typed-router.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection ES6UnusedImports
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-resolver' {
export type ParamParserCustom = never
}
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<
'/',
'/',
Record<never, never>,
Record<never, never>,
| never
>,
'/demo': RouteRecordInfo<
'/demo',
'/demo',
Record<never, never>,
Record<never, never>,
| never
>,
}
/**
* Route file to route info map by unplugin-vue-router.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* Each key is a file path relative to the project root with 2 properties:
* - routes: union of route names of the possible routes when in this page (passed to useRoute<...>())
* - views: names of nested views (can be passed to <RouterView name="...">)
*
* @internal
*/
export interface _RouteFileInfoMap {
'src/pages/index.vue': {
routes:
| '/'
views:
| never
}
'src/pages/demo.vue': {
routes:
| '/demo'
views:
| never
}
}
/**
* Get a union of possible route names in a certain route component file.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* @internal
*/
export type _RouteNamesForFilePath<FilePath extends string> =
_RouteFileInfoMap extends Record<FilePath, infer Info>
? Info['routes']
: keyof RouteNamedMap
}

86
uno.config.ts Normal file
View File

@ -0,0 +1,86 @@
import {
defineConfig,
presetWind4,
transformerDirectives,
transformerVariantGroup,
presetIcons,
} from 'unocss'
export default defineConfig({
presets: [
presetWind4({
preflights: { reset: true },
}),
presetIcons({
prefix: 'i-',
extraProperties: {
display: 'inline-block',
'vertical-align': 'middle',
},
}),
],
transformers: [
// 启用 @apply 指令支持
transformerDirectives(),
// 启用 `hover:(bg-gray-400 text-white)` 类写法
transformerVariantGroup(),
],
theme: {
colors: {
// 品牌与状态色
primary: 'var(--wm-color-primary)',
success: 'var(--wm-color-success)',
warning: 'var(--wm-color-warning)',
danger: 'var(--wm-color-danger)',
info: 'var(--wm-color-info)',
// 文字色
'text-primary': 'var(--wm-color-text-primary)',
'text-regular': 'var(--wm-color-text-regular)',
'text-secondary': 'var(--wm-color-text-secondary)',
// 背景色
'bg-page': 'var(--wm-bg-color-page)',
'bg-base': 'var(--wm-bg-color-base)',
},
spacing: {
xs: 'var(--wm-spacing-xs)',
sm: 'var(--wm-spacing-sm)',
md: 'var(--wm-spacing-md)',
lg: 'var(--wm-spacing-lg)',
xl: 'var(--wm-spacing-xl)',
'2xl': 'var(--wm-spacing-2xl)',
},
fontSize: {
xs: 'var(--wm-font-size-xs)',
sm: 'var(--wm-font-size-sm)',
base: 'var(--wm-font-size-base)',
lg: 'var(--wm-font-size-lg)',
xl: 'var(--wm-font-size-xl)',
'2xl': 'var(--wm-font-size-2xl)',
'3xl': 'var(--wm-font-size-3xl)',
},
borderRadius: {
sm: 'var(--wm-border-radius-sm)',
base: 'var(--wm-border-radius-base)',
lg: 'var(--wm-border-radius-lg)',
full: 'var(--wm-border-radius-full)',
},
},
rules: [
...['primary', 'success', 'warning', 'danger', 'info'].flatMap((color) => [
[`bg-${color}-light-3`, { 'background-color': `var(--wm-color-${color}-light-3)` }],
[`bg-${color}-light-5`, { 'background-color': `var(--wm-color-${color}-light-5)` }],
[`bg-${color}-light-7`, { 'background-color': `var(--wm-color-${color}-light-7)` }],
[`bg-${color}-light-9`, { 'background-color': `var(--wm-color-${color}-light-9)` }],
[`text-${color}-dark-2`, { color: `var(--wm-color-${color}-dark-2)` }],
]),
],
shortcuts: {
// 布局
'flex-center': 'flex justify-center items-center',
'flex-col-center': 'flex flex-col justify-center items-center',
},
})

151
vite.config.ts Normal file
View File

@ -0,0 +1,151 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import UnoCSS from 'unocss/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import VueRouter from 'unplugin-vue-router/vite'
import Layouts from 'vite-plugin-vue-layouts'
import AutoImport from 'unplugin-auto-import/vite'
import { VueRouterAutoImports } from 'unplugin-vue-router'
import Components from 'unplugin-vue-components/vite'
import vueI18n from '@intlify/unplugin-vue-i18n/vite'
import path from 'node:path'
import { compression } from 'vite-plugin-compression2'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import legacy from '@vitejs/plugin-legacy'
import { visualizer } from 'rollup-plugin-visualizer'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
// 如果你有其他场景判断,可修改此处,本文假设只针对生产环境
const isProd = mode === 'prod'
return {
build: {
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
reportCompressedSize: true,
minify: 'terser',
cssCodeSplit: true,
sourcemap: !isProd,
terserOptions: isProd
? {
// 代码压缩配置
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
},
// 代码混淆配置
mangle: {
toplevel: true, // 混淆顶层变量名
eval: true, // 混淆 eval 中的变量
},
// 输出配置
format: {
comments: false,
},
}
: {},
olldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vue',
test: /node_modules[\\/]vue/,
priority: 20,
},
{
name: 'vue-router',
test: /node_modules[\\/]vue-router/,
priority: 15,
},
{
name: 'pinia',
test: /node_modules[\\/]pinia/,
priority: 10,
},
],
},
},
},
},
plugins: [
VueRouter({}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
vueJsx(),
UnoCSS(), // 配置 SVG 图标插件
createSvgIconsPlugin({
// SVG 图标目录
iconDirs: [fileURLToPath(new URL('./src/assets/icons', import.meta.url))],
// 生成的 symbol ID 格式
symbolId: 'icon-[dir]-[name]',
}),
Layouts({
layoutsDirs: 'src/layouts',
defaultLayout: 'default',
}),
AutoImport({
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/, // .md
],
imports: ['vue', VueRouterAutoImports, 'pinia', '@vueuse/core'],
}),
Components({
deep: true,
directoryAsNamespace: false,
}),
vueI18n({
// 语言包目录
include: path.resolve(__dirname, './src/i18n/locales/**'),
// 开发模式下也启动编译时处理
runtimeOnly: false,
// 仅使用组合式 API
compositionOnly: true,
// 完整安装
fullInstall: true,
}),
// Gzip 压缩
compression({
algorithms: ['gzip'],
threshold: 10240, // 超过 10KB 的文件才压缩
deleteOriginalAssets: false, // 不删除原文件
}),
ViteImageOptimizer({
png: {
quality: 80,
},
jpeg: {
quality: 80,
},
webp: {
quality: 80,
},
}),
legacy({
targets: ['defaults', 'not IE 11'],
}),
visualizer({
filename: 'stats.html',
}),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
},
}
})