mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-10-03 23:20:01 +08:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8417432343 | ||
|
f2e82e725f | ||
|
ac9d7e84e5 | ||
|
050ab3e7ed | ||
|
79501a53f0 | ||
|
501b64e884 | ||
|
03a7891ed4 | ||
|
e66e4fb17c | ||
|
ade869ea30 | ||
|
a1085e1922 | ||
|
2535252310 | ||
|
228ccaee5b | ||
|
3144acbc39 | ||
|
1b4639d5d8 |
2
.env
2
.env
@ -20,7 +20,7 @@ VITE_STORAGE_PREFIX =
|
||||
VITE_COPYRIGHT_INFO = Copyright © 2024 chansee97
|
||||
|
||||
# 自动刷新token
|
||||
VITE_AUTO_REFRESH_TOKEN = Y
|
||||
VITE_AUTO_REFRESH_TOKEN = N
|
||||
|
||||
# 默认多语言 enUS | zhCN
|
||||
VITE_DEFAULT_LANG = enUS
|
||||
|
@ -1,6 +0,0 @@
|
||||
# 是否开启压缩资源
|
||||
VITE_BUILD_COMPRESS=N
|
||||
|
||||
# 压缩算法 gzip | brotliCompress | deflate | deflateRaw
|
||||
VITE_COMPRESS_TYPE=gzip
|
||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -10,6 +10,7 @@
|
||||
"antfu.iconify",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"antfu.unocss",
|
||||
"vue.volar"
|
||||
"vue.volar",
|
||||
"tu6ge.naive-ui-intelligence"
|
||||
]
|
||||
}
|
||||
|
@ -34,11 +34,9 @@ export interface ServiceProxyPluginOptions {
|
||||
serviceConfig: FullServiceConfig
|
||||
/** 代理路径前缀(可选,默认为 'proxy-') */
|
||||
proxyPrefix?: string
|
||||
/** 开发环境名称(可选,默认为 'development') */
|
||||
devEnvName?: ServiceEnvType
|
||||
/** 是否启用代理配置 */
|
||||
enableProxy?: boolean
|
||||
/** 环境变量名(可选,默认为 '__PROXY_MAPPING__') */
|
||||
/** 环境变量名(可选,默认为 '__URL_MAP__') */
|
||||
envName?: string
|
||||
/** d.ts 类型文件生成路径(可选,如果传入路径则在该路径生成 d.ts 类型文件) */
|
||||
dts?: string
|
||||
@ -47,16 +45,15 @@ export interface ServiceProxyPluginOptions {
|
||||
export default function createServiceProxyPlugin(options: ServiceProxyPluginOptions) {
|
||||
const {
|
||||
serviceConfig,
|
||||
devEnvName = 'development',
|
||||
proxyPrefix = 'proxy-',
|
||||
enableProxy = true,
|
||||
envName = '__PROXY_MAPPING__',
|
||||
envName = '__URL_MAP__',
|
||||
dts,
|
||||
} = options
|
||||
|
||||
return {
|
||||
name: 'vite-auto-proxy',
|
||||
config(config: UserConfig, { command }: { mode: string, command: 'build' | 'serve' }) {
|
||||
config(config: UserConfig, { mode, command }: { mode: string, command: 'build' | 'serve' }) {
|
||||
// 只在开发环境(serve命令)时生成代理配置
|
||||
const isDev = command === 'serve'
|
||||
|
||||
@ -66,9 +63,9 @@ export default function createServiceProxyPlugin(options: ServiceProxyPluginOpti
|
||||
}
|
||||
|
||||
if (!enableProxy || !isDev) {
|
||||
// 在非开发环境下,生成原始地址映射(path 和 rawPath 都是原始地址)
|
||||
const rawMapping: ProxyMapping = {}
|
||||
const envConfig = serviceConfig[devEnvName]
|
||||
const envConfig = serviceConfig[mode]
|
||||
|
||||
if (envConfig) {
|
||||
Object.entries(envConfig).forEach(([serviceName, serviceUrl]) => {
|
||||
rawMapping[serviceName] = {
|
||||
@ -76,7 +73,12 @@ export default function createServiceProxyPlugin(options: ServiceProxyPluginOpti
|
||||
rawPath: serviceUrl,
|
||||
}
|
||||
})
|
||||
console.warn(`[auto-proxy] 已加载 ${Object.keys(envConfig).length} 个服务地址`)
|
||||
}
|
||||
else {
|
||||
console.warn(`[auto-proxy] 未找到环境 "${mode}" 的配置`)
|
||||
}
|
||||
|
||||
config.define[envName] = JSON.stringify(rawMapping)
|
||||
|
||||
// 生成 d.ts 类型文件(如果指定了路径)
|
||||
@ -86,9 +88,9 @@ export default function createServiceProxyPlugin(options: ServiceProxyPluginOpti
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(`[auto-proxy] 已加载${devEnvName}模式 ${Object.keys(serviceConfig[devEnvName]).length} 个服务地址`)
|
||||
console.warn(`[auto-proxy] 已加载${mode}模式 ${Object.keys(serviceConfig[mode]).length} 个服务地址`)
|
||||
|
||||
const { proxyConfig, proxyMapping } = generateProxyFromServiceConfig(serviceConfig, devEnvName, proxyPrefix)
|
||||
const { proxyConfig, proxyMapping } = generateProxyFromServiceConfig(serviceConfig, mode, proxyPrefix)
|
||||
|
||||
Object.entries(proxyMapping).forEach(([serviceName, proxyItem]) => {
|
||||
console.warn(`[auto-proxy] 服务: ${serviceName} | 代理地址: ${proxyItem.path} | 实际地址: ${proxyItem.rawPath}`)
|
||||
|
@ -83,7 +83,6 @@ export function createVitePlugins(env: ImportMetaEnv) {
|
||||
AutoProxy({
|
||||
enableProxy: env.VITE_HTTP_PROXY === 'Y',
|
||||
serviceConfig,
|
||||
devEnvName: 'dev',
|
||||
dts: 'src/typings/auto-proxy.d.ts',
|
||||
}),
|
||||
]
|
||||
|
@ -9,7 +9,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="appLoading"></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
@ -108,9 +108,6 @@
|
||||
"QRCode": "QR code",
|
||||
"about": "About",
|
||||
"clipboard": "Clipboard",
|
||||
"demo403": "403",
|
||||
"demo404": "404",
|
||||
"demo500": "500",
|
||||
"dictionarySetting": "Dictionary settings",
|
||||
"documents": "Document",
|
||||
"documentsVite": "Vite",
|
||||
@ -122,7 +119,6 @@
|
||||
"editor": "Editor",
|
||||
"editorMd": "MarkDown editor",
|
||||
"editorRich": "Rich text editor",
|
||||
"error": "Exception page",
|
||||
"icons": "Icon",
|
||||
"justSuper": "Supervisible",
|
||||
"map": "Map",
|
||||
|
@ -151,10 +151,6 @@
|
||||
"permission": "权限",
|
||||
"permissionDemo": "权限示例",
|
||||
"justSuper": "super可见",
|
||||
"error": "异常页",
|
||||
"demo403": "403",
|
||||
"demo404": "404",
|
||||
"demo500": "500",
|
||||
"setting": "系统设置",
|
||||
"accountSetting": "用户设置",
|
||||
"dictionarySetting": "字典设置",
|
||||
|
17
netlify.toml
17
netlify.toml
@ -1,17 +0,0 @@
|
||||
[build]
|
||||
publish = "dist"
|
||||
command = "vite build --mode prod"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/manifest.webmanifest"
|
||||
|
||||
[headers.values]
|
||||
Content-Type = "application/manifest+json"
|
48
package.json
48
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nova-admin",
|
||||
"type": "module",
|
||||
"version": "0.9.15",
|
||||
"version": "0.9.18",
|
||||
"private": true,
|
||||
"description": "a clean and concise back-end management template based on Vue3, Vite5, Typescript, and Naive UI.",
|
||||
"author": {
|
||||
@ -38,11 +38,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite --mode dev --port 9980",
|
||||
"dev:test": "vite --mode test",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"build": "vite build --mode prod",
|
||||
"dev:prod": "vite --mode production",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "vite build --mode test",
|
||||
"preview": "vite preview --port 9981",
|
||||
"lint": "eslint . && vue-tsc --noEmit",
|
||||
"lint:fix": "eslint . --fix",
|
||||
@ -50,43 +48,43 @@
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^13.3.0",
|
||||
"alova": "^3.3.2",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"alova": "^3.3.4",
|
||||
"colord": "^2.9.3",
|
||||
"echarts": "^5.6.0",
|
||||
"md-editor-v3": "^5.6.1",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.3.0",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"pro-naive-ui": "^2.4.3",
|
||||
"quill": "^2.0.3",
|
||||
"radash": "^12.1.0",
|
||||
"vue": "^3.5.16",
|
||||
"radash": "^12.1.1",
|
||||
"vue": "^3.5.18",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.14.1",
|
||||
"@antfu/eslint-config": "^5.0.0",
|
||||
"@iconify-json/icon-park-outline": "^1.2.2",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.2.0",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"eslint": "^9.29.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"naive-ui": "^2.41.1",
|
||||
"sass": "^1.86.3",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"naive-ui": "^2.42.0",
|
||||
"sass": "^1.89.2",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^66.2.0",
|
||||
"unocss": "^66.3.3",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vite": "^6.3.5",
|
||||
"unplugin-icons": "^22.2.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.6",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "7.7.6",
|
||||
"vue-tsc": "^2.2.10"
|
||||
"vite-plugin-vue-devtools": "8.0.0",
|
||||
"vue-tsc": "^3.0.5"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
|
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
ignoredBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- esbuild
|
||||
- simple-git-hooks
|
||||
- vue-demi
|
||||
- unrs-resolver
|
@ -1,12 +1,9 @@
|
||||
/** 不同请求服务的环境配置 */
|
||||
export const serviceConfig: Record<ServiceEnvType, Record<string, string>> = {
|
||||
dev: {
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
url: 'http://localhost:3000',
|
||||
},
|
||||
test: {
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
},
|
||||
prod: {
|
||||
production: {
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
},
|
||||
}
|
||||
|
30
src/App.vue
30
src/App.vue
@ -1,24 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { naiveI18nOptions } from '@/utils'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useAppStore } from './store'
|
||||
import AppMain from './AppMain.vue'
|
||||
import AppLoading from './components/common/AppLoading.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const naiveLocale = computed(() => {
|
||||
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
|
||||
},
|
||||
)
|
||||
// 使用 Suspense 处理异步组件加载
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider
|
||||
class="wh-full" inline-theme-disabled :theme="appStore.colorMode === 'dark' ? darkTheme : null"
|
||||
:locale="naiveLocale.locale" :date-locale="naiveLocale.dateLocale" :theme-overrides="appStore.theme"
|
||||
>
|
||||
<naive-provider>
|
||||
<router-view />
|
||||
<Watermark :show-watermark="appStore.showWatermark" />
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
<Suspense>
|
||||
<!-- 异步组件 -->
|
||||
<AppMain />
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<template #fallback>
|
||||
<AppLoading />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
57
src/AppMain.vue
Normal file
57
src/AppMain.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import type { App } from 'vue'
|
||||
import { installRouter } from '@/router'
|
||||
import { installPinia } from '@/store'
|
||||
import { naiveI18nOptions } from '@/utils'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useAppStore } from './store'
|
||||
|
||||
// 创建异步初始化 Promise - 这会让组件变成异步组件
|
||||
const initializationPromise = (async () => {
|
||||
// 获取当前应用实例
|
||||
const app = getCurrentInstance()?.appContext.app
|
||||
if (!app) {
|
||||
throw new Error('Failed to get app instance')
|
||||
}
|
||||
|
||||
// 注册模块 Pinia
|
||||
await installPinia(app)
|
||||
|
||||
// 注册模块 Vue-router
|
||||
await installRouter(app)
|
||||
|
||||
// 注册模块 指令/静态资源
|
||||
const modules = import.meta.glob<{ install: (app: App) => void }>('./modules/*.ts', {
|
||||
eager: true,
|
||||
})
|
||||
|
||||
Object.values(modules).forEach(module => app.use(module))
|
||||
|
||||
return true
|
||||
})()
|
||||
|
||||
// 等待初始化完成 - 这使得整个 setup 函数变成异步的
|
||||
await initializationPromise
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const naiveLocale = computed(() => {
|
||||
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider
|
||||
class="wh-full"
|
||||
inline-theme-disabled
|
||||
:theme="appStore.colorMode === 'dark' ? darkTheme : null"
|
||||
:locale="naiveLocale.locale"
|
||||
:date-locale="naiveLocale.dateLocale"
|
||||
:theme-overrides="appStore.theme"
|
||||
>
|
||||
<naive-provider>
|
||||
<router-view />
|
||||
<Watermark :show-watermark="appStore.showWatermark" />
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 60 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
/** 异常类型 403 404 500 */
|
||||
type: '403' | '404' | '500'
|
||||
}>()
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-center h-full">
|
||||
<img
|
||||
v-if="type === '403'"
|
||||
src="@/assets/svg/error-403.svg"
|
||||
alt=""
|
||||
class="w-1/3"
|
||||
>
|
||||
<img
|
||||
v-if="type === '404'"
|
||||
src="@/assets/svg/error-404.svg"
|
||||
alt=""
|
||||
class="w-1/3"
|
||||
>
|
||||
<img
|
||||
v-if="type === '500'"
|
||||
src="@/assets/svg/error-500.svg"
|
||||
alt=""
|
||||
class="w-1/3"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
{{ $t('app.backHome') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
@ -6,11 +6,15 @@ const routeStore = useRouteStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-el>
|
||||
<n-el
|
||||
class="h-full"
|
||||
:class="[
|
||||
appStore.layoutMode === 'full-content' ? 'p-0' : 'p-16px',
|
||||
]"
|
||||
style="background-color: var(--action-color);"
|
||||
>
|
||||
<router-view
|
||||
v-slot="{ Component, route }"
|
||||
class="flex-1 p-16px"
|
||||
style="background-color: var(--action-color);"
|
||||
>
|
||||
<transition :name="appStore.transitionAnimation" mode="out-in">
|
||||
<keep-alive :include="routeStore.cacheRoutes">
|
||||
|
38
src/layouts/components/common/MobileDrawer.vue
Normal file
38
src/layouts/components/common/MobileDrawer.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import Search from '../header/Search.vue'
|
||||
import Notices from '../header/Notices.vue'
|
||||
import UserCenter from '../header/UserCenter.vue'
|
||||
import Setting from './Setting.vue'
|
||||
|
||||
const showDrawer = defineModel<boolean>('show', { default: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="showDrawer"
|
||||
:width="280"
|
||||
placement="right"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
>
|
||||
<n-drawer-content :native-scrollbar="false" :body-content-style="{ padding: '0' }">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<UserCenter />
|
||||
<div class="ml-auto" />
|
||||
<Search />
|
||||
<Notices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
|
||||
<template #footer>
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<div class="ml-auto" />
|
||||
<Setting />
|
||||
</template>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { useRouteStore } from '@/store'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
// 搜索值
|
||||
@ -143,13 +144,14 @@ function handleMouseEnter(index: number) {
|
||||
|
||||
<template>
|
||||
<CommonWrapper @click="openModal">
|
||||
<icon-park-outline-search /><n-tag round size="small" class="font-mono cursor-pointer">
|
||||
<icon-park-outline-search />
|
||||
<n-tag v-if="!appStore.isMobile" round size="small" class="font-mono cursor-pointer">
|
||||
CtrlK
|
||||
</n-tag>
|
||||
</CommonWrapper>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
class="w-560px fixed top-60px inset-x-0"
|
||||
class="w-560px fixed top-60px inset-x-0 max-w-full"
|
||||
size="small"
|
||||
preset="card"
|
||||
:segmented="{
|
||||
|
@ -61,7 +61,7 @@ function handleSelect(key: string | number) {
|
||||
})
|
||||
}
|
||||
if (key === 'userCenter')
|
||||
router.push('/userCenter')
|
||||
router.push('/user-center')
|
||||
|
||||
if (key === 'guthub')
|
||||
window.open('https://github.com/chansee97/nova-admin')
|
||||
|
@ -2,6 +2,7 @@ import BackTop from './common/BackTop.vue'
|
||||
import Setting from './common/Setting.vue'
|
||||
import SettingDrawer from './common/SettingDrawer.vue'
|
||||
import Logo from './common/Logo.vue'
|
||||
import MobileDrawer from './common/MobileDrawer.vue'
|
||||
|
||||
import Breadcrumb from './header/Breadcrumb.vue'
|
||||
import CollapaseButton from './header/CollapaseButton.vue'
|
||||
@ -18,6 +19,7 @@ export {
|
||||
CollapaseButton,
|
||||
FullScreen,
|
||||
Logo,
|
||||
MobileDrawer,
|
||||
Notices,
|
||||
Search,
|
||||
Setting,
|
||||
|
@ -2,16 +2,49 @@
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
let previousLayoutMode = appStore.layoutMode
|
||||
|
||||
function enterFullContent() {
|
||||
previousLayoutMode = appStore.layoutMode
|
||||
appStore.layoutMode = 'full-content'
|
||||
}
|
||||
|
||||
function exitFullContent() {
|
||||
// 如果是全屏或者数据不存在,则恢复为默认的vertical
|
||||
if (previousLayoutMode === 'full-content' || !previousLayoutMode) {
|
||||
previousLayoutMode = 'vertical'
|
||||
}
|
||||
appStore.layoutMode = previousLayoutMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<n-tooltip v-if="!appStore.isMobile" placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.contentFullScreen = !appStore.contentFullScreen">
|
||||
<icon-park-outline-off-screen-one v-if="appStore.contentFullScreen" />
|
||||
<icon-park-outline-full-screen-one v-else />
|
||||
<CommonWrapper @click="enterFullContent">
|
||||
<icon-park-outline-full-screen-one />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>{{ $t('app.togglContentFullScreen') }}</span>
|
||||
{{ $t('app.togglContentFullScreen') }}
|
||||
</n-tooltip>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="appStore.layoutMode === 'full-content'"
|
||||
class="fixed top-4 right-0 z-[9999]"
|
||||
>
|
||||
<n-tooltip placement="left" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-el
|
||||
class="bg-[var(--primary-color)] c-[var(--base-color)] rounded-l-lg shadow-lg p-2 cursor-pointer"
|
||||
@click="exitFullContent"
|
||||
>
|
||||
<icon-park-outline-off-screen-one />
|
||||
</n-el>
|
||||
</template>
|
||||
{{ $t('app.togglContentFullScreen') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CollapaseButton,
|
||||
FullScreen,
|
||||
Logo,
|
||||
MobileDrawer,
|
||||
Notices,
|
||||
Search,
|
||||
Setting,
|
||||
@ -14,12 +15,12 @@ import {
|
||||
UserCenter,
|
||||
} from './components'
|
||||
import Content from './Content.vue'
|
||||
|
||||
import { ProLayout, useLayoutMenu } from 'pro-naive-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
const { layoutMode } = storeToRefs(useAppStore())
|
||||
|
||||
const {
|
||||
@ -31,16 +32,19 @@ const {
|
||||
menus: routeStore.menus,
|
||||
})
|
||||
|
||||
watch(() => route.path, (value) => {
|
||||
activeKey.value = value
|
||||
watch(() => route.path, () => {
|
||||
activeKey.value = routeStore.activeMenu
|
||||
}, { immediate: true })
|
||||
|
||||
// 移动端抽屉控制
|
||||
const showMobileDrawer = ref(false)
|
||||
|
||||
const sidebarWidth = ref(240)
|
||||
const sidebarCollapsedWidth = ref(64)
|
||||
|
||||
const hasHorizontalMenu = computed(() => ['horizontal', 'mixed-two-column', 'mixed-sidebar'].includes(layoutMode.value))
|
||||
|
||||
const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.value))
|
||||
const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.value) || appStore.isMobile)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -48,7 +52,8 @@ const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.v
|
||||
<ProLayout
|
||||
v-model:collapsed="appStore.collapsed"
|
||||
:mode="layoutMode"
|
||||
:show-logo="appStore.showLogo"
|
||||
:is-mobile="appStore.isMobile"
|
||||
:show-logo="appStore.showLogo && !appStore.isMobile"
|
||||
:show-footer="appStore.showFooter"
|
||||
:show-tabbar="appStore.showTabs"
|
||||
nav-fixed
|
||||
@ -65,10 +70,16 @@ const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.v
|
||||
</template>
|
||||
|
||||
<template #nav-left>
|
||||
<div v-if="!hasHorizontalMenu || !hidenCollapaseButton" class="h-full flex-y-center gap-1 p-x-sm">
|
||||
<CollapaseButton v-if="!hidenCollapaseButton" />
|
||||
<Breadcrumb v-if="!hasHorizontalMenu" />
|
||||
</div>
|
||||
<template v-if="appStore.isMobile">
|
||||
<Logo />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="!hasHorizontalMenu || !hidenCollapaseButton" class="h-full flex-y-center gap-1 p-x-sm">
|
||||
<CollapaseButton v-if="!hidenCollapaseButton" />
|
||||
<Breadcrumb v-if="!hasHorizontalMenu" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #nav-center>
|
||||
@ -79,13 +90,30 @@ const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.v
|
||||
|
||||
<template #nav-right>
|
||||
<div class="h-full flex-y-center gap-1 p-x-xl">
|
||||
<Search />
|
||||
<Notices />
|
||||
<FullScreen />
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<Setting />
|
||||
<UserCenter />
|
||||
<!-- 移动端:只显示菜单按钮 -->
|
||||
<template v-if="appStore.isMobile">
|
||||
<n-button
|
||||
quaternary
|
||||
@click="showMobileDrawer = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="18">
|
||||
<icon-park-outline-hamburger-button />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<!-- 桌面端:显示完整功能组件 -->
|
||||
<template v-else>
|
||||
<Search />
|
||||
<Notices />
|
||||
<FullScreen />
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<Setting />
|
||||
<UserCenter />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -111,5 +139,10 @@ const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.v
|
||||
<Content />
|
||||
<BackTop />
|
||||
<SettingDrawer />
|
||||
|
||||
<!-- 移动端功能抽屉 -->
|
||||
<MobileDrawer v-model:show="showMobileDrawer">
|
||||
<n-menu v-bind="layout.verticalMenuProps" />
|
||||
</MobileDrawer>
|
||||
</ProLayout>
|
||||
</template>
|
||||
|
38
src/main.ts
38
src/main.ts
@ -1,35 +1,5 @@
|
||||
import type { App } from 'vue'
|
||||
import { installRouter } from '@/router'
|
||||
import { installPinia } from '@/store'
|
||||
import AppVue from './App.vue'
|
||||
import AppLoading from './components/common/AppLoading.vue'
|
||||
import App from './App.vue'
|
||||
|
||||
async function setupApp() {
|
||||
// 载入全局loading加载状态
|
||||
const appLoading = createApp(AppLoading)
|
||||
appLoading.mount('#appLoading')
|
||||
|
||||
// 创建vue实例
|
||||
const app = createApp(AppVue)
|
||||
|
||||
// 注册模块Pinia
|
||||
await installPinia(app)
|
||||
|
||||
// 注册模块 Vue-router
|
||||
await installRouter(app)
|
||||
|
||||
/* 注册模块 指令/静态资源 */
|
||||
Object.values(
|
||||
import.meta.glob<{ install: (app: App) => void }>('./modules/*.ts', {
|
||||
eager: true,
|
||||
}),
|
||||
).map(i => app.use(i))
|
||||
|
||||
// 卸载载入动画
|
||||
appLoading.unmount()
|
||||
|
||||
// 挂载
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
setupApp()
|
||||
// 创建应用实例并挂载
|
||||
const app = createApp(App)
|
||||
app.mount('#app')
|
||||
|
@ -22,6 +22,19 @@ export function setupRouterGuard(router: Router) {
|
||||
// 判断有无TOKEN,登录鉴权
|
||||
const isLogin = Boolean(local.get('accessToken'))
|
||||
|
||||
// 处理根路由重定向
|
||||
if (to.name === 'root') {
|
||||
if (isLogin) {
|
||||
// 已登录,重定向到首页
|
||||
next({ path: import.meta.env.VITE_HOME_PATH, replace: true })
|
||||
}
|
||||
else {
|
||||
// 未登录,重定向到登录页
|
||||
next({ path: '/login', replace: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是login路由,直接放行
|
||||
if (to.name === 'login') {
|
||||
// login页面不需要任何认证检查,直接放行
|
||||
@ -34,23 +47,31 @@ export function setupRouterGuard(router: Router) {
|
||||
}
|
||||
// 如果路由设置了requiresAuth为true,且用户未登录,重定向到登录页
|
||||
else if (to.meta.requiresAuth === true && !isLogin) {
|
||||
const redirect = to.name === '404' ? undefined : to.fullPath
|
||||
const redirect = to.name === 'not-found' ? undefined : to.fullPath
|
||||
next({ path: '/login', query: { redirect } })
|
||||
return
|
||||
}
|
||||
|
||||
// 判断路由有无进行初始化
|
||||
if (!routeStore.isInitAuthRoute) {
|
||||
await routeStore.initAuthRoute()
|
||||
// 动态路由加载完回到根路由
|
||||
if (to.name === '404') {
|
||||
// 等待权限路由加载好了,回到之前的路由,否则404
|
||||
next({
|
||||
path: to.fullPath,
|
||||
replace: true,
|
||||
query: to.query,
|
||||
hash: to.hash,
|
||||
})
|
||||
if (!routeStore.isInitAuthRoute && to.name !== 'login') {
|
||||
try {
|
||||
await routeStore.initAuthRoute()
|
||||
// 动态路由加载完回到根路由
|
||||
if (to.name === 'not-found') {
|
||||
// 等待权限路由加载好了,回到之前的路由,否则404
|
||||
next({
|
||||
path: to.fullPath,
|
||||
replace: true,
|
||||
query: to.query,
|
||||
hash: to.hash,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// 如果路由初始化失败(比如 401 错误),重定向到登录页
|
||||
const redirect = to.fullPath !== '/' ? to.fullPath : undefined
|
||||
next({ path: '/login', query: redirect ? { redirect } : undefined })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,13 @@ export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'root',
|
||||
redirect: '/appRoot',
|
||||
children: [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue'), // 注意这里要带上 文件后缀.vue
|
||||
component: () => import('@/views/build-in/login/index.vue'), // 注意这里要带上 文件后缀.vue
|
||||
meta: {
|
||||
title: '登录',
|
||||
withoutTab: true,
|
||||
@ -21,7 +20,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/public',
|
||||
name: 'publicAccess',
|
||||
component: () => import('@/views/demo/publicAccess/index.vue'),
|
||||
component: () => import('@/views/build-in/public-access/index.vue'),
|
||||
meta: {
|
||||
title: '公共访问示例',
|
||||
requiresAuth: false,
|
||||
@ -29,38 +28,19 @@ export const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: '403',
|
||||
component: () => import('@/views/error/403/index.vue'),
|
||||
meta: {
|
||||
title: '用户无权限',
|
||||
withoutTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
component: () => import('@/views/error/404/index.vue'),
|
||||
path: '/not-found',
|
||||
name: 'not-found',
|
||||
component: () => import('@/views/build-in/not-found/index.vue'),
|
||||
meta: {
|
||||
title: '找不到页面',
|
||||
icon: 'icon-park-outline:ghost',
|
||||
withoutTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: '500',
|
||||
component: () => import('@/views/error/500/index.vue'),
|
||||
meta: {
|
||||
title: '服务器错误',
|
||||
icon: 'icon-park-outline:close-wifi',
|
||||
withoutTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@/views/error/404/index.vue'),
|
||||
name: '404',
|
||||
component: () => import('@/views/build-in/not-found/index.vue'),
|
||||
name: 'not-found',
|
||||
meta: {
|
||||
title: '找不到页面',
|
||||
icon: 'icon-park-outline:ghost',
|
||||
|
@ -19,7 +19,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
pinTab: true,
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/workbench/index.vue',
|
||||
id: 2,
|
||||
id: 101,
|
||||
pid: 1,
|
||||
},
|
||||
{
|
||||
@ -30,7 +30,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:anchor',
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/monitor/index.vue',
|
||||
id: 3,
|
||||
id: 102,
|
||||
pid: 1,
|
||||
},
|
||||
{
|
||||
@ -41,53 +41,53 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 4,
|
||||
id: 2,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'multi2',
|
||||
path: '/multi/multi2',
|
||||
path: '/multi/multi-2',
|
||||
title: '多级菜单子页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi2/index.vue',
|
||||
id: 6,
|
||||
pid: 4,
|
||||
componentPath: '/demo/multi/multi-2/index.vue',
|
||||
id: 201,
|
||||
pid: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi2Detail',
|
||||
path: '/multi/multi2/detail',
|
||||
name: 'multi2-detail',
|
||||
path: '/multi/multi-2/detail',
|
||||
title: '菜单详情页',
|
||||
requiresAuth: false,
|
||||
icon: 'icon-park-outline:list',
|
||||
hide: true,
|
||||
activeMenu: '/multi/multi2',
|
||||
activeMenu: '/multi/multi-2',
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi2/detail/index.vue',
|
||||
id: 7,
|
||||
pid: 4,
|
||||
componentPath: '/demo/multi/multi-2/detail/index.vue',
|
||||
id: 20101,
|
||||
pid: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi3',
|
||||
path: '/multi/multi3',
|
||||
path: '/multi/multi-3',
|
||||
title: '多级菜单',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 8,
|
||||
pid: 4,
|
||||
id: 202,
|
||||
pid: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi4',
|
||||
path: '/multi/multi3/multi4',
|
||||
path: '/multi/multi-3/multi-4',
|
||||
title: '多级菜单3-1',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
componentPath: '/demo/multi/multi3/multi4/index.vue',
|
||||
id: 9,
|
||||
pid: 8,
|
||||
componentPath: '/demo/multi/multi-3/multi-4/index.vue',
|
||||
id: 20201,
|
||||
pid: 202,
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
@ -97,28 +97,38 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:list-two',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 10,
|
||||
id: 3,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'commonList',
|
||||
path: '/list/commonList',
|
||||
path: '/list/common-list',
|
||||
title: '常用列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list-view',
|
||||
componentPath: '/demo/list/commonList/index.vue',
|
||||
id: 11,
|
||||
pid: 10,
|
||||
componentPath: '/demo/list/common-list/index.vue',
|
||||
id: 301,
|
||||
pid: 3,
|
||||
},
|
||||
{
|
||||
name: 'cardList',
|
||||
path: '/list/cardList',
|
||||
path: '/list/card-list',
|
||||
title: '卡片列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:view-grid-list',
|
||||
componentPath: '/demo/list/cardList/index.vue',
|
||||
id: 12,
|
||||
pid: 10,
|
||||
componentPath: '/demo/list/card-list/index.vue',
|
||||
id: 302,
|
||||
pid: 3,
|
||||
},
|
||||
{
|
||||
name: 'draggableList',
|
||||
path: '/list/draggable-list',
|
||||
title: '拖拽列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:menu-fold',
|
||||
componentPath: '/demo/list/draggable-list/index.vue',
|
||||
id: 303,
|
||||
pid: 3,
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
@ -128,7 +138,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:application-one',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 13,
|
||||
id: 4,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
@ -138,8 +148,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:international',
|
||||
componentPath: '/demo/fetch/index.vue',
|
||||
id: 5,
|
||||
pid: 13,
|
||||
id: 401,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'echarts',
|
||||
@ -148,8 +158,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:chart-proportion',
|
||||
componentPath: '/demo/echarts/index.vue',
|
||||
id: 15,
|
||||
pid: 13,
|
||||
id: 402,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'map',
|
||||
@ -159,8 +169,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'carbon:map',
|
||||
keepAlive: true,
|
||||
componentPath: '/demo/map/index.vue',
|
||||
id: 17,
|
||||
pid: 13,
|
||||
id: 403,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'editor',
|
||||
@ -170,8 +180,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:editor',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 18,
|
||||
pid: 13,
|
||||
id: 404,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'editorMd',
|
||||
@ -180,8 +190,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'ri:markdown-line',
|
||||
componentPath: '/demo/editor/md/index.vue',
|
||||
id: 19,
|
||||
pid: 18,
|
||||
id: 40401,
|
||||
pid: 404,
|
||||
},
|
||||
{
|
||||
name: 'editorRich',
|
||||
@ -190,8 +200,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:edit-one',
|
||||
componentPath: '/demo/editor/rich/index.vue',
|
||||
id: 20,
|
||||
pid: 18,
|
||||
id: 40402,
|
||||
pid: 404,
|
||||
},
|
||||
{
|
||||
name: 'clipboard',
|
||||
@ -200,8 +210,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:clipboard',
|
||||
componentPath: '/demo/clipboard/index.vue',
|
||||
id: 21,
|
||||
pid: 13,
|
||||
id: 405,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'icons',
|
||||
@ -210,18 +220,38 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'local:cool',
|
||||
componentPath: '/demo/icons/index.vue',
|
||||
id: 22,
|
||||
pid: 13,
|
||||
id: 406,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'QRCode',
|
||||
path: '/demo/QRCode',
|
||||
path: '/demo/qr-code',
|
||||
title: '二维码',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:two-dimensional-code',
|
||||
componentPath: '/demo/QRCode/index.vue',
|
||||
id: 23,
|
||||
pid: 13,
|
||||
componentPath: '/demo/qr-code/index.vue',
|
||||
id: 407,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'cascader',
|
||||
path: '/demo/cascader',
|
||||
title: '省市区联动',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:add-subset',
|
||||
componentPath: '/demo/cascader/index.vue',
|
||||
id: 408,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'dict',
|
||||
path: '/demo/dict',
|
||||
title: '字典示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/demo/dict/index.vue',
|
||||
id: 409,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
@ -231,7 +261,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:file-doc',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 24,
|
||||
id: 5,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
@ -241,8 +271,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vue',
|
||||
componentPath: '/demo/documents/vue/index.vue',
|
||||
id: 25,
|
||||
pid: 24,
|
||||
id: 501,
|
||||
pid: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsVite',
|
||||
@ -251,169 +281,21 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vitejs',
|
||||
componentPath: '/demo/documents/vite/index.vue',
|
||||
id: 26,
|
||||
pid: 24,
|
||||
id: 502,
|
||||
pid: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsVueuse',
|
||||
path: '/documents/vueuse',
|
||||
path: '/documents/vue-use',
|
||||
title: 'VueUse(外链)',
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vueuse',
|
||||
href: 'https://vueuse.org/guide/',
|
||||
componentPath: 'null',
|
||||
id: 27,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
name: 'permission',
|
||||
path: '/permission',
|
||||
title: '权限',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:people-safe',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 28,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'permissionDemo',
|
||||
path: '/permission/permission',
|
||||
title: '权限示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:right-user',
|
||||
componentPath: '/demo/permission/permission/index.vue',
|
||||
id: 29,
|
||||
pid: 28,
|
||||
},
|
||||
{
|
||||
name: 'justSuper',
|
||||
path: '/permission/justSuper',
|
||||
title: 'super可见',
|
||||
requiresAuth: true,
|
||||
roles: [
|
||||
'super',
|
||||
],
|
||||
icon: 'icon-park-outline:wrong-user',
|
||||
componentPath: '/demo/permission/justSuper/index.vue',
|
||||
id: 30,
|
||||
pid: 28,
|
||||
},
|
||||
{
|
||||
name: 'error',
|
||||
path: '/error',
|
||||
title: '异常页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:error-computer',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 31,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'demo403',
|
||||
path: '/error/403',
|
||||
title: '403',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:error',
|
||||
order: 3,
|
||||
componentPath: '/error/403/index.vue',
|
||||
id: 32,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
name: 'demo404',
|
||||
path: '/error/404',
|
||||
title: '404',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:error',
|
||||
order: 2,
|
||||
componentPath: '/error/404/index.vue',
|
||||
id: 33,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
name: 'demo500',
|
||||
path: '/error/500',
|
||||
title: '500',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:data-error',
|
||||
order: 1,
|
||||
componentPath: '/error/500/index.vue',
|
||||
id: 34,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
name: 'setting',
|
||||
path: '/setting',
|
||||
title: '系统设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:setting',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 35,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'accountSetting',
|
||||
path: '/setting/account',
|
||||
title: '用户设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:every-user',
|
||||
componentPath: '/setting/account/index.vue',
|
||||
id: 36,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
name: 'dictionarySetting',
|
||||
path: '/setting/dictionary',
|
||||
title: '字典设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/setting/dictionary/index.vue',
|
||||
id: 37,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
name: 'menuSetting',
|
||||
path: '/setting/menu',
|
||||
title: '菜单设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:application-menu',
|
||||
componentPath: '/setting/menu/index.vue',
|
||||
id: 38,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
name: 'userCenter',
|
||||
path: '/userCenter',
|
||||
title: '个人中心',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:user-avatar-filled-alt',
|
||||
componentPath: '/demo/userCenter/index.vue',
|
||||
id: 39,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
title: '关于',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:info',
|
||||
componentPath: '/demo/about/index.vue',
|
||||
id: 40,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'cascader',
|
||||
path: '/demo/cascader',
|
||||
title: '省市区联动',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:add-subset',
|
||||
componentPath: '/demo/cascader/index.vue',
|
||||
id: 41,
|
||||
pid: 13,
|
||||
id: 503,
|
||||
pid: 5,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'documentsNova',
|
||||
path: '/documents/nova',
|
||||
@ -422,28 +304,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'local:logo',
|
||||
href: 'https://nova-admin-docs.netlify.app/',
|
||||
componentPath: '2333333',
|
||||
id: 42,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
name: 'dict',
|
||||
path: '/demo/dict',
|
||||
title: '字典示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/demo/dict/index.vue',
|
||||
id: 43,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
name: 'draggableList',
|
||||
path: '/list/draggableList',
|
||||
title: '拖拽列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:menu-fold',
|
||||
componentPath: '/demo/list/draggableList/index.vue',
|
||||
id: 44,
|
||||
pid: 10,
|
||||
id: 504,
|
||||
pid: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsPublic',
|
||||
@ -453,7 +315,104 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'local:logo',
|
||||
href: '/public',
|
||||
componentPath: 'null',
|
||||
id: 45,
|
||||
pid: 24,
|
||||
id: 505,
|
||||
pid: 5,
|
||||
},
|
||||
{
|
||||
name: 'permission',
|
||||
path: '/permission',
|
||||
title: '权限',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:people-safe',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 6,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'permissionDemo',
|
||||
path: '/permission/permission',
|
||||
title: '权限示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:right-user',
|
||||
componentPath: '/demo/permission/permission/index.vue',
|
||||
id: 601,
|
||||
pid: 6,
|
||||
},
|
||||
{
|
||||
name: 'justSuper',
|
||||
path: '/permission/just-super',
|
||||
title: 'super可见',
|
||||
requiresAuth: true,
|
||||
roles: [
|
||||
'super',
|
||||
],
|
||||
icon: 'icon-park-outline:wrong-user',
|
||||
componentPath: '/demo/permission/just-super/index.vue',
|
||||
id: 602,
|
||||
pid: 6,
|
||||
},
|
||||
{
|
||||
name: 'setting',
|
||||
path: '/setting',
|
||||
title: '系统设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:setting',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 7,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'accountSetting',
|
||||
path: '/setting/account',
|
||||
title: '用户设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:every-user',
|
||||
componentPath: '/setting/account/index.vue',
|
||||
id: 701,
|
||||
pid: 7,
|
||||
},
|
||||
{
|
||||
name: 'dictionarySetting',
|
||||
path: '/setting/dictionary',
|
||||
title: '字典设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/setting/dictionary/index.vue',
|
||||
id: 702,
|
||||
pid: 7,
|
||||
},
|
||||
{
|
||||
name: 'menuSetting',
|
||||
path: '/setting/menu',
|
||||
title: '菜单设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:application-menu',
|
||||
componentPath: '/setting/menu/index.vue',
|
||||
id: 703,
|
||||
pid: 7,
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
title: '关于',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:info',
|
||||
componentPath: '/demo/about/index.vue',
|
||||
id: 8,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'userCenter',
|
||||
path: '/user-center',
|
||||
title: '个人中心',
|
||||
requiresAuth: true,
|
||||
hide: true,
|
||||
icon: 'carbon:user-avatar-filled-alt',
|
||||
componentPath: '/build-in/user-center/index.vue',
|
||||
id: 999,
|
||||
pid: null,
|
||||
},
|
||||
|
||||
]
|
||||
|
@ -19,9 +19,11 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
|
||||
// 服务端判定token过期
|
||||
refreshTokenOnSuccess: {
|
||||
// 当服务端返回401时,表示token过期
|
||||
isExpired: (response, method) => {
|
||||
isExpired: async (response, method) => {
|
||||
const res = await response.clone().json()
|
||||
|
||||
const isExpired = method.meta && method.meta.isExpired
|
||||
return response.status === 401 && !isExpired
|
||||
return (response.status === 401 || res.code === 401) && !isExpired
|
||||
},
|
||||
|
||||
// 当token过期时触发,在此函数中触发刷新token
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createAlovaInstance } from './alova'
|
||||
|
||||
export const request = createAlovaInstance({
|
||||
baseURL: __PROXY_MAPPING__.url.path,
|
||||
baseURL: __URL_MAP__.url.path,
|
||||
})
|
||||
|
||||
export const blankInstance = createAlovaInstance({
|
||||
|
@ -17,6 +17,8 @@ const { system, store } = useColorMode({
|
||||
emitAuto: true,
|
||||
})
|
||||
|
||||
const isMobile = useMediaQuery('(max-width: 700px)')
|
||||
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
state: () => {
|
||||
return {
|
||||
@ -38,7 +40,6 @@ export const useAppStore = defineStore('app-store', {
|
||||
showSetting: false,
|
||||
transitionAnimation: 'fade-slide' as TransitionAnimation,
|
||||
layoutMode: 'vertical' as ProLayoutMode,
|
||||
contentFullScreen: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@ -51,6 +52,9 @@ export const useAppStore = defineStore('app-store', {
|
||||
fullScreen() {
|
||||
return isFullscreen.value
|
||||
},
|
||||
isMobile() {
|
||||
return isMobile.value
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 重置所有设置
|
||||
@ -68,8 +72,7 @@ export const useAppStore = defineStore('app-store', {
|
||||
this.showBreadcrumbIcon = true
|
||||
this.showWatermark = false
|
||||
this.transitionAnimation = 'fade-slide'
|
||||
this.layoutMode = 'leftMenu'
|
||||
this.contentFullScreen = false
|
||||
this.layoutMode = 'vertical'
|
||||
|
||||
// 重置所有配色
|
||||
this.setPrimaryColor(this.primaryColor)
|
||||
|
@ -39,23 +39,22 @@ export const useRouteStore = defineStore('route-store', {
|
||||
|
||||
async initRouteInfo() {
|
||||
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
|
||||
const userInfo = local.get('userInfo')
|
||||
try {
|
||||
// Get user's route
|
||||
const result = await fetchUserRoutes({
|
||||
id: 1,
|
||||
})
|
||||
|
||||
if (!userInfo || !userInfo.id) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
return
|
||||
if (!result.isSuccess || !result.data) {
|
||||
throw new Error('Failed to fetch user routes')
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize route info:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Get user's route
|
||||
const { data } = await fetchUserRoutes({
|
||||
id: userInfo.id,
|
||||
})
|
||||
|
||||
if (!data)
|
||||
return
|
||||
|
||||
return data
|
||||
}
|
||||
else {
|
||||
this.rowRoutes = staticRoutes
|
||||
@ -65,25 +64,33 @@ export const useRouteStore = defineStore('route-store', {
|
||||
async initAuthRoute() {
|
||||
this.isInitAuthRoute = false
|
||||
|
||||
// Initialize route information
|
||||
const rowRoutes = await this.initRouteInfo()
|
||||
if (!rowRoutes) {
|
||||
window.$message.error($t(`app.getRouteError`))
|
||||
return
|
||||
try {
|
||||
// Initialize route information
|
||||
const rowRoutes = await this.initRouteInfo()
|
||||
if (!rowRoutes) {
|
||||
const error = new Error('Failed to get route information')
|
||||
window.$message.error($t(`app.getRouteError`))
|
||||
throw error
|
||||
}
|
||||
this.rowRoutes = rowRoutes
|
||||
|
||||
// Generate actual route and insert
|
||||
const routes = createRoutes(rowRoutes)
|
||||
router.addRoute(routes)
|
||||
|
||||
// Generate side menu
|
||||
this.menus = createMenus(rowRoutes)
|
||||
|
||||
// Generate the route cache
|
||||
this.cacheRoutes = generateCacheRoutes(rowRoutes)
|
||||
|
||||
this.isInitAuthRoute = true
|
||||
}
|
||||
catch (error) {
|
||||
// 重置状态并重新抛出错误
|
||||
this.isInitAuthRoute = false
|
||||
throw error
|
||||
}
|
||||
this.rowRoutes = rowRoutes
|
||||
|
||||
// Generate actual route and insert
|
||||
const routes = createRoutes(rowRoutes)
|
||||
router.addRoute(routes)
|
||||
|
||||
// Generate side menu
|
||||
this.menus = createMenus(rowRoutes)
|
||||
|
||||
// Generate the route cache
|
||||
this.cacheRoutes = generateCacheRoutes(rowRoutes)
|
||||
|
||||
this.isInitAuthRoute = true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
3
src/typings/env.d.ts
vendored
3
src/typings/env.d.ts
vendored
@ -1,10 +1,9 @@
|
||||
/**
|
||||
*后台服务的环境类型
|
||||
* - dev: 后台开发环境
|
||||
* - test: 后台测试环境
|
||||
* - prod: 后台生产环境
|
||||
*/
|
||||
type ServiceEnvType = 'dev' | 'test' | 'prod'
|
||||
type ServiceEnvType = 'dev' | 'production'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 项目基本地址 */
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Login, Register, ResetPwd } from './components'
|
||||
|
||||
type IformType = 'login' | 'register' | 'resetPwd'
|
||||
type IformType = 'login' | 'register' | 'resetPwd'
|
||||
const formType: Ref<IformType> = ref('login')
|
||||
const formComponets = {
|
||||
login: Login,
|
||||
@ -18,7 +18,7 @@ const appName = import.meta.env.VITE_APP_NAME
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
</div>
|
||||
<n-el
|
||||
<div
|
||||
class="p-4xl h-full w-full sm:w-450px sm:h-unset"
|
||||
style="background: var(--card-color);box-shadow: var(--box-shadow-1);"
|
||||
>
|
||||
@ -36,7 +36,7 @@ const appName = import.meta.env.VITE_APP_NAME
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</n-el>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</n-el>
|
19
src/views/build-in/not-found/index.vue
Normal file
19
src/views/build-in/not-found/index.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-center h-full">
|
||||
<img
|
||||
src="@/assets/svg/error-not-found.svg"
|
||||
alt=""
|
||||
class="w-1/3"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
{{ $t('app.backHome') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
import Chart from './components/chart.vue'
|
||||
import Chart2 from './components/chart2.vue'
|
||||
import Chart3 from './components/chart3.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
id: 0,
|
||||
@ -32,218 +35,224 @@ const tableData = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-grid
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="访问量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#de4307"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-chart-histogram />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计访问数</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="下载量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#ffb549"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-chart-graph />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计下载量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="浏览量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#1687a7"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-average />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计浏览量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="注册量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#42218E"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-chart-pie />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计注册量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<n-card content-style="padding: 0;">
|
||||
<n-tabs
|
||||
type="line"
|
||||
size="large"
|
||||
:tabs-padding="20"
|
||||
pane-style="padding: 20px;"
|
||||
>
|
||||
<n-tab-pane name="流量趋势">
|
||||
<Chart />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="访问量趋势">
|
||||
<Chart2 />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<n-card
|
||||
title="访问来源"
|
||||
:segmented="{
|
||||
content: true,
|
||||
}"
|
||||
<n-grid
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
:cols="12"
|
||||
item-responsive
|
||||
responsive="screen"
|
||||
>
|
||||
<!-- 统计卡片 - 移动端每行2个,桌面端每行4个 -->
|
||||
<n-gi span="6 m:3">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<Chart3 />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="16">
|
||||
<n-card
|
||||
title="成交记录"
|
||||
:segmented="{
|
||||
content: true,
|
||||
}"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
quaternary
|
||||
>
|
||||
更多
|
||||
</n-button>
|
||||
</template>
|
||||
<n-table
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
<n-statistic label="访问量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#de4307"
|
||||
size="42"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易名称</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>进度</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in tableData"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.start }}</td>
|
||||
<td>{{ item.end }}</td>
|
||||
<td>{{ item.prograss }}%</td>
|
||||
<td>
|
||||
<n-tag
|
||||
:bordered="false"
|
||||
type="info"
|
||||
>
|
||||
{{ item.status }}
|
||||
</n-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</template>
|
||||
<icon-park-outline-chart-histogram />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计访问数</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="6 m:3">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="下载量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#ffb549"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-chart-graph />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计下载量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="6 m:3">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="浏览量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#1687a7"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-average />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计浏览量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="6 m:3">
|
||||
<n-card>
|
||||
<n-space
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<n-statistic label="注册量">
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="12039"
|
||||
show-separator
|
||||
/>
|
||||
</n-statistic>
|
||||
<n-icon
|
||||
color="#42218E"
|
||||
size="42"
|
||||
>
|
||||
<icon-park-outline-chart-pie />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
<span>累计注册量</span>
|
||||
<span><n-number-animation
|
||||
:from="0"
|
||||
:to="322039"
|
||||
show-separator
|
||||
/></span>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<!-- 图表区域 - 全宽显示 -->
|
||||
<n-gi :span="12">
|
||||
<n-card content-style="padding: 0;">
|
||||
<n-tabs
|
||||
type="line"
|
||||
size="large"
|
||||
:tabs-padding="20"
|
||||
pane-style="padding: 20px;"
|
||||
>
|
||||
<n-tab-pane name="流量趋势">
|
||||
<Chart />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="访问量趋势">
|
||||
<Chart2 />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<style scoped></style>
|
||||
<!-- 访问来源 - 移动端全宽,桌面端1/3宽 -->
|
||||
<n-gi span="12 m:4">
|
||||
<n-card
|
||||
title="访问来源"
|
||||
:segmented="{
|
||||
content: true,
|
||||
}"
|
||||
>
|
||||
<Chart3 />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<!-- 成交记录 - 移动端全宽,桌面端2/3宽 -->
|
||||
<n-gi span="12 m:8">
|
||||
<n-card
|
||||
title="成交记录"
|
||||
:segmented="{
|
||||
content: true,
|
||||
}"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
quaternary
|
||||
>
|
||||
更多
|
||||
</n-button>
|
||||
</template>
|
||||
<n-table
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:scroll-x="appStore.isMobile ? 600 : undefined"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易名称</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>进度</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in tableData"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.start }}</td>
|
||||
<td>{{ item.end }}</td>
|
||||
<td>{{ item.prograss }}%</td>
|
||||
<td>
|
||||
<n-tag
|
||||
:bordered="false"
|
||||
type="info"
|
||||
>
|
||||
{{ item.status }}
|
||||
</n-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
@ -9,94 +9,102 @@ const { userInfo } = useAuthStore()
|
||||
<n-grid
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
:cols="3"
|
||||
item-responsive
|
||||
responsive="screen"
|
||||
>
|
||||
<n-gi :span="16">
|
||||
<!-- 左侧主要内容区 - 移动端全宽,桌面端2/3宽 -->
|
||||
<n-gi span="3 m:2">
|
||||
<n-space
|
||||
vertical
|
||||
:size="16"
|
||||
>
|
||||
<!-- 图表区域 -->
|
||||
<n-card style="--n-padding-left: 0;">
|
||||
<Chart />
|
||||
</n-card>
|
||||
<n-card>
|
||||
<n-grid
|
||||
:x-gap="8"
|
||||
:y-gap="8"
|
||||
>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="活跃用户">
|
||||
<n-number-animation show-separator :from="0" :to="12039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:every-user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="用户">
|
||||
<n-number-animation show-separator :from="0" :to="44039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:preview-open" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="浏览量">
|
||||
<n-number-animation show-separator :from="0" :to="551039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:star" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="收藏数">
|
||||
<n-number-animation show-separator :from="0" :to="7739" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<!-- 统计卡片区域 -->
|
||||
<n-grid
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
:cols="4"
|
||||
item-responsive
|
||||
responsive="screen"
|
||||
>
|
||||
<n-gi span="2 l:1">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="活跃用户">
|
||||
<n-number-animation show-separator :from="0" :to="12039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2 l:1">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:every-user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="用户">
|
||||
<n-number-animation show-separator :from="0" :to="44039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2 l:1">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:preview-open" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="浏览量">
|
||||
<n-number-animation show-separator :from="0" :to="551039" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2 l:1">
|
||||
<n-card>
|
||||
<n-thing>
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<nova-icon :size="26" icon="icon-park-outline:star" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-statistic label="收藏数">
|
||||
<n-number-animation show-separator :from="0" :to="7739" />
|
||||
</n-statistic>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-card title="动态">
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
@ -167,7 +175,9 @@ const { userInfo } = useAuthStore()
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
|
||||
<!-- 右侧边栏 - 移动端全宽,桌面端1/3宽 -->
|
||||
<n-gi span="3 m:1">
|
||||
<n-space
|
||||
vertical
|
||||
:size="16"
|
||||
@ -226,11 +236,14 @@ const { userInfo } = useAuthStore()
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
<!-- 订单和待办统计 -->
|
||||
<n-grid
|
||||
:x-gap="8"
|
||||
:y-gap="8"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
:cols="2"
|
||||
>
|
||||
<n-gi :span="12">
|
||||
<!-- 移动端和桌面端都是每行2个 -->
|
||||
<n-gi :span="1">
|
||||
<n-card>
|
||||
<n-flex vertical align="center">
|
||||
<n-text depth="3">
|
||||
@ -245,7 +258,7 @@ const { userInfo } = useAuthStore()
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-gi :span="1">
|
||||
<n-card>
|
||||
<n-flex vertical align="center">
|
||||
<n-text depth="3">
|
||||
|
@ -122,7 +122,7 @@ function handleResetSearch() {
|
||||
model.value = { ...initialModel }
|
||||
}
|
||||
|
||||
type ModalType = 'add' | 'edit'
|
||||
type ModalType = 'add' | 'edit'
|
||||
const modalType = ref<ModalType>('add')
|
||||
function setModalType(type: ModalType) {
|
||||
modalType.value = type
|
@ -1,7 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ErrorTip type="403" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ErrorTip type="404" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ErrorTip type="500" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,10 +1,10 @@
|
||||
<script setup lang="tsx">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import CopyText from '@/components/custom/CopyText.vue'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchAllRoutes } from '@/service'
|
||||
import { arrayToTree, createIcon } from '@/utils'
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import { renderProCopyableText } from 'pro-naive-ui'
|
||||
import TableModal from './components/TableModal.vue'
|
||||
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
@ -45,11 +45,7 @@ const columns: DataTableColumns<AppRoute.RowRoute> = [
|
||||
{
|
||||
title: '路径',
|
||||
key: 'path',
|
||||
render: (row) => {
|
||||
return (
|
||||
<CopyText value={row.path} />
|
||||
)
|
||||
},
|
||||
render: row => renderProCopyableText(row.path),
|
||||
},
|
||||
{
|
||||
title: '组件路径',
|
||||
|
Loading…
x
Reference in New Issue
Block a user