+RpJ_jrtzMOL>d9=R=e|^>j7!DsICt-9-`T7J*
zir8PFB27N=wd!erEoY8p+d
zqH|K6F0_!)OUu*Yu@s;EnbQRHP1(Zo)F+=TLgYhCYbsAx_0B%nqPxbh#BY;vGHirv>j2WG7ca__?5KTfa
z|L^GU{l9EBt)3w7?1Hy8^C5A+YkEU5}
z%NCY{o6p{DHpcbjf_}6>w~(x#Ydybe-7siol8PKaj$`EJbf8-vKe3w!|6%(y>{1%c
zy4~jYb`Wmwx--D?X1*Y<*+!3(+MsMnP?CU-llaN6=~xw~yZPGkeWMI=T_83B%!mp1lTvYx
zxL(Jk#rEHw|MvQYo3h1a(IPtA9|-XBoVG-2P_dKu^)~`16G=^boJT-FR-&b!Bg>>c
z?%K9$50u~izq_Cfm~2=V)caQkw%g#KBOkYN=q_b&7YDz|Dpw2E#_}Dob~>J4!8IFa
zkSqVni#$X$~qn0I{4qeeke~sxwrSAjgP$#
zahk$YNWeb5t;V;n`~*-#hlvg#&p)%E7JX^m#bF(gV<8LrL1C(RC5F!$dOKEPAi;
z9Vo?QuthjByL@fh^+$j2KaB$9%=YKJ@~0PWPS#YF+TAb=GYhr9l(CCWcfo)Lrm!bM
zDOh4f8Wbay8w2yiF$c;&{C8iw()x348lAH`XxbB6qYQMV$z_Lykfas2&fl_o7hXEJ
zwOw(K&nmzB|1>l*>r1Uu?=y<%pA;rd9iugi@;xOcgA>T1)b
z`<6y;;_+lCC?e*VMR<~PsH-?;wD?N1l5Um1fe<&8tTxOe~l
z%gu}PYJ2qaCcH^;n5po;VFO~b)EbL_{5Op;)@5Oo76mzM-EBl5`w5wdAp`?Q@GG?J
z-4+16Y4*uG2mj%8iTImP{?EVrpD&qAO_0cCJ`OWoiBT#<78jYcfc9rIM%F2hsdVTi
za%pe2Z<D<@7~EOxFw^6A
+import { useClipboard } from '@vueuse/core'
+import Message from 'vue-m-message'
+import settingsDefault from '@/settings.default'
+import { getTwoObjectDiff } from '@/utils'
+import eventBus from '@/utils/eventBus'
+import useSettingsStore from '@/store/modules/settings'
+
+defineOptions({
+ name: 'AppSetting',
+})
+
+const settingsStore = useSettingsStore()
+
+const isShow = ref(false)
+
+onMounted(() => {
+ eventBus.on('global-app-setting-toggle', () => {
+ isShow.value = !isShow.value
+ })
+})
+
+const { copy, copied, isSupported } = useClipboard()
+
+watch(copied, (val) => {
+ if (val) {
+ Message.success('复制成功,请粘贴到 src/settings.ts 文件中!', {
+ zIndex: 2000,
+ })
+ }
+})
+
+function handleCopy() {
+ copy(JSON.stringify(getTwoObjectDiff(settingsDefault, settingsStore.settings), null, 2))
+}
+
+
+
+
+
+
+ 应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
+
+
+ 注意:在生产环境中应关闭该模块。
+
+
+
+
+
+
+
+
+ 复制配置
+
+
+
+
diff --git a/src/components/Auth/index.vue b/src/components/Auth/index.vue
new file mode 100755
index 0000000..a25fe08
--- /dev/null
+++ b/src/components/Auth/index.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/AuthAll/index.vue b/src/components/AuthAll/index.vue
new file mode 100755
index 0000000..3c0f36e
--- /dev/null
+++ b/src/components/AuthAll/index.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/NotAllowed/index.vue b/src/components/NotAllowed/index.vue
new file mode 100755
index 0000000..a156cad
--- /dev/null
+++ b/src/components/NotAllowed/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ 403
+
+
+ 抱歉,你无权访问该页面
+
+
+
+ {{ data.countdown }} 秒后,返回首页
+
+
+
+
+
diff --git a/src/components/PageLayout/index.vue b/src/components/PageLayout/index.vue
new file mode 100644
index 0000000..013bf15
--- /dev/null
+++ b/src/components/PageLayout/index.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+ {{ settingsStore.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/PageMain/index.vue b/src/components/PageMain/index.vue
new file mode 100644
index 0000000..51af1e9
--- /dev/null
+++ b/src/components/PageMain/index.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue
new file mode 100755
index 0000000..5bc0879
--- /dev/null
+++ b/src/components/SvgIcon/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Trend/index.vue b/src/components/Trend/index.vue
new file mode 100755
index 0000000..2afffc2
--- /dev/null
+++ b/src/components/Trend/index.vue
@@ -0,0 +1,38 @@
+
+
+
+
+ {{ prefix }}
+ {{ value }}
+ {{ suffix }}
+
+
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100755
index 0000000..44cf130
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,32 @@
+import './utils/system.copyright'
+
+import Message from 'vue-m-message'
+import 'vue-m-message/dist/style.css'
+
+import 'overlayscrollbars/overlayscrollbars.css'
+
+import App from './App.vue'
+import pinia from './store'
+import router from './router'
+import ui from './ui-provider'
+
+// 自定义指令
+import directive from '@/utils/directive'
+
+// 加载 svg 图标
+import 'virtual:svg-icons-register'
+
+import 'virtual:uno.css'
+
+// 全局样式
+import '@/assets/styles/globals.scss'
+
+const app = createApp(App)
+
+app.use(Message)
+app.use(pinia)
+app.use(router)
+app.use(ui)
+directive(app)
+
+app.mount('#app')
diff --git a/src/mock/user.ts b/src/mock/user.ts
new file mode 100755
index 0000000..eb307b0
--- /dev/null
+++ b/src/mock/user.ts
@@ -0,0 +1,47 @@
+import { defineFakeRoute } from 'vite-plugin-fake-server/client'
+import Mock from 'mockjs'
+
+export default defineFakeRoute([
+ {
+ url: '/mock/user/login',
+ method: 'post',
+ response: ({ body }) => {
+ return {
+ error: '',
+ status: 1,
+ data: Mock.mock({
+ account: body.account,
+ token: `${body.account}_@string`,
+ avatar: 'https://fantastic-mobile.github.io/logo.png',
+ }),
+ }
+ },
+ },
+ {
+ url: '/mock/user/permission',
+ method: 'get',
+ response: ({ headers }) => {
+ let permissions: string[] = []
+ if (headers.token?.indexOf('admin') === 0) {
+ permissions = [
+ 'permission.browse',
+ 'permission.create',
+ 'permission.edit',
+ 'permission.remove',
+ ]
+ }
+ else if (headers.token?.indexOf('test') === 0) {
+ permissions = [
+ 'permission.browse',
+ ]
+ }
+ return {
+ error: '',
+ status: 1,
+ data: {
+ permissions,
+ },
+ }
+ },
+ },
+])
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100755
index 0000000..8474bcc
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,123 @@
+import type { RouteRecordRaw } from 'vue-router/auto'
+import { createRouter, createWebHashHistory } from 'vue-router/auto'
+import { routes } from 'vue-router/auto-routes'
+import path from 'path-browserify'
+import { useNProgress } from '@vueuse/integrations/useNProgress'
+import '@/assets/styles/nprogress.scss'
+import useSettingsStore from '@/store/modules/settings'
+import useUserStore from '@/store/modules/user'
+import useKeepAliveStore from '@/store/modules/keepAlive'
+
+const { isLoading } = useNProgress(null, {
+ showSpinner: false,
+ parent: '#app',
+})
+
+function resolveRoutePath(basePath?: string, routePath?: string) {
+ return basePath ? path.resolve(basePath, routePath ?? '') : routePath ?? ''
+}
+
+// 将多层嵌套路由处理成一级
+function flatRoutesRecursive(routes: RouteRecordRaw[], baseUrl = '') {
+ const result: RouteRecordRaw[] = []
+ for (const route of routes) {
+ if (route.children) {
+ result.push(...flatRoutesRecursive(route.children, resolveRoutePath(baseUrl, route.path)))
+ }
+ else {
+ result.push({
+ ...route,
+ path: resolveRoutePath(baseUrl, route.path),
+ })
+ }
+ }
+ return result
+}
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: flatRoutesRecursive(routes),
+})
+
+router.beforeEach(async (to, from, next) => {
+ const settingsStore = useSettingsStore()
+ const userStore = useUserStore()
+ settingsStore.settings.app.enableProgress && (isLoading.value = true)
+ if (to.meta.auth) {
+ if (userStore.isLogin) {
+ // 获取用户权限
+ if (settingsStore.settings.app.enablePermission) {
+ !userStore.isGetPermissions && await userStore.getPermissions()
+ }
+ next()
+ }
+ else {
+ next({
+ name: 'login',
+ query: {
+ redirect: to.fullPath,
+ },
+ })
+ }
+ }
+ else {
+ next()
+ }
+})
+
+router.afterEach((to, from) => {
+ const settingsStore = useSettingsStore()
+ settingsStore.settings.app.enableProgress && (isLoading.value = false)
+ settingsStore.setTitle(to.meta.title ?? '')
+ const keepAliveStore = useKeepAliveStore()
+ if (to.fullPath !== from.fullPath) {
+ // 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
+ if (to.meta.cache) {
+ const componentName = to.matched.at(-1)?.components?.default.name
+ if (componentName) {
+ keepAliveStore.add(componentName)
+ }
+ else {
+ // turbo-console-disable-next-line
+ console.warn('[Fantastic-mobile] 该页面组件未设置组件名,会导致缓存失效,请检查')
+ }
+ }
+ // 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
+ if (from.meta.cache) {
+ const componentName = from.matched.at(-1)?.components?.default.name
+ if (componentName) {
+ // 通过 meta.cache 判断针对哪些页面进行缓存
+ switch (typeof from.meta.cache) {
+ case 'string':
+ if (from.meta.cache !== to.name) {
+ keepAliveStore.remove(componentName)
+ }
+ break
+ case 'object':
+ if (!from.meta.cache.includes(to.name)) {
+ keepAliveStore.remove(componentName)
+ }
+ break
+ }
+ // 通过 meta.noCache 判断针对哪些页面不需要进行缓存
+ if (from.meta.noCache) {
+ switch (typeof from.meta.noCache) {
+ case 'string':
+ if (from.meta.noCache === to.name) {
+ keepAliveStore.remove(componentName)
+ }
+ break
+ case 'object':
+ if (from.meta.noCache.includes(to.name)) {
+ keepAliveStore.remove(componentName)
+ }
+ break
+ }
+ }
+ }
+ }
+ }
+ document.documentElement.scrollTop = 0
+})
+
+export default router
diff --git a/src/settings.default.ts b/src/settings.default.ts
new file mode 100644
index 0000000..f6066de
--- /dev/null
+++ b/src/settings.default.ts
@@ -0,0 +1,27 @@
+// 该文件为系统默认配置,请勿修改!!!
+
+const globalSettingsDefault: RecursiveRequired = {
+ app: {
+ colorScheme: 'light',
+ enablePermission: false,
+ enableProgress: true,
+ enableDynamicTitle: false,
+ enableBackTop: true,
+ },
+ navbar: {
+ enable: false,
+ },
+ tabbar: {
+ enable: false,
+ list: [],
+ },
+ copyright: {
+ enable: false,
+ dates: '',
+ company: '',
+ website: '',
+ beian: '',
+ },
+}
+
+export default globalSettingsDefault
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..65d28b7
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,38 @@
+import { defaultsDeep } from 'lodash-es'
+import settingsDefault from '@/settings.default'
+
+const globalSettings: Settings.all = {
+ app: {
+ enablePermission: true,
+ enableDynamicTitle: true,
+ },
+ tabbar: {
+ list: [
+ {
+ path: '/feature',
+ icon: 'i-ic:sharp-auto-awesome',
+ activeIcon: 'i-ic:twotone-auto-awesome',
+ text: '特色',
+ },
+ {
+ path: '/',
+ icon: 'i-ic:sharp-home',
+ activeIcon: 'i-ic:twotone-home',
+ text: '主页',
+ },
+ {
+ path: '/user',
+ icon: 'i-ic:baseline-person',
+ activeIcon: 'i-ic:twotone-person',
+ text: '我的',
+ },
+ ],
+ },
+ copyright: {
+ dates: '2024-present',
+ company: 'Fantastic-mobile',
+ website: 'https://fantastic-mobile.github.io',
+ },
+}
+
+export default defaultsDeep(globalSettings, settingsDefault) as RecursiveRequired
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100755
index 0000000..a6e3624
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,3 @@
+const pinia = createPinia()
+
+export default pinia
diff --git a/src/store/modules/keepAlive.ts b/src/store/modules/keepAlive.ts
new file mode 100755
index 0000000..444feb9
--- /dev/null
+++ b/src/store/modules/keepAlive.ts
@@ -0,0 +1,42 @@
+const useKeepAliveStore = defineStore(
+ // 唯一ID
+ 'keepAlive',
+ () => {
+ const list = ref([])
+
+ function add(name: string | string[]) {
+ if (typeof name === 'string') {
+ !list.value.includes(name) && list.value.push(name)
+ }
+ else {
+ name.forEach((v) => {
+ v && !list.value.includes(v) && list.value.push(v)
+ })
+ }
+ }
+ function remove(name: string | string[]) {
+ if (typeof name === 'string') {
+ list.value = list.value.filter((v) => {
+ return v !== name
+ })
+ }
+ else {
+ list.value = list.value.filter((v) => {
+ return !name.includes(v)
+ })
+ }
+ }
+ function clean() {
+ list.value = []
+ }
+
+ return {
+ list,
+ add,
+ remove,
+ clean,
+ }
+ },
+)
+
+export default useKeepAliveStore
diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts
new file mode 100755
index 0000000..a2af73b
--- /dev/null
+++ b/src/store/modules/settings.ts
@@ -0,0 +1,55 @@
+import settingsDefault from '@/settings'
+
+const useSettingsStore = defineStore(
+ // 唯一ID
+ 'settings',
+ () => {
+ const settings = ref(settingsDefault)
+
+ const prefersColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
+ const currentColorScheme = ref>()
+ watch(() => settings.value.app.colorScheme, (val) => {
+ if (val === '') {
+ prefersColorScheme.addEventListener('change', updateTheme)
+ }
+ else {
+ prefersColorScheme.removeEventListener('change', updateTheme)
+ }
+ }, {
+ immediate: true,
+ })
+ watch(() => settings.value.app.colorScheme, updateTheme, {
+ immediate: true,
+ })
+ function updateTheme() {
+ let colorScheme = settings.value.app.colorScheme
+ if (colorScheme === '') {
+ colorScheme = prefersColorScheme.matches ? 'dark' : 'light'
+ }
+ currentColorScheme.value = colorScheme
+ switch (colorScheme) {
+ case 'light':
+ document.documentElement.classList.remove('dark')
+ break
+ case 'dark':
+ document.documentElement.classList.add('dark')
+ break
+ }
+ }
+
+ const title = ref('')
+ // 设置网页标题
+ function setTitle(val: string) {
+ title.value = val
+ }
+
+ return {
+ settings,
+ currentColorScheme,
+ title,
+ setTitle,
+ }
+ },
+)
+
+export default useSettingsStore
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
new file mode 100755
index 0000000..e418547
--- /dev/null
+++ b/src/store/modules/user.ts
@@ -0,0 +1,70 @@
+import apiUser from '@/api/modules/user'
+import router from '@/router'
+
+const useUserStore = defineStore(
+ // 唯一ID
+ 'user',
+ () => {
+ const account = ref(localStorage.account ?? '')
+ const token = ref(localStorage.token ?? '')
+ const avatar = ref(localStorage.avatar ?? '')
+ const isGetPermissions = ref(false)
+ const permissions = ref([])
+
+ const isLogin = computed(() => {
+ if (token.value) {
+ return true
+ }
+ return false
+ })
+
+ function login(data: {
+ account: string
+ password: string
+ }) {
+ return new Promise((resolve, reject) => {
+ apiUser.login(data).then((res) => {
+ localStorage.setItem('account', res.data.account)
+ localStorage.setItem('token', res.data.token)
+ localStorage.setItem('avatar', res.data.avatar)
+ account.value = res.data.account
+ token.value = res.data.token
+ avatar.value = res.data.avatar
+ resolve(res)
+ }).catch((error) => {
+ reject(error)
+ })
+ })
+ }
+ function logout() {
+ // 模拟退出登录,清除 token 信息
+ localStorage.removeItem('account')
+ localStorage.removeItem('token')
+ localStorage.removeItem('avatar')
+ account.value = ''
+ token.value = ''
+ avatar.value = ''
+ router.push('/')
+ }
+ // 获取权限
+ async function getPermissions() {
+ const res = await apiUser.permission()
+ permissions.value = res.data.permissions
+ isGetPermissions.value = true
+ }
+
+ return {
+ account,
+ token,
+ avatar,
+ isLogin,
+ isGetPermissions,
+ permissions,
+ login,
+ logout,
+ getPermissions,
+ }
+ },
+)
+
+export default useUserStore
diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts
new file mode 100755
index 0000000..52f8319
--- /dev/null
+++ b/src/types/auto-imports.d.ts
@@ -0,0 +1,87 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+export {}
+declare global {
+ const EffectScope: typeof import('vue')['EffectScope']
+ const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
+ const computed: typeof import('vue')['computed']
+ const createApp: typeof import('vue')['createApp']
+ const createPinia: typeof import('pinia')['createPinia']
+ const customRef: typeof import('vue')['customRef']
+ const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+ const defineComponent: typeof import('vue')['defineComponent']
+ const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
+ const defineStore: typeof import('pinia')['defineStore']
+ const effectScope: typeof import('vue')['effectScope']
+ const getActivePinia: typeof import('pinia')['getActivePinia']
+ const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+ const getCurrentScope: typeof import('vue')['getCurrentScope']
+ const h: typeof import('vue')['h']
+ const inject: typeof import('vue')['inject']
+ 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 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/auto')['onBeforeRouteLeave']
+ const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
+ const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+ const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+ const onDeactivated: typeof import('vue')['onDeactivated']
+ const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+ 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 onUnmounted: typeof import('vue')['onUnmounted']
+ const onUpdated: typeof import('vue')['onUpdated']
+ const provide: typeof import('vue')['provide']
+ const reactive: typeof import('vue')['reactive']
+ const readonly: typeof import('vue')['readonly']
+ const ref: typeof import('vue')['ref']
+ const resolveComponent: typeof import('vue')['resolveComponent']
+ 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 toRaw: typeof import('vue')['toRaw']
+ 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 unref: typeof import('vue')['unref']
+ const useAttrs: typeof import('vue')['useAttrs']
+ const useAuth: typeof import('../utils/composables/useAuth')['default']
+ const useCssModule: typeof import('vue')['useCssModule']
+ const useCssVars: typeof import('vue')['useCssVars']
+ const useGlobalProperties: typeof import('../utils/composables/useGlobalProperties')['default']
+ const useLink: typeof import('vue-router/auto')['useLink']
+ const usePage: typeof import('../utils/composables/usePage')['default']
+ const useRoute: typeof import('vue-router/auto')['useRoute']
+ const useRouter: typeof import('vue-router/auto')['useRouter']
+ const useSlots: typeof import('vue')['useSlots']
+ const watch: typeof import('vue')['watch']
+ const watchEffect: typeof import('vue')['watchEffect']
+ const watchPostEffect: typeof import('vue')['watchPostEffect']
+ const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+}
+// for type re-export
+declare global {
+ // @ts-ignore
+ export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+ import('vue')
+}
diff --git a/src/types/components.d.ts b/src/types/components.d.ts
new file mode 100644
index 0000000..c0b787d
--- /dev/null
+++ b/src/types/components.d.ts
@@ -0,0 +1,27 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+ export interface GlobalComponents {
+ AppSetting: typeof import('./../components/AppSetting/index.vue')['default']
+ Auth: typeof import('./../components/Auth/index.vue')['default']
+ AuthAll: typeof import('./../components/AuthAll/index.vue')['default']
+ HBadge: typeof import('./../ui-kit/HBadge.vue')['default']
+ HButton: typeof import('./../ui-kit/HButton.vue')['default']
+ HInput: typeof import('./../ui-kit/HInput.vue')['default']
+ HSlideover: typeof import('./../ui-kit/HSlideover.vue')['default']
+ HTabList: typeof import('./../ui-kit/HTabList.vue')['default']
+ HToggle: typeof import('./../ui-kit/HToggle.vue')['default']
+ NotAllowed: typeof import('./../components/NotAllowed/index.vue')['default']
+ PageLayout: typeof import('./../components/PageLayout/index.vue')['default']
+ PageMain: typeof import('./../components/PageMain/index.vue')['default']
+ RouterLink: typeof import('vue-router')['RouterLink']
+ RouterView: typeof import('vue-router')['RouterView']
+ SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
+ Trend: typeof import('./../components/Trend/index.vue')['default']
+ }
+}
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
new file mode 100644
index 0000000..30e3cac
--- /dev/null
+++ b/src/types/global.d.ts
@@ -0,0 +1,98 @@
+type RecursiveRequired = {
+ [P in keyof T]-?: RecursiveRequired
+}
+type RecursivePartial = {
+ [P in keyof T]?: RecursivePartial
+}
+
+declare namespace Settings {
+ interface app {
+ /**
+ * 颜色方案
+ * @默认值 `''` 跟随系统
+ * @可选值 `'light'` 明亮模式
+ * @可选值 `'dark'` 暗黑模式
+ */
+ colorScheme?: '' | 'light' | 'dark'
+ /**
+ * 是否开启权限功能
+ * @默认值 `false`
+ */
+ enablePermission?: boolean
+ /**
+ * 是否开启载入进度条
+ * @默认值 `true`
+ */
+ enableProgress?: boolean
+ /**
+ * 是否开启动态标题
+ * @默认值 `false`
+ */
+ enableDynamicTitle?: boolean
+ /**
+ * 是否开启返回顶部按钮
+ * @默认值 `true`
+ */
+ enableBackTop?: boolean
+ }
+ interface navbar {
+ /**
+ * 是否启用
+ * @默认值 `true`
+ */
+ enable?: boolean
+ }
+ interface tabbar {
+ /**
+ * 是否启用
+ * @默认值 `false`
+ */
+ enable?: boolean
+ /**
+ * 导航菜单
+ */
+ list?: {
+ path: string
+ icon?: string
+ activeIcon?: string
+ text?: string
+ }[]
+ }
+ interface copyright {
+ /**
+ * 是否开启底部版权,同时在路由 meta 对象里可以单独设置某个路由是否显示底部版权信息
+ * @默认值 `false`
+ */
+ enable?: boolean
+ /**
+ * 网站运行日期
+ * @默认值 `''`
+ */
+ dates?: string
+ /**
+ * 公司名称
+ * @默认值 `''`
+ */
+ company?: string
+ /**
+ * 网站地址
+ * @默认值 `''`
+ */
+ website?: string
+ /**
+ * 网站备案号
+ * @默认值 `''`
+ */
+ beian?: string
+ }
+ interface all {
+ /** 应用设置 */
+ app?: app
+ /** 顶部导航栏 */
+ navbar?: navbar
+ /** 底部导航栏 */
+ tabbar?: tabbar
+ /** 底部版权设置 */
+ copyright?: copyright
+ }
+}
diff --git a/src/types/route-meta.d.ts b/src/types/route-meta.d.ts
new file mode 100644
index 0000000..c567c11
--- /dev/null
+++ b/src/types/route-meta.d.ts
@@ -0,0 +1,12 @@
+import type { RouteNamedMap } from 'vue-router/auto-routes'
+
+export {}
+
+declare module 'vue-router' {
+ interface RouteMeta {
+ title?: string
+ cache?: boolean | keyof RouteNamedMap | (keyof RouteNamedMap)[]
+ noCache?: keyof RouteNamedMap | (keyof RouteNamedMap)[]
+ auth?: boolean | string | string[]
+ }
+}
diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts
new file mode 100755
index 0000000..0326343
--- /dev/null
+++ b/src/types/shims.d.ts
@@ -0,0 +1,5 @@
+declare interface Window {
+ // extend the window
+}
+
+declare module 'vue-esign'
diff --git a/src/types/typed-router.d.ts b/src/types/typed-router.d.ts
new file mode 100644
index 0000000..4bbe5ae
--- /dev/null
+++ b/src/types/typed-router.d.ts
@@ -0,0 +1,49 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// 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-routes' {
+ import type {
+ RouteRecordInfo,
+ ParamValue,
+ ParamValueOneOrMore,
+ ParamValueZeroOrMore,
+ ParamValueZeroOrOne,
+ } from 'unplugin-vue-router/types'
+
+ /**
+ * Route name map generated by unplugin-vue-router
+ */
+ export interface RouteNamedMap {
+ '/': RouteRecordInfo<'/', '/', Record, Record>,
+ '/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue }, { all: ParamValue }>,
+ '/feature/': RouteRecordInfo<'/feature/', '/feature', Record, Record>,
+ '/feature/component/basic': RouteRecordInfo<'/feature/component/basic', '/feature/component/basic', Record, Record>,
+ '/feature/component/built-in': RouteRecordInfo<'/feature/component/built-in', '/feature/component/built-in', Record, Record>,
+ '/feature/component/pagemain-demo': RouteRecordInfo<'/feature/component/pagemain-demo', '/feature/component/pagemain-demo', Record, Record>,
+ '/feature/component/trend-demo': RouteRecordInfo<'/feature/component/trend-demo', '/feature/component/trend-demo', Record, Record>,
+ '/feature/function/icon': RouteRecordInfo<'/feature/function/icon', '/feature/function/icon', Record, Record>,
+ '/feature/function/keepAlive': RouteRecordInfo<'/feature/function/keepAlive', '/feature/function/keepAlive', Record, Record>,
+ '/feature/function/unocss': RouteRecordInfo<'/feature/function/unocss', '/feature/function/unocss', Record, Record>,
+ '/feature/navbar/custom-area': RouteRecordInfo<'/feature/navbar/custom-area', '/feature/navbar/custom-area', Record, Record>,
+ '/feature/navbar/custom-navbar': RouteRecordInfo<'/feature/navbar/custom-navbar', '/feature/navbar/custom-navbar', Record, Record>,
+ '/feature/navbar/default': RouteRecordInfo<'/feature/navbar/default', '/feature/navbar/default', Record, Record>,
+ '/feature/navbar/none': RouteRecordInfo<'/feature/navbar/none', '/feature/navbar/none', Record, Record>,
+ '/feature/permission/': RouteRecordInfo<'/feature/permission/', '/feature/permission', Record, Record>,
+ '/feature/permission/test': RouteRecordInfo<'/feature/permission/test', '/feature/permission/test', Record, Record>,
+ '/feature/plugin/animation': RouteRecordInfo<'/feature/plugin/animation', '/feature/plugin/animation', Record, Record>,
+ '/feature/plugin/echarts': RouteRecordInfo<'/feature/plugin/echarts', '/feature/plugin/echarts', Record, Record>,
+ '/feature/plugin/esign': RouteRecordInfo<'/feature/plugin/esign', '/feature/plugin/esign', Record, Record>,
+ '/feature/plugin/qrcode': RouteRecordInfo<'/feature/plugin/qrcode', '/feature/plugin/qrcode', Record, Record>,
+ '/feature/plugin/swiper': RouteRecordInfo<'/feature/plugin/swiper', '/feature/plugin/swiper', Record, Record>,
+ '/feature/plugin/vchart': RouteRecordInfo<'/feature/plugin/vchart', '/feature/plugin/vchart', Record, Record>,
+ '/feature/tabbar/custom-area': RouteRecordInfo<'/feature/tabbar/custom-area', '/feature/tabbar/custom-area', Record, Record>,
+ '/feature/tabbar/default': RouteRecordInfo<'/feature/tabbar/default', '/feature/tabbar/default', Record, Record>,
+ 'login': RouteRecordInfo<'login', '/login', Record, Record>,
+ 'reload': RouteRecordInfo<'reload', '/reload', Record, Record>,
+ '/user/': RouteRecordInfo<'/user/', '/user', Record, Record>,
+ }
+}
diff --git a/src/ui-kit/HBadge.vue b/src/ui-kit/HBadge.vue
new file mode 100644
index 0000000..a0deebb
--- /dev/null
+++ b/src/ui-kit/HBadge.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
diff --git a/src/ui-kit/HButton.vue b/src/ui-kit/HButton.vue
new file mode 100644
index 0000000..23a1e24
--- /dev/null
+++ b/src/ui-kit/HButton.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/src/ui-kit/HInput.vue b/src/ui-kit/HInput.vue
new file mode 100644
index 0000000..debdb68
--- /dev/null
+++ b/src/ui-kit/HInput.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/src/ui-kit/HSlideover.vue b/src/ui-kit/HSlideover.vue
new file mode 100644
index 0000000..4b0687d
--- /dev/null
+++ b/src/ui-kit/HSlideover.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/src/ui-kit/HTabList.vue b/src/ui-kit/HTabList.vue
new file mode 100644
index 0000000..58ca336
--- /dev/null
+++ b/src/ui-kit/HTabList.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui-kit/HToggle.vue b/src/ui-kit/HToggle.vue
new file mode 100644
index 0000000..a9190e1
--- /dev/null
+++ b/src/ui-kit/HToggle.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui-provider/index.ts b/src/ui-provider/index.ts
new file mode 100644
index 0000000..26ef998
--- /dev/null
+++ b/src/ui-provider/index.ts
@@ -0,0 +1,10 @@
+import type { App } from 'vue'
+import Vant from 'vant'
+import 'vant/lib/index.css'
+import '@vant/touch-emulator'
+
+function install(app: App) {
+ app.use(Vant)
+}
+
+export default { install }
diff --git a/src/ui-provider/index.vue b/src/ui-provider/index.vue
new file mode 100644
index 0000000..4179e08
--- /dev/null
+++ b/src/ui-provider/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/src/utils/composables/useAuth.ts b/src/utils/composables/useAuth.ts
new file mode 100755
index 0000000..76a3f8b
--- /dev/null
+++ b/src/utils/composables/useAuth.ts
@@ -0,0 +1,35 @@
+import useSettingsStore from '@/store/modules/settings'
+import useUserStore from '@/store/modules/user'
+
+export default function useAuth() {
+ function hasPermission(permission: string) {
+ const settingsStore = useSettingsStore()
+ const userStore = useUserStore()
+ if (settingsStore.settings.app.enablePermission) {
+ return userStore.permissions.includes(permission)
+ }
+ else {
+ return true
+ }
+ }
+
+ function auth(value: string | string[]) {
+ let auth
+ if (typeof value === 'string') {
+ auth = value !== '' ? hasPermission(value) : true
+ }
+ else {
+ auth = value.length > 0 ? value.some(item => hasPermission(item)) : true
+ }
+ return auth
+ }
+
+ function authAll(value: string[]) {
+ return value.length > 0 ? value.every(item => hasPermission(item)) : true
+ }
+
+ return {
+ auth,
+ authAll,
+ }
+}
diff --git a/src/utils/composables/useGlobalProperties.ts b/src/utils/composables/useGlobalProperties.ts
new file mode 100755
index 0000000..8cb57df
--- /dev/null
+++ b/src/utils/composables/useGlobalProperties.ts
@@ -0,0 +1,6 @@
+import type { ComponentInternalInstance } from 'vue'
+
+export default function useGlobalProperties() {
+ const { appContext } = getCurrentInstance() as ComponentInternalInstance
+ return appContext.config.globalProperties
+}
diff --git a/src/utils/composables/usePage.ts b/src/utils/composables/usePage.ts
new file mode 100644
index 0000000..e1dd5cf
--- /dev/null
+++ b/src/utils/composables/usePage.ts
@@ -0,0 +1,13 @@
+export default function usePage() {
+ const router = useRouter()
+
+ function reload() {
+ router.push({
+ name: 'reload',
+ })
+ }
+
+ return {
+ reload,
+ }
+}
diff --git a/src/utils/dayjs.ts b/src/utils/dayjs.ts
new file mode 100755
index 0000000..37bf2f2
--- /dev/null
+++ b/src/utils/dayjs.ts
@@ -0,0 +1,6 @@
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+
+dayjs.locale('zh-cn')
+
+export default dayjs
diff --git a/src/utils/directive.ts b/src/utils/directive.ts
new file mode 100755
index 0000000..bbd2481
--- /dev/null
+++ b/src/utils/directive.ts
@@ -0,0 +1,19 @@
+import type { App } from 'vue'
+
+export default function directive(app: App) {
+ // 注册 v-auth 和 v-auth-all 指令
+ app.directive('auth', {
+ mounted: (el, binding) => {
+ if (!useAuth().auth(binding.value)) {
+ el.remove()
+ }
+ },
+ })
+ app.directive('auth-all', {
+ mounted: (el, binding) => {
+ if (!useAuth().authAll(binding.value)) {
+ el.remove()
+ }
+ },
+ })
+}
diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts
new file mode 100755
index 0000000..9ea0600
--- /dev/null
+++ b/src/utils/eventBus.ts
@@ -0,0 +1,3 @@
+import mitt from 'mitt'
+
+export default mitt()
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100755
index 0000000..d5369d3
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,26 @@
+function isObject(value: any) {
+ return typeof value === 'object' && !Array.isArray(value)
+}
+// 比较两个对象,并提取出不同的部分
+export function getTwoObjectDiff(originalObj: Record, diffObj: Record) {
+ if (!isObject(originalObj) || !isObject(diffObj)) {
+ return diffObj
+ }
+ const diff: Record = {}
+ for (const key in diffObj) {
+ const originalValue = originalObj[key]
+ const diffValue = diffObj[key]
+ if (JSON.stringify(originalValue) !== JSON.stringify(diffValue)) {
+ if (isObject(originalValue) && isObject(diffValue)) {
+ const nestedDiff = getTwoObjectDiff(originalValue, diffValue)
+ if (Object.keys(nestedDiff).length > 0) {
+ diff[key] = nestedDiff
+ }
+ }
+ else {
+ diff[key] = diffValue
+ }
+ }
+ }
+ return diff
+}
diff --git a/src/utils/system.copyright.ts b/src/utils/system.copyright.ts
new file mode 100755
index 0000000..79c3902
--- /dev/null
+++ b/src/utils/system.copyright.ts
@@ -0,0 +1,16 @@
+// 请勿删除
+if (import.meta.env.PROD) {
+ const copyright_common_style = 'font-size: 14px; margin-bottom: 2px; padding: 6px 8px; color: #fff;'
+ const copyright_main_style = `${copyright_common_style} background: #e24329;`
+ const copyright_sub_style = `${copyright_common_style} background: #707070;`
+ if (navigator.language.toLowerCase() === 'zh-cn') {
+ // eslint-disable-next-line no-console
+ console.info('%c由%cFantastic-mobile%c驱动', copyright_sub_style, copyright_main_style, copyright_sub_style, '\nhttps://fantastic-mobile.github.io')
+ }
+ else {
+ // eslint-disable-next-line no-console
+ console.info('%cPowered by%cFantastic-mobile', copyright_sub_style, copyright_main_style, '\nhttps://fantastic-mobile.github.io')
+ }
+}
+
+export {}
diff --git a/src/views/[...all].vue b/src/views/[...all].vue
new file mode 100755
index 0000000..8239dc5
--- /dev/null
+++ b/src/views/[...all].vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ 404
+
+
+ 抱歉,你访问的页面不存在
+
+
+ {{ data.countdown }} 秒后,返回首页
+
+
+
+
diff --git a/src/views/feature/component/basic.vue b/src/views/feature/component/basic.vue
new file mode 100644
index 0000000..7b20153
--- /dev/null
+++ b/src/views/feature/component/basic.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+ 框架内置 Vant 组件库,本页仅展示部分组件,更多组件及使用说明请查看 Vant 官网
+
+
+
+ 主要按钮
+
+
+ 成功按钮
+
+
+ 默认按钮
+
+
+ 危险按钮
+
+
+ 警告按钮
+
+
+
+
+
+
+
+
+
+
+
+ 单选框 1
+
+
+ 单选框 2
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/component/built-in.vue b/src/views/feature/component/built-in.vue
new file mode 100644
index 0000000..2d2a6d7
--- /dev/null
+++ b/src/views/feature/component/built-in.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+ PageMain 是最常用的页面组件,几乎所有页面都会使用到
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 按钮
+
+
+ 按钮
+
+
+ 按钮
+
+
+ 按钮
+
+
+
+
+
+
+
+ 打开
+
+
+ 这里是 slideover 内容
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/component/pagemain-demo.vue b/src/views/feature/component/pagemain-demo.vue
new file mode 100644
index 0000000..9fc0dd6
--- /dev/null
+++ b/src/views/feature/component/pagemain-demo.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+ PageMain 是最常用的页面组件,几乎所有页面都会使用到
+
+
+ 这里放页面内容
+
+
+
+
+ 通过 slot 设置标题
+
+ 还可以放置自定义按钮
+
+
+
+ 这里放页面内容
+
+
+
+ Fantastic-mobile
+
+
+
+
+
diff --git a/src/views/feature/component/trend-demo.vue b/src/views/feature/component/trend-demo.vue
new file mode 100644
index 0000000..e084f21
--- /dev/null
+++ b/src/views/feature/component/trend-demo.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+ 标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/function/icon.vue b/src/views/feature/function/icon.vue
new file mode 100644
index 0000000..35322d6
--- /dev/null
+++ b/src/views/feature/function/icon.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+ 单色 Icon
+
+
+
+
+
+ 彩色 Icon
+
+
+
+
+
+ Iconify Icon
+
+
+
+
+
+
+
翻转:
+
+
+ 无
+
+
+ 水平翻转
+
+
+ 垂直翻转
+
+
+ 水平垂直翻转
+
+
+
旋转:
+
+
+
+
+
+
+
diff --git a/src/views/feature/function/keepAlive.vue b/src/views/feature/function/keepAlive.vue
new file mode 100644
index 0000000..f97aa48
--- /dev/null
+++ b/src/views/feature/function/keepAlive.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/function/unocss.vue b/src/views/feature/function/unocss.vue
new file mode 100644
index 0000000..78e18d5
--- /dev/null
+++ b/src/views/feature/function/unocss.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/src/views/feature/index.vue b/src/views/feature/index.vue
new file mode 100644
index 0000000..d5e46aa
--- /dev/null
+++ b/src/views/feature/index.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+ {{ route.title }}
+
+
+
+
+
+
+
diff --git a/src/views/feature/navbar/custom-area.vue b/src/views/feature/navbar/custom-area.vue
new file mode 100644
index 0000000..b6634c9
--- /dev/null
+++ b/src/views/feature/navbar/custom-area.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+ 操作按钮
+
+
+
+
+
+ Switch: {{ checked }}
+
+
+ 返回
+
+
+
+
diff --git a/src/views/feature/navbar/custom-navbar.vue b/src/views/feature/navbar/custom-navbar.vue
new file mode 100644
index 0000000..0f33077
--- /dev/null
+++ b/src/views/feature/navbar/custom-navbar.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ 使用自定义导航可以满足更复杂的业务场景,例如当前页面,你可以滚动页面试试。
+
+ 返回
+
+
+ {{ i }}
+
+
+
+
+
+
+
diff --git a/src/views/feature/navbar/default.vue b/src/views/feature/navbar/default.vue
new file mode 100644
index 0000000..37b021b
--- /dev/null
+++ b/src/views/feature/navbar/default.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ 返回
+
+
+
+
diff --git a/src/views/feature/navbar/none.vue b/src/views/feature/navbar/none.vue
new file mode 100644
index 0000000..c076c80
--- /dev/null
+++ b/src/views/feature/navbar/none.vue
@@ -0,0 +1,9 @@
+
+
+
+
+ 返回
+
+
+
+
diff --git a/src/views/feature/permission/index.vue b/src/views/feature/permission/index.vue
new file mode 100644
index 0000000..5d759aa
--- /dev/null
+++ b/src/views/feature/permission/index.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+ 切换帐号
+
+ 帐号权限
+ {{ userStore.permissions }}
+ 访问鉴权页面
+
+
+ 点击访问
+
+
+ 鉴权组件(请对照代码查看)
+
+
+
+ 你有 permission.browse 权限
+
+
+
+ 你没有 permission.browse 权限
+
+
+
+
+
+ 你有 permission.create 权限
+
+
+
+ 你没有 permission.create 权限
+
+
+
+
+
+ 你有 permission.browse 或 permission.create 权限
+
+
+
+ 你没有 permission.browse 或 permission.create 权限
+
+
+
+
+
+ 你有 permission.browse 和 permission.create 权限
+
+
+
+ 你没有 permission.browse 和 permission.create 权限
+
+
+
+
+ 鉴权指令(请对照代码查看)
+
+
+ 如果你有 permission.browse 权限则能看到这句话
+
+
+ 如果你有 permission.create 权限则能看到这句话
+
+
+ 如果你有 permission.browse 或 permission.create 权限则能看到这句话
+
+
+ 如果你有 permission.browse 和 permission.create 权限则能看到这句话
+
+
+ 鉴权函数(请对照代码查看)
+
+
+
+
+ 校验 permission.browse 权限
+
+
+
+
+ 校验 permission.create 权限
+
+
+
+
+ 校验 permission.browse 或 permission.create 权限
+
+
+
+
+ 校验 permission.browse 和 permission.create 权限
+
+
+
+
+
+
+
diff --git a/src/views/feature/permission/test.vue b/src/views/feature/permission/test.vue
new file mode 100644
index 0000000..8415814
--- /dev/null
+++ b/src/views/feature/permission/test.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+ 你能看到这个页面,说明你有访问权限。
+
+
+
diff --git a/src/views/feature/plugin/animation.vue b/src/views/feature/plugin/animation.vue
new file mode 100644
index 0000000..cf35766
--- /dev/null
+++ b/src/views/feature/plugin/animation.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add animate.css
+
+
+
+
+
+
+
+
+ {{ animateList.find(item => item.value === animateIn)?.text }}
+
+
+
+
+
+
+
+ {{ animateList.find(item => item.value === animateOut)?.text }}
+
+
+
+
+
+
+
+
+ {{ flag ? '隐藏' : '显示' }}
+
+
+
+
+
+
+
diff --git a/src/views/feature/plugin/echarts.vue b/src/views/feature/plugin/echarts.vue
new file mode 100644
index 0000000..e7446dd
--- /dev/null
+++ b/src/views/feature/plugin/echarts.vue
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add echarts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/plugin/esign.vue b/src/views/feature/plugin/esign.vue
new file mode 100644
index 0000000..f74c276
--- /dev/null
+++ b/src/views/feature/plugin/esign.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add vue-esign
+
+
+
+
+
+
+
+
+
+ 清空画板
+
+
+ 生成图片
+
+
+ 下载图片
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/plugin/qrcode.vue b/src/views/feature/plugin/qrcode.vue
new file mode 100644
index 0000000..8d8351e
--- /dev/null
+++ b/src/views/feature/plugin/qrcode.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add qrcode
+
+
+ pnpm add @types/qrcode -D
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/plugin/swiper.vue b/src/views/feature/plugin/swiper.vue
new file mode 100644
index 0000000..31d20aa
--- /dev/null
+++ b/src/views/feature/plugin/swiper.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add swiper
+
+
+
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+ Slide 5
+ Slide 6
+ Slide 7
+ Slide 8
+ Slide 9
+ Slide 10
+
+
+
+
+
+
diff --git a/src/views/feature/plugin/vchart.vue b/src/views/feature/plugin/vchart.vue
new file mode 100644
index 0000000..fd1c5bc
--- /dev/null
+++ b/src/views/feature/plugin/vchart.vue
@@ -0,0 +1,441 @@
+
+
+
+
+
+
+
+
+
+
+
+ 「插件」栏目下均为第三方插件的演示页面,框架默认并不包含这些插件。如需使用,请先安装对应插件。
+
+
安装命令:
+
+
+ pnpm add @visactor/vchart
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/feature/tabbar/custom-area.vue b/src/views/feature/tabbar/custom-area.vue
new file mode 100644
index 0000000..36fb97b
--- /dev/null
+++ b/src/views/feature/tabbar/custom-area.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
可以根据业务逻辑做更精细化的处理,比如动态切换显示内容。
+
+
+
+
+ 返回
+
+
+
+
diff --git a/src/views/feature/tabbar/default.vue b/src/views/feature/tabbar/default.vue
new file mode 100644
index 0000000..b02ce67
--- /dev/null
+++ b/src/views/feature/tabbar/default.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
底部导航默认是全局关闭的,可以单独给指定路由设置开启底部导航。
+
当然也可以设置为全局开启,指定路由设置关闭。
+
+ 返回
+
+
+
+
diff --git a/src/views/index.vue b/src/views/index.vue
new file mode 100755
index 0000000..0815147
--- /dev/null
+++ b/src/views/index.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
![](@/assets/images/logo.png)
+
+ Fantastic-mobile
+
+
+ 别具一格, 自成一派
+
+
+
+
+ 开发文档
+
+
+
+
+
diff --git a/src/views/login.vue b/src/views/login.vue
new file mode 100755
index 0000000..1dc8584
--- /dev/null
+++ b/src/views/login.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
![](@/assets/images/logo.png)
+
+
+
+
+
+
+
+
+ 提交
+
+
+ 演示账号一键登录
+
+
+
+ admin
+
+
+ test
+
+
+
+
+
+
+
+
+
diff --git a/src/views/reload.vue b/src/views/reload.vue
new file mode 100755
index 0000000..e5dde11
--- /dev/null
+++ b/src/views/reload.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/views/user/index.vue b/src/views/user/index.vue
new file mode 100644
index 0000000..1ef342b
--- /dev/null
+++ b/src/views/user/index.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+ Hi, {{ userStore.account }}
+
+
+ 这是个人中心示例页面噢~
+
+
+
+
+
+
+ Vite
+
+
+
+ Vue.js
+
+
+
+ UnoCSS
+
+
+
+ Pinia
+
+
+
+
+
+
+
+
+
+ 分享
+
+ 999+
+
+
+
+
+
+
+
+
+
+ 登出
+
+
+
+
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100755
index 0000000..cd3d1c6
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,32 @@
+export default {
+ extends: [
+ 'stylelint-config-standard-scss',
+ 'stylelint-config-standard-vue/scss',
+ 'stylelint-config-recess-order',
+ '@stylistic/stylelint-config',
+ ],
+ plugins: [
+ 'stylelint-scss',
+ ],
+ rules: {
+ 'at-rule-no-unknown': null,
+ 'no-descending-specificity': null,
+ 'property-no-unknown': null,
+ 'font-family-no-missing-generic-family-keyword': null,
+ 'selector-class-pattern': null,
+ 'scss/double-slash-comment-empty-line-before': null,
+ 'scss/no-global-function-names': null,
+ '@stylistic/max-line-length': null,
+ '@stylistic/block-closing-brace-newline-after': [
+ 'always',
+ {
+ ignoreAtRules: ['if', 'else'],
+ },
+ ],
+ },
+ allowEmptyInput: true,
+ ignoreFiles: [
+ 'node_modules/**/*',
+ 'dist*/**/*',
+ ],
+}
diff --git a/themes/index.ts b/themes/index.ts
new file mode 100644
index 0000000..0406e18
--- /dev/null
+++ b/themes/index.ts
@@ -0,0 +1,39 @@
+import { hex2rgba } from '@unocss/preset-mini/utils'
+
+export const lightTheme = {
+ // 颜色主题
+ 'color-scheme': 'light',
+ // 内置 UI
+ '--ui-primary': hex2rgba('#0f0f0f')!.join(' '),
+ '--ui-text': hex2rgba('#fcfcfc')!.join(' '),
+ // 主体
+ '--g-bg': '#f2f2f2',
+ '--g-container-bg': '#fff',
+ '--g-border-color': '#DCDFE6',
+ // 导航栏
+ '--g-navbar-bg': '#fff',
+ '--g-navbar-color': '#0f0f0f',
+ // 标签栏
+ '--g-tabbar-bg': '#fff',
+ '--g-tabbar-color': '#6f6f6f',
+ '--g-tabbar-active-color': '#0f0f0f',
+}
+
+export const darkTheme = {
+ // 颜色主题
+ 'color-scheme': 'dark',
+ // 内置 UI
+ '--ui-primary': hex2rgba('#e5e5e5')!.join(' '),
+ '--ui-text': hex2rgba('#242b33')!.join(' '),
+ // 主体
+ '--g-bg': '#0a0a0a',
+ '--g-container-bg': '#141414',
+ '--g-border-color': '#15191e',
+ // 导航栏
+ '--g-navbar-bg': '#141414',
+ '--g-navbar-color': '#e5e5e5',
+ // 标签栏
+ '--g-tabbar-bg': '#141414',
+ '--g-tabbar-color': '#6f6f6f',
+ '--g-tabbar-active-color': '#e5e5e5',
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100755
index 0000000..9fe58a7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,47 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "jsx": "preserve",
+ "lib": [
+ "ESNext",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "useDefineForClassFields": true,
+ "baseUrl": "./",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "paths": {
+ "@/*": [
+ "src/*"
+ ],
+ "#/*": [
+ "src/types/*"
+ ]
+ },
+ "resolveJsonModule": true,
+ "types": [
+ "vite/client",
+ "unplugin-vue-router/client"
+ ],
+ "allowImportingTsExtensions": true,
+ "allowJs": false,
+ "strict": true,
+ "noEmit": true,
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "skipLibCheck": true
+ },
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ],
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.d.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue"
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100755
index 0000000..f496282
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true
+ },
+ "include": [
+ "package.json",
+ "vite.config.ts",
+ "vite/**/*.ts"
+ ]
+}
diff --git a/uno.config.ts b/uno.config.ts
new file mode 100755
index 0000000..84c0073
--- /dev/null
+++ b/uno.config.ts
@@ -0,0 +1,87 @@
+import type { Theme } from 'unocss/preset-uno'
+import {
+ defineConfig,
+ presetAttributify,
+ presetIcons,
+ presetTypography,
+ presetUno,
+ transformerCompileClass,
+ transformerDirectives,
+ transformerVariantGroup,
+} from 'unocss'
+import presetRemToPx from '@unocss/preset-rem-to-px'
+import presetSafeArea from '@yeungkc/unocss-preset-safe-area'
+import { entriesToCss, toArray } from '@unocss/core'
+import { darkTheme, lightTheme } from './themes'
+
+export default defineConfig({
+ content: {
+ pipeline: {
+ include: [
+ /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
+ 'src/**/*.{js,ts}',
+ ],
+ },
+ },
+ variants: [
+ (matcher) => {
+ if (!matcher.startsWith('hocus:') && !matcher.startsWith('hocus-')) {
+ return matcher
+ }
+ return {
+ matcher: matcher.slice(6),
+ selector: s => `${s}:hover, ${s}:focus`,
+ }
+ },
+ ],
+ shortcuts: [
+ {
+ 'flex-center': 'flex justify-center items-center',
+ 'flex-col-center': 'flex flex-col justify-center items-center',
+ },
+ ],
+ preflights: [
+ {
+ getCSS: () => {
+ const returnCss: any = []
+ // 明亮主题
+ const lightCss = entriesToCss(Object.entries(lightTheme))
+ const lightRoots = toArray([`*,::before,::after`, `::backdrop`])
+ returnCss.push(lightRoots.map(root => `${root}{${lightCss}}`).join(''))
+ // 暗黑主题
+ const darkCss = entriesToCss(Object.entries(darkTheme))
+ const darkRoots = toArray([`html.dark,html.dark *,html.dark ::before,html.dark ::after`, `html.dark ::backdrop`])
+ returnCss.push(darkRoots.map(root => `${root}{${darkCss}}`).join(''))
+
+ return returnCss.join('')
+ },
+ },
+ ],
+ theme: {
+ colors: {
+ 'ui-primary': 'rgb(var(--ui-primary))',
+ 'ui-text': 'rgb(var(--ui-text))',
+ },
+ },
+ presets: [
+ presetUno(),
+ presetAttributify(),
+ presetIcons({
+ extraProperties: {
+ 'display': 'inline-block',
+ 'vertical-align': 'middle',
+ },
+ }),
+ presetTypography(),
+ presetRemToPx(),
+ presetSafeArea(),
+ ],
+ transformers: [
+ transformerDirectives(),
+ transformerVariantGroup(),
+ transformerCompileClass(),
+ ],
+ configDeps: [
+ 'themes/index.ts',
+ ],
+})
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100755
index 0000000..1b51fd4
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,51 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+import { defineConfig, loadEnv } from 'vite'
+import createVitePlugins from './vite/plugins'
+
+// https://vitejs.dev/config/
+export default ({ mode, command }) => {
+ const env = loadEnv(mode, process.cwd())
+ // 全局 scss 资源
+ const scssResources = []
+ fs.readdirSync('src/assets/styles/resources').forEach((dirname) => {
+ if (fs.statSync(`src/assets/styles/resources/${dirname}`).isFile()) {
+ scssResources.push(`@use "src/assets/styles/resources/${dirname}" as *;`)
+ }
+ })
+ return defineConfig({
+ base: './',
+ // 开发服务器选项 https://cn.vitejs.dev/config/#server-options
+ server: {
+ open: true,
+ port: 9000,
+ proxy: {
+ '/proxy': {
+ target: env.VITE_APP_API_BASEURL,
+ changeOrigin: command === 'serve' && env.VITE_OPEN_PROXY === 'true',
+ rewrite: path => path.replace(/\/proxy/, ''),
+ },
+ },
+ },
+ // 构建选项 https://cn.vitejs.dev/config/#server-fsserve-root
+ build: {
+ outDir: mode === 'production' ? 'dist' : `dist-${mode}`,
+ sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
+ },
+ plugins: createVitePlugins(env, command === 'build'),
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ '#': path.resolve(__dirname, 'src/types'),
+ },
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: scssResources.join(''),
+ },
+ },
+ },
+ })
+}
diff --git a/vite/plugins/app-info.ts b/vite/plugins/app-info.ts
new file mode 100755
index 0000000..da5f116
--- /dev/null
+++ b/vite/plugins/app-info.ts
@@ -0,0 +1,25 @@
+import boxen from 'boxen'
+import picocolors from 'picocolors'
+import type { Plugin } from 'vite'
+
+export default function appInfo(): Plugin {
+ return {
+ name: 'appInfo',
+ apply: 'serve',
+ async buildStart() {
+ const { bold, green, cyan, bgGreen, underline } = picocolors
+ // eslint-disable-next-line no-console
+ console.log(
+ boxen(
+ `${bold(green(`由 ${bgGreen('Fantastic-mobile')} 驱动`))}\n\n${underline('https://fantastic-mobile.github.io')}\n\n当前使用:${cyan('基础版')}`,
+ {
+ padding: 1,
+ margin: 1,
+ borderStyle: 'double',
+ textAlignment: 'center',
+ },
+ ),
+ )
+ },
+ }
+}
diff --git a/vite/plugins/archiver.ts b/vite/plugins/archiver.ts
new file mode 100755
index 0000000..2cc973e
--- /dev/null
+++ b/vite/plugins/archiver.ts
@@ -0,0 +1,33 @@
+import fs from 'node:fs'
+import dayjs from 'dayjs'
+import archiver from 'archiver'
+import type { Plugin } from 'vite'
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+}
+
+export default function createArchiver(env): Plugin {
+ const { VITE_BUILD_ARCHIVE } = env
+ let outDir: string
+ return {
+ name: 'vite-plugin-archiver',
+ apply: 'build',
+ configResolved(resolvedConfig) {
+ outDir = resolvedConfig.build.outDir
+ },
+ async closeBundle() {
+ if (['zip', 'tar'].includes(VITE_BUILD_ARCHIVE)) {
+ await sleep(1000)
+ const archive = archiver(VITE_BUILD_ARCHIVE, {
+ ...(VITE_BUILD_ARCHIVE === 'zip' && { zlib: { level: 9 } }),
+ ...(VITE_BUILD_ARCHIVE === 'tar' && { gzip: true, gzipOptions: { level: 9 } }),
+ })
+ const output = fs.createWriteStream(`${outDir}.${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.${VITE_BUILD_ARCHIVE === 'zip' ? 'zip' : 'tar.gz'}`)
+ archive.pipe(output)
+ archive.directory(outDir, false)
+ archive.finalize()
+ }
+ },
+ }
+}
diff --git a/vite/plugins/auto-import.ts b/vite/plugins/auto-import.ts
new file mode 100755
index 0000000..4def61b
--- /dev/null
+++ b/vite/plugins/auto-import.ts
@@ -0,0 +1,19 @@
+import autoImport from 'unplugin-auto-import/vite'
+import { VueRouterAutoImports } from 'unplugin-vue-router'
+
+export default function createAutoImport() {
+ return autoImport({
+ imports: [
+ 'vue',
+ 'pinia',
+ VueRouterAutoImports,
+ {
+ 'vue-router/auto': ['useLink'],
+ },
+ ],
+ dts: './src/types/auto-imports.d.ts',
+ dirs: [
+ './src/utils/composables/**',
+ ],
+ })
+}
diff --git a/vite/plugins/banner.ts b/vite/plugins/banner.ts
new file mode 100755
index 0000000..a799214
--- /dev/null
+++ b/vite/plugins/banner.ts
@@ -0,0 +1,11 @@
+import banner from 'vite-plugin-banner'
+
+export default function createBanner() {
+ return banner(`
+/**
+ * 由 Fantastic-mobile 提供技术支持
+ * Powered by Fantastic-mobile
+ * https://fantastic-mobile.github.io/
+ */
+ `)
+}
diff --git a/vite/plugins/components.ts b/vite/plugins/components.ts
new file mode 100755
index 0000000..2e608bf
--- /dev/null
+++ b/vite/plugins/components.ts
@@ -0,0 +1,13 @@
+import components from 'unplugin-vue-components/vite'
+
+export default function createComponents() {
+ return components({
+ dirs: [
+ 'src/components/*',
+ 'src/ui-kit',
+ ],
+ deep: false,
+ include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/],
+ dts: './src/types/components.d.ts',
+ })
+}
diff --git a/vite/plugins/compression.ts b/vite/plugins/compression.ts
new file mode 100755
index 0000000..76b3c26
--- /dev/null
+++ b/vite/plugins/compression.ts
@@ -0,0 +1,24 @@
+import { compression } from 'vite-plugin-compression2'
+import type { PluginOption } from 'vite'
+
+export default function createCompression(env, isBuild) {
+ const plugin: (PluginOption | PluginOption[])[] = []
+ if (isBuild) {
+ const { VITE_BUILD_COMPRESS } = env
+ const compressList = VITE_BUILD_COMPRESS.split(',')
+ if (compressList.includes('gzip')) {
+ plugin.push(
+ compression(),
+ )
+ }
+ if (compressList.includes('brotli')) {
+ plugin.push(
+ compression({
+ exclude: [/\.(br)$/, /\.(gz)$/],
+ algorithm: 'brotliCompress',
+ }),
+ )
+ }
+ }
+ return plugin
+}
diff --git a/vite/plugins/console.ts b/vite/plugins/console.ts
new file mode 100755
index 0000000..71705ca
--- /dev/null
+++ b/vite/plugins/console.ts
@@ -0,0 +1,5 @@
+import TurboConsole from 'unplugin-turbo-console/vite'
+
+export default function createConsole() {
+ return TurboConsole()
+}
diff --git a/vite/plugins/devtools.ts b/vite/plugins/devtools.ts
new file mode 100755
index 0000000..599c747
--- /dev/null
+++ b/vite/plugins/devtools.ts
@@ -0,0 +1,6 @@
+import VueDevTools from 'vite-plugin-vue-devtools'
+
+export default function createDevtools(env) {
+ const { VITE_OPEN_DEVTOOLS } = env
+ return VITE_OPEN_DEVTOOLS === 'true' && VueDevTools()
+}
diff --git a/vite/plugins/index.ts b/vite/plugins/index.ts
new file mode 100755
index 0000000..e171a26
--- /dev/null
+++ b/vite/plugins/index.ts
@@ -0,0 +1,40 @@
+import type { PluginOption } from 'vite'
+import VueRouter from 'unplugin-vue-router/vite'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import appInfo from './app-info'
+
+import createDevtools from './devtools'
+import createAutoImport from './auto-import'
+import createComponents from './components'
+import createUnocss from './unocss'
+import createSvgIcon from './svg-icon'
+import createMock from './mock'
+import createCompression from './compression'
+import createArchiver from './archiver'
+import createConsole from './console'
+import createBanner from './banner'
+
+export default function createVitePlugins(viteEnv, isBuild = false) {
+ const vitePlugins: (PluginOption | PluginOption[])[] = [
+ appInfo(),
+ VueRouter({
+ routesFolder: './src/views',
+ dts: './src/types/typed-router.d.ts',
+ exclude: ['**/components', '**/_*/**', '**/_*'],
+ }),
+ vue(),
+ vueJsx(),
+ ]
+ vitePlugins.push(createDevtools(viteEnv))
+ vitePlugins.push(createAutoImport())
+ vitePlugins.push(createComponents())
+ vitePlugins.push(createUnocss())
+ vitePlugins.push(createSvgIcon(isBuild))
+ vitePlugins.push(createMock(viteEnv, isBuild))
+ vitePlugins.push(...createCompression(viteEnv, isBuild))
+ vitePlugins.push(createArchiver(viteEnv))
+ vitePlugins.push(createConsole())
+ vitePlugins.push(createBanner())
+ return vitePlugins
+}
diff --git a/vite/plugins/mock.ts b/vite/plugins/mock.ts
new file mode 100755
index 0000000..380370b
--- /dev/null
+++ b/vite/plugins/mock.ts
@@ -0,0 +1,11 @@
+import { vitePluginFakeServer } from 'vite-plugin-fake-server'
+
+export default function createMock(env, isBuild) {
+ const { VITE_BUILD_MOCK } = env
+ return vitePluginFakeServer({
+ logger: !isBuild,
+ include: 'src/mock',
+ infixName: false,
+ enableProd: isBuild && VITE_BUILD_MOCK === 'true',
+ })
+}
diff --git a/vite/plugins/svg-icon.ts b/vite/plugins/svg-icon.ts
new file mode 100755
index 0000000..f76753d
--- /dev/null
+++ b/vite/plugins/svg-icon.ts
@@ -0,0 +1,11 @@
+import path from 'node:path'
+import process from 'node:process'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+
+export default function createSvgIcon(isBuild) {
+ return createSvgIconsPlugin({
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/')],
+ symbolId: 'icon-[dir]-[name]',
+ svgoOptions: isBuild,
+ })
+}
diff --git a/vite/plugins/unocss.ts b/vite/plugins/unocss.ts
new file mode 100755
index 0000000..8160e63
--- /dev/null
+++ b/vite/plugins/unocss.ts
@@ -0,0 +1,5 @@
+import Unocss from 'unocss/vite'
+
+export default function createUnocss() {
+ return Unocss()
+}