fix: 教师端样式第一版

This commit is contained in:
陆光LG
2025-08-12 23:36:04 +08:00
parent 5986c032dd
commit f8ac3cc751
59 changed files with 6188 additions and 1691 deletions

2
.env
View File

@@ -2,7 +2,7 @@
VITE_APP_TITLE=万维智学考试通
# 项目本地运行端口号
VITE_PORT=80
VITE_PORT=5173
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN=true

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -136,7 +136,7 @@
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
<img src="/logo.png" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title">%VITE_APP_TITLE%</div>
</div>
<div class="app-loading-item">

View File

@@ -5,6 +5,8 @@
"author": "xingyu",
"private": false,
"scripts": {
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"i": "pnpm install",
"dev": "vite --mode env.local",
"dev-server": "vite --mode dev",
@@ -30,6 +32,7 @@
"@form-create/element-ui": "^3.2.11",
"@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tauri-apps/api": "^2.7.0",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23",
@@ -87,6 +90,7 @@
"@iconify/json": "^2.2.187",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@purge-icons/generated": "^0.9.0",
"@tauri-apps/cli": "^2.7.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.21",
"@types/nprogress": "^0.2.3",

129
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/api':
specifier: ^2.7.0
version: 2.7.0
'@videojs-player/vue':
specifier: ^1.0.0
version: 1.0.0(@types/video.js@7.3.58)(video.js@7.21.6)(vue@3.5.12(typescript@5.3.3))
@@ -189,6 +192,9 @@ importers:
'@purge-icons/generated':
specifier: ^0.9.0
version: 0.9.0
'@tauri-apps/cli':
specifier: ^2.7.1
version: 2.7.1
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -1693,6 +1699,80 @@ packages:
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
'@tauri-apps/api@2.7.0':
resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==}
'@tauri-apps/cli-darwin-arm64@2.7.1':
resolution: {integrity: sha512-j2NXQN6+08G03xYiyKDKqbCV2Txt+hUKg0a8hYr92AmoCU8fgCjHyva/p16lGFGUG3P2Yu0xiNe1hXL9ZuRMzA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.7.1':
resolution: {integrity: sha512-CdYAefeM35zKsc91qIyKzbaO7FhzTyWKsE8hj7tEJ1INYpoh1NeNNyL/NSEA3Nebi5ilugioJ5tRK8ZXG8y3gw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.7.1':
resolution: {integrity: sha512-dnvyJrTA1UJxJjQ8q1N/gWomjP8Twij1BUQu2fdcT3OPpqlrbOk5R1yT0oD/721xoKNjroB5BXCsmmlykllxNg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.7.1':
resolution: {integrity: sha512-FtBW6LJPNRTws3qyUc294AqCWU91l/H0SsFKq6q4Q45MSS4x6wxLxou8zB53tLDGEPx3JSoPLcDaSfPlSbyujQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-arm64-musl@2.7.1':
resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-riscv64-gnu@2.7.1':
resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@tauri-apps/cli-linux-x64-gnu@2.7.1':
resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-linux-x64-musl@2.7.1':
resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@2.7.1':
resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.7.1':
resolution: {integrity: sha512-1oeibfyWQPVcijOrTg709qhbXArjX3x1MPjrmA5anlygwrbByxLBcLXvotcOeULFcnH2FYUMMLLant8kgvwE5A==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.7.1':
resolution: {integrity: sha512-D7Q9kDObutuirCNLxYQ7KAg2Xxg99AjcdYz/KuMw5HvyEPbkC9Q7JL0vOrQOrHEHxIQ2lYzFOZvKKoC2yyqXcg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.7.1':
resolution: {integrity: sha512-RcGWR4jOUEl92w3uvI0h61Llkfj9lwGD1iwvDRD2isMrDhOzjeeeVn9aGzeW1jubQ/kAbMYfydcA4BA0Cy733Q==}
engines: {node: '>= 10'}
hasBin: true
'@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==, tarball: https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz}
@@ -6721,6 +6801,55 @@ snapshots:
'@sxzz/popperjs-es@2.11.7': {}
'@tauri-apps/api@2.7.0': {}
'@tauri-apps/cli-darwin-arm64@2.7.1':
optional: true
'@tauri-apps/cli-darwin-x64@2.7.1':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.7.1':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.7.1':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.7.1':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.7.1':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.7.1':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.7.1':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.7.1':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.7.1':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.7.1':
optional: true
'@tauri-apps/cli@2.7.1':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.7.1
'@tauri-apps/cli-darwin-x64': 2.7.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.7.1
'@tauri-apps/cli-linux-arm64-gnu': 2.7.1
'@tauri-apps/cli-linux-arm64-musl': 2.7.1
'@tauri-apps/cli-linux-riscv64-gnu': 2.7.1
'@tauri-apps/cli-linux-x64-gnu': 2.7.1
'@tauri-apps/cli-linux-x64-musl': 2.7.1
'@tauri-apps/cli-win32-arm64-msvc': 2.7.1
'@tauri-apps/cli-win32-ia32-msvc': 2.7.1
'@tauri-apps/cli-win32-x64-msvc': 2.7.1
'@transloadit/prettier-bytes@0.0.7': {}
'@trysound/sax@0.2.0': {}

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

4
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

4957
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.3.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.7.0", features = [] }
tauri-plugin-log = "2"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,33 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-start-dragging",
"core:app:allow-app-hide",
"core:app:allow-app-show",
"core:window:allow-set-size",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window",
"core:webview:allow-webview-hide",
"core:webview:allow-webview-show",
"core:webview:allow-webview-close",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-show",
"core:window:allow-set-title",
"core:window:allow-set-fullscreen",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:resources:allow-close",
"core:resources:default",
"core:window:allow-set-position",
"core:window:allow-set-always-on-top",
"core:window:allow-set-always-on-bottom",
"core:window:allow-unmaximize",
"core:window:allow-unminimize"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

16
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,16 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

38
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "teacher-end",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build"
},
"app": {
"windows": [
{
"title": "teacher-end",
"width": 1280,
"height": 720,
"resizable": true,
"fullscreen": false,
"decorations": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@@ -0,0 +1,37 @@
<template>
<el-button
:class="buttonClass"
:size="size"
:type="type"
:icon="icon || 'ep:home-filled'"
@click="goToDashboard"
>
{{ text || '返回桌面' }}
</el-button>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
interface Props {
text?: string
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'
size?: 'large' | 'default' | 'small'
icon?: string
buttonClass?: string
}
withDefaults(defineProps<Props>(), {
text: '返回桌面',
type: 'primary',
size: 'default',
icon: 'ep:home-filled',
buttonClass: ''
})
const router = useRouter()
const goToDashboard = () => {
router.push('/dashboard')
}
</script>

View File

@@ -0,0 +1,3 @@
import BackToDashboard from './BackToDashboard.vue'
export { BackToDashboard }

View File

@@ -171,7 +171,7 @@
/>
<div class="flex flex-col">
<el-text>手机扫码预览</el-text>
<Qrcode :text="previewUrl" logo="/logo.gif" />
<Qrcode :text="previewUrl" logo="/logo.png" />
</div>
</div>
</Dialog>

View File

@@ -0,0 +1,79 @@
<template>
<div class="window-controls">
<div @click="goBack" class="control-button">
<el-icon><ArrowLeft /></el-icon>
</div>
<div class="flex-grow"></div>
<div @click="minimizeWindow" class="control-button">
<el-icon><Minus /></el-icon>
</div>
<div @click="toggleMaximizeWindow" class="control-button">
<el-icon><FullScreen /></el-icon>
</div>
<div @click="closeWindow" class="control-button close-button">
<el-icon><Close /></el-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useRouter } from 'vue-router'
import { ArrowLeft, Close, FullScreen, Minus } from '@element-plus/icons-vue'
const router = useRouter()
const appWindow = WebviewWindow.getCurrent()
const minimizeWindow = () => {
appWindow.minimize()
}
const toggleMaximizeWindow = () => {
appWindow.toggleMaximize()
}
const closeWindow = () => {
appWindow.close()
}
const goBack = () => {
router.back()
}
</script>
<style scoped>
.window-controls {
display: flex;
align-items: center;
height: 30px;
-webkit-app-region: drag;
background-color: transparent;
position: fixed;
top: 0;
right: 0;
z-index: 9999;
}
.control-button {
-webkit-app-region: no-drag;
width: 40px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.control-button:hover {
background-color: #f0f0f0;
}
.close-button:hover {
background-color: #e81123;
color: white;
}
.flex-grow {
flex-grow: 1;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="window-controls">
<div @click="minimizeWindow" class="control-btn">-</div>
<div @click="toggleMaximize" class="control-btn">[ ]</div>
<div @click="closeWindow" class="control-btn">x</div>
</div>
</template>
<script setup lang="ts">
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
const thisWindow = WebviewWindow.getCurrent()
const minimizeWindow = () => {
thisWindow.minimize()
}
const toggleMaximize = async () => {
if (await thisWindow.isMaximized()) {
thisWindow.unmaximize()
} else {
thisWindow.maximize()
}
}
const closeWindow = () => {
thisWindow.close()
}
</script>
<style scoped>
.window-controls {
display: flex;
position: fixed;
top: 0;
right: 0;
z-index: 9999;
-webkit-app-region: no-drag;
}
.control-btn {
width: 40px;
height: 30px;
line-height: 30px;
text-align: center;
cursor: pointer;
}
.control-btn:hover {
background-color: #eee;
}
</style>

View File

@@ -1,6 +1,8 @@
import type { App } from 'vue'
import { Icon } from './Icon'
import { BackToDashboard } from './BackToDashboard'
export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon)
app.component('BackToDashboard', BackToDashboard)
}

View File

@@ -1,5 +1,5 @@
<script lang="tsx">
import { computed, defineComponent, unref } from 'vue'
import { computed, defineComponent } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { Backtop } from '@/components/Backtop'
import { Setting } from '@/layout/components/Setting'
@@ -25,22 +25,9 @@ const handleClickOutside = () => {
}
const renderLayout = () => {
switch (unref(layout)) {
case 'classic':
const { renderClassic } = useRenderLayout()
return renderClassic()
case 'topLeft':
const { renderTopLeft } = useRenderLayout()
return renderTopLeft()
case 'top':
const { renderTop } = useRenderLayout()
return renderTop()
case 'cutMenu':
const { renderCutMenu } = useRenderLayout()
return renderCutMenu()
default:
break
}
// 强制使用 dashboard 布局,移除所有左侧菜单相关的布局
const { renderDashboard } = useRenderLayout()
return renderDashboard()
}
export default defineComponent({

View File

@@ -0,0 +1,3 @@
import Exit from './src/Exit.vue'
export { Exit }

View File

@@ -0,0 +1,44 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { CloseBold } from '@element-plus/icons-vue'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { useTagsViewStore } from '@/store/modules/tagsView'
defineOptions({ name: 'ScreenFull' })
const { getPrefixCls } = useDesign()
const tagsViewStore = useTagsViewStore()
const userStore = useUserStore()
const prefixCls = getPrefixCls('screenfull')
defineProps({
color: propTypes.string.def('')
})
const toggleExit = () => {
ElMessageBox.confirm('确定要退出应用吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
cancelButtonClass: 'el-button--danger',
confirmButtonClass: 'el-button--success',
closeOnClickModal: false
})
.then(async () => {
const thisWindow = await WebviewWindow.getCurrent()
await userStore.loginOut()
tagsViewStore.delAllViews()
thisWindow.close()
})
.catch(() => {
// Handle cancel action if needed
})
}
</script>
<template>
<div :class="prefixCls" @click="toggleExit">
<el-icon><CloseBold /></el-icon>
</div>
</template>

View File

@@ -99,7 +99,7 @@ export default defineComponent({
>
{{
default: () => {
const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
const { renderMenuItem } = useRenderMenuItem()
return renderMenuItem(unref(routers))
}
}}

View File

@@ -0,0 +1,3 @@
import Minimize from './src/Minimize.vue'
export { Minimize }

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { SemiSelect } from '@element-plus/icons-vue'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
defineOptions({ name: 'ScreenFull' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('screenfull')
defineProps({
color: propTypes.string.def('')
})
const toggleMinimize = async () => {
const thisWindow = await WebviewWindow.getCurrent();
thisWindow.minimize();
}
</script>
<template>
<div :class="prefixCls" @click="toggleMinimize">
<el-icon><SemiSelect /></el-icon>
</div>
</template>

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { useClipboard, useCssVar } from '@vueuse/core'
import { useCssVar } from '@vueuse/core'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useDesign } from '@/hooks/web/useDesign'
@@ -10,8 +9,6 @@ import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
defineOptions({ name: 'Setting' })
@@ -55,6 +52,14 @@ const setHeaderTheme = (color: string) => {
}
}
// 标签页显示控制
const tagsViewEnabled = ref(appStore.getTagsView)
const setTagsView = (enabled: boolean) => {
appStore.setTagsView(enabled)
tagsViewEnabled.value = enabled
}
// 菜单主题相关
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
@@ -106,89 +111,6 @@ watch(
}
)
// 拷贝
const copyConfig = async () => {
const { copy, copied, isSupported } = useClipboard({
source: `
// 面包屑
breadcrumb: ${appStore.getBreadcrumb},
// 面包屑图标
breadcrumbIcon: ${appStore.getBreadcrumbIcon},
// 折叠图标
hamburger: ${appStore.getHamburger},
// 全屏图标
screenfull: ${appStore.getScreenfull},
// 尺寸图标
size: ${appStore.getSize},
// 多语言图标
locale: ${appStore.getLocale},
// 消息图标
message: ${appStore.getMessage},
// 标签页
tagsView: ${appStore.getTagsView},
// 标签页
tagsViewImmerse: ${appStore.getTagsViewImmerse},
// 标签页图标
tagsViewIcon: ${appStore.getTagsViewIcon},
// logo
logo: ${appStore.getLogo},
// 菜单手风琴
uniqueOpened: ${appStore.getUniqueOpened},
// 固定header
fixedHeader: ${appStore.getFixedHeader},
// 页脚
footer: ${appStore.getFooter},
// 灰色模式
greyMode: ${appStore.getGreyMode},
// layout布局
layout: '${appStore.getLayout}',
// 暗黑模式
isDark: ${appStore.getIsDark},
// 组件尺寸
currentSize: '${appStore.getCurrentSize}',
// 主题相关
theme: {
// 主题色
elColorPrimary: '${appStore.getTheme.elColorPrimary}',
// 左侧菜单边框颜色
leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
// 左侧菜单背景颜色
leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
// 左侧菜单字体颜色
leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
// logo字体颜色
logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
// logo边框颜色
logoBorderColor: '${appStore.getTheme.logoBorderColor}',
// 头部背景颜色
topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
// 头部字体颜色
topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
// 头部悬停颜色
topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
// 头部边框颜色
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
}
`
})
if (!isSupported) {
ElMessage.error(t('setting.copyFailed'))
} else {
await copy()
if (unref(copied)) {
ElMessage.success(t('setting.copySuccess'))
}
}
}
// 清空缓存
const clear = () => {
const { wsCache } = useCache()
@@ -197,31 +119,39 @@ const clear = () => {
wsCache.delete(CACHE_KEY.IS_DARK)
window.location.reload()
}
// 开放给外部组件调用
const openSettings = () => {
drawer.value = true
}
defineExpose({
openSettings
})
</script>
<template>
<div
:class="prefixCls"
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
@click="drawer = true"
>
<Icon color="#fff" icon="ep:setting" />
<!-- 隐藏浮动按钮改为通过个人中心菜单访问 -->
<div style="display: none">
<div
:class="prefixCls"
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
@click="drawer = true"
>
<Icon color="#fff" icon="ep:setting" />
</div>
</div>
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px" class="setting-drawer">
<template #header>
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
<span class="text-16px font-700">主题修改</span>
</template>
<div class="text-center">
<div class="text-center setting-content">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>
<LayoutRadioPicker />
<!-- 系统主题 -->
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
<ColorRadioPicker
@@ -256,38 +186,17 @@ const clear = () => {
@change="setHeaderTheme"
/>
<!-- 菜单主题 -->
<template v-if="layout !== 'top'">
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</template>
<!-- 标签页设置 -->
<ElDivider>标签页设置</ElDivider>
<div class="flex items-center justify-between px-4 py-2">
<span class="text-14px">显示标签页</span>
<ElSwitch v-model="tagsViewEnabled" @change="setTagsView" />
</div>
</div>
<!-- 界面显示 -->
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
<InterfaceDisplay />
<ElDivider />
<div>
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
</div>
<div class="mt-5px">
<ElButton class="w-full" type="danger" @click="clear">
{{ t('setting.clearAndReset') }}
</ElButton>
<ElButton class="w-full" type="danger" @click="clear"> 重置 </ElButton>
</div>
</ElDrawer>
</template>
@@ -297,6 +206,26 @@ $prefix-cls: #{$namespace}-setting;
.#{$prefix-cls} {
border-radius: 6px 0 0 6px;
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
z-index: 1200; /*修正没有z-index会被表格层覆盖,值不要超过4000*/
}
.setting-content {
max-height: 70vh;
overflow-y: auto;
}
</style>
<style lang="scss">
.setting-drawer {
.el-drawer {
max-height: 60vh;
top: 20vh;
border-radius: 8px 0 0 8px;
}
.el-drawer__body {
padding: 16px;
overflow-y: auto;
}
}
</style>

View File

@@ -2,33 +2,15 @@
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { useWatermark } from '@/hooks/web/useWatermark'
import { useAppStore } from '@/store/modules/app'
defineOptions({ name: 'InterfaceDisplay' })
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const { setWatermark } = useWatermark()
const prefixCls = getPrefixCls('interface-display')
const appStore = useAppStore()
const water = ref()
// 面包屑
const breadcrumb = ref(appStore.getBreadcrumb)
const breadcrumbChange = (show: boolean) => {
appStore.setBreadcrumb(show)
}
// 面包屑图标
const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
const breadcrumbIconChange = (show: boolean) => {
appStore.setBreadcrumbIcon(show)
}
// 折叠图标
const hamburger = ref(appStore.getHamburger)
@@ -43,27 +25,6 @@ const screenfullChange = (show: boolean) => {
appStore.setScreenfull(show)
}
// 尺寸图标
const size = ref(appStore.getSize)
const sizeChange = (show: boolean) => {
appStore.setSize(show)
}
// 多语言图标
const locale = ref(appStore.getLocale)
const localeChange = (show: boolean) => {
appStore.setLocale(show)
}
// 消息图标
const message = ref(appStore.getMessage)
const messageChange = (show: boolean) => {
appStore.setMessage(show)
}
// 标签页
const tagsView = ref(appStore.getTagsView)
@@ -73,20 +34,6 @@ const tagsViewChange = (show: boolean) => {
appStore.setTagsView(show)
}
// 标签页沉浸
const tagsViewImmerse = ref(appStore.getTagsViewImmerse)
const tagsViewImmerseChange = (immerse: boolean) => {
appStore.setTagsViewImmerse(immerse)
}
// 标签页图标
const tagsViewIcon = ref(appStore.getTagsViewIcon)
const tagsViewIconChange = (show: boolean) => {
appStore.setTagsViewIcon(show)
}
// logo
const logo = ref(appStore.getLogo)
@@ -94,25 +41,13 @@ const logoChange = (show: boolean) => {
appStore.setLogo(show)
}
// 菜单手风琴
const uniqueOpened = ref(appStore.getUniqueOpened)
const uniqueOpenedChange = (uniqueOpened: boolean) => {
appStore.setUniqueOpened(uniqueOpened)
}
// 固定头部
const fixedHeader = ref(appStore.getFixedHeader)
const fixedHeader = ref(true) // 始终固定头部
const fixedHeaderChange = (show: boolean) => {
appStore.setFixedHeader(show)
}
// 页脚
const footer = ref(appStore.getFooter)
const footerChange = (show: boolean) => {
appStore.setFooter(show)
const fixedHeaderChange = () => {
// 确保始终为true
appStore.setFixedHeader(true)
fixedHeader.value = true
}
// 灰色模式
@@ -122,18 +57,6 @@ const greyModeChange = (show: boolean) => {
appStore.setGreyMode(show)
}
// 固定菜单
const fixedMenu = ref(appStore.getFixedMenu)
const fixedMenuChange = (show: boolean) => {
appStore.setFixedMenu(show)
}
// 设置水印
const setWater = () => {
setWatermark(water.value)
}
const layout = computed(() => appStore.getLayout)
watch(
@@ -148,16 +71,6 @@ watch(
<template>
<div :class="prefixCls">
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
<ElSwitch v-model="hamburger" @change="hamburgerChange" />
@@ -168,69 +81,24 @@ watch(
<ElSwitch v-model="screenfull" @change="screenfullChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
<ElSwitch v-model="size" @change="sizeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.localeIcon') }}</span>
<ElSwitch v-model="locale" @change="localeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.messageIcon') }}</span>
<ElSwitch v-model="message" @change="messageChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsView') }}</span>
<ElSwitch v-model="tagsView" @change="tagsViewChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsViewImmerse') }}</span>
<ElSwitch v-model="tagsViewImmerse" @change="tagsViewImmerseChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.logo') }}</span>
<ElSwitch v-model="logo" @change="logoChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.footer') }}</span>
<ElSwitch v-model="footer" @change="footerChange" />
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" :disabled="true" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.greyMode') }}</span>
<ElSwitch v-model="greyMode" @change="greyModeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('watermark.watermark') }}</span>
<ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
</div>
</div>
</template>

View File

@@ -1,46 +1,23 @@
<script lang="tsx">
import { defineComponent, computed } from 'vue'
import { Message } from '@/layout/components//Message'
import { Collapse } from '@/layout/components/Collapse'
import { UserInfo } from '@/layout/components/UserInfo'
import { Screenfull } from '@/layout/components/Screenfull'
import { Breadcrumb } from '@/layout/components/Breadcrumb'
import { SizeDropdown } from '@/layout/components/SizeDropdown'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue'
import Minimize from '@/layout/components/Minimize/src/Minimize.vue'
import Exit from '@/layout/components/Exit/src/Exit.vue'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('tool-header')
const appStore = useAppStore()
// 面包屑
const breadcrumb = computed(() => appStore.getBreadcrumb)
// 折叠图标
const hamburger = computed(() => appStore.getHamburger)
// 全屏图标
const screenfull = computed(() => appStore.getScreenfull)
// 搜索图片
const search = computed(() => appStore.search)
// 尺寸图标
const size = computed(() => appStore.getSize)
// 布局
const layout = computed(() => appStore.getLayout)
// 多语言图标
const locale = computed(() => appStore.getLocale)
// 消息图标
const message = computed(() => appStore.getMessage)
export default defineComponent({
name: 'ToolHeader',
setup() {
@@ -50,35 +27,21 @@ export default defineComponent({
class={[
prefixCls,
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between',
'bg-[var(--top-header-bg-color)] text-[var(--top-header-text-color)]',
'dark:bg-[var(--el-bg-color)]'
]}
data-tauri-drag-region
>
{layout.value !== 'top' ? (
<div class="h-full flex items-center">
{hamburger.value && layout.value !== 'cutMenu' ? (
<Collapse class="custom-hover" color="var(--top-header-text-color)"></Collapse>
) : undefined}
{breadcrumb.value ? <Breadcrumb class="lt-md:hidden"></Breadcrumb> : undefined}
</div>
<div class="h-full flex items-center">{/* 左侧预留区域 */}</div>
) : undefined}
<div class="h-full flex items-center">
<UserInfo></UserInfo>
{screenfull.value ? (
<Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
) : undefined}
{search.value ? <RouterSearch isModal={false} /> : undefined}
{size.value ? (
<SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
) : undefined}
{locale.value ? (
<LocaleDropdown
class="custom-hover"
color="var(--top-header-text-color)"
></LocaleDropdown>
) : undefined}
{message.value ? (
<Message class="custom-hover" color="var(--top-header-text-color)"></Message>
) : undefined}
<UserInfo></UserInfo>
{<Minimize class="custom-hover" color="var(--top-header-text-color)"></Minimize>}
{<Exit class="custom-hover-exit" color="var(--top-header-text-color)"></Exit>}
</div>
</div>
)
@@ -91,5 +54,7 @@ $prefix-cls: #{$namespace}-tool-header;
.#{$prefix-cls} {
transition: left var(--transition-time-02);
// 移除硬编码的背景色使用CSS变量控制
// background-color: #0071c5;
}
</style>

View File

@@ -8,6 +8,7 @@ import { useUserStore } from '@/store/modules/user'
import LockDialog from './components/LockDialog.vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock'
import { Setting } from '@/layout/components/Setting'
defineOptions({ name: 'UserInfo' })
@@ -49,8 +50,13 @@ const loginOut = async () => {
const toProfile = async () => {
push('/user/profile')
}
const toDocument = () => {
window.open('https://doc.iocoder.cn/')
// Setting组件引用
const settingRef = ref()
// 打开主题设置
const openThemeSettings = () => {
settingRef.value?.openSettings()
}
</script>
@@ -64,18 +70,25 @@ const toDocument = () => {
</div>
<template #dropdown>
<ElDropdownMenu>
<!-- 个人中心 -->
<ElDropdownItem>
<Icon icon="ep:tools" />
<div @click="toProfile">{{ t('common.profile') }}</div>
</ElDropdownItem>
<!-- <ElDropdownItem>-->
<!-- <Icon icon="ep:menu" />-->
<!-- <div @click="toDocument">{{ t('common.document') }}</div>-->
<!-- </ElDropdownItem>-->
<!-- 主题修改 -->
<ElDropdownItem>
<Icon icon="ep:setting" />
<div @click="openThemeSettings">主题修改</div>
</ElDropdownItem>
<!-- 锁定屏幕 -->
<ElDropdownItem divided>
<Icon icon="ep:lock" />
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<!-- 退出系统 -->
<ElDropdownItem divided @click="loginOut">
<Icon icon="ep:switch-button" />
<div>{{ t('common.loginOut') }}</div>
@@ -86,6 +99,9 @@ const toDocument = () => {
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
<!-- Setting组件 -->
<Setting ref="settingRef" />
<teleport to="body">
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />

View File

@@ -1,13 +1,12 @@
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { Menu } from '@/layout/components/Menu'
import { TabMenu } from '@/layout/components/TabMenu'
import { TagsView } from '@/layout/components/TagsView'
import { Logo } from '@/layout/components/Logo'
import AppView from './AppView.vue'
import ToolHeader from './ToolHeader.vue'
import { ElScrollbar } from 'element-plus'
import { ElScrollbar, ElButton } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@/components/Icon'
import { useRouter, useRoute } from 'vue-router'
const { getPrefixCls } = useDesign()
@@ -20,95 +19,71 @@ const pageLoading = computed(() => appStore.getPageLoading)
// 标签页
const tagsView = computed(() => appStore.getTagsView)
// 菜单折叠
const collapse = computed(() => appStore.getCollapse)
// logo
const logo = computed(() => appStore.logo)
// 固定头部
const fixedHeader = computed(() => appStore.getFixedHeader)
// 是否是移动端
const mobile = computed(() => appStore.getMobile)
// 固定菜单
const fixedMenu = computed(() => appStore.getFixedMenu)
export const useRenderLayout = () => {
const renderClassic = () => {
const router = useRouter()
const route = useRoute()
// 检查是否在主页
const isDashboardPage = computed(() => {
return route.path === '/dashboard' || route.path === '/'
})
const goToDashboard = () => {
router.push('/dashboard')
}
const renderDashboard = () => {
return (
<>
<div
class={[
'absolute top-0 left-0 h-full layout-border__right',
{ '!fixed z-3000': mobile.value }
]}
>
{logo.value ? (
<Logo
class={[
'bg-[var(--left-menu-bg-color)] relative',
{
'!pl-0': mobile.value && collapse.value,
'w-[var(--left-menu-min-width)]': appStore.getCollapse,
'w-[var(--left-menu-max-width)]': !appStore.getCollapse
}
]}
style="transition: all var(--transition-time-02);"
></Logo>
) : undefined}
<Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
</div>
<div
class={[
`${prefixCls}-content`,
'absolute top-0 h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value && !mobile.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value && !mobile.value && !mobile.value,
'fixed !w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<div class={[`${prefixCls}-content`, 'w-full h-[100%]']}>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
fixedHeader.value
fixedHeader.value && tagsView.value,
'!h-[calc(100%-var(--top-tool-height))] mt-[var(--top-tool-height)]':
fixedHeader.value && !tagsView.value
}
]}
>
<div
class={[
{
'fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
collapse.value && fixedHeader.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
!collapse.value && fixedHeader.value && !mobile.value,
'!w-full !left-0': mobile.value
'fixed top-0 left-0 z-10 w-full': fixedHeader.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ToolHeader
class={[
'bg-[var(--top-header-bg-color)]',
{
'layout-border__bottom': !tagsView.value
}
]}
></ToolHeader>
<div class="flex items-center bg-[var(--top-header-bg-color)] h-[var(--top-tool-height)] px-4">
{/* Logo区域 */}
<div class="flex items-center mr-auto">
<img src="/logo.png" alt="Logo" class="h-8 w-8 mr-3" />
<span class="text-lg font-semibold text-[var(--top-header-text-color)]">
</span>
</div>
{/* 工具栏 */}
<ToolHeader class="flex-shrink-0"></ToolHeader>
</div>
{tagsView.value ? (
<TagsView class="layout-border__top layout-border__bottom"></TagsView>
) : undefined}
) : !isDashboardPage.value ? (
<div class="flex items-center h-[var(--tags-view-height)] bg-[var(--top-header-bg-color)] layout-border__top layout-border__bottom px-4">
<ElButton
type="primary"
onClick={goToDashboard}
>
<Icon icon="ep:home-filled" class="mr-1" />
</ElButton>
</div>
) : null}
</div>
<AppView></AppView>
@@ -118,177 +93,10 @@ export const useRenderLayout = () => {
)
}
const renderTopLeft = () => {
return (
<>
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom dark:bg-[var(--el-bg-color)]">
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
<div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
<Menu class="relative layout-border__right !h-full"></Menu>
<div
class={[
`${prefixCls}-content`,
'h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value && tagsView.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'layout-border__bottom absolute',
{
'!fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</div>
</>
)
}
const renderTop = () => {
return (
<>
<div
class={[
'flex items-center justify-between bg-[var(--top-header-bg-color)] relative',
{
'layout-border__bottom': !tagsView.value
}
]}
>
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
<Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
<ToolHeader></ToolHeader>
</div>
<div class={[`${prefixCls}-content`, 'w-full h-[calc(100%-var(--top-tool-height))]']}>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value && tagsView.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'layout-border__bottom layout-border__top relative',
{
'!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</>
)
}
const renderCutMenu = () => {
return (
<>
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">
{logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
<div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
<TabMenu></TabMenu>
<div
class={[
`${prefixCls}-content`,
'h-[100%]',
{
'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
!collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
collapse.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
!collapse.value && fixedMenu.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value && tagsView.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'relative layout-border__bottom',
{
'!fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-min-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-max-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value && fixedMenu.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</div>
</>
)
}
return {
renderClassic,
renderTopLeft,
renderTop,
renderCutMenu
renderDashboard
}
}
// 默认导出以兼容 auto-components.d.ts
export default useRenderLayout

View File

@@ -8,6 +8,10 @@ import { usePageLoading } from '@/hooks/web/usePageLoading'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useUserStoreWithOut } from '@/store/modules/user'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
import { useAppStoreWithOut } from '@/store/modules/app'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache()
const { start, done } = useNProgress()
@@ -68,6 +72,23 @@ router.beforeEach(async (to, from, next) => {
const dictStore = useDictStoreWithOut()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut()
// 根据路由自动切换布局 - 只有当前路径是dashboard时才使用dashboard布局
if (to.path === '/dashboard' || to.path === '/') {
appStore.setLayout('dashboard')
appStore.setFixedHeader(true) // 设置header固定
} else {
// 获取当前保存的布局设置如果不是dashboard就恢复到之前的设置
const currentLayout = appStore.getLayout
if (currentLayout === 'dashboard') {
// 恢复到默认布局或从缓存中获取之前的布局
const cachedLayout = wsCache.get(CACHE_KEY.LAYOUT) || 'classic'
appStore.setLayout(cachedLayout === 'dashboard' ? 'classic' : cachedLayout)
appStore.setFixedHeader(true) // 保持header固定
}
}
if (!dictStore.getIsSetDict) {
await dictStore.setDictMap()
}
@@ -103,4 +124,18 @@ router.afterEach((to) => {
useTitle(to?.meta?.title as string)
done() // 结束Progress
loadDone()
// 路由切换后的布局处理
const appStore = useAppStoreWithOut()
if (to.path === '/dashboard' || to.path === '/') {
appStore.setLayout('dashboard')
appStore.setFixedHeader(true) // 设置header固定
} else {
const currentLayout = appStore.getLayout
if (currentLayout === 'dashboard') {
// 恢复到默认布局
appStore.setLayout('classic')
appStore.setFixedHeader(true) // 保持header固定
}
}
})

View File

@@ -1,11 +1,11 @@
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import remainingRouter from './modules/remaining'
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#createWebHistory URL不带#
history: createWebHashHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#createWebHistory URL不带#
strict: true,
routes: remainingRouter as RouteRecordRaw[],
scrollBehavior: () => ({ left: 0, top: 0 })

View File

@@ -53,14 +53,14 @@ const remainingRouter: AppRouteRecordRaw[] = [
{
path: '/',
component: Layout,
redirect: '/index',
redirect: '/dashboard',
name: 'Home',
meta: {},
children: [
{
path: 'index',
component: () => import('@/views/Home/Index.vue'),
name: 'Index',
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: {
title: t('router.home'),
icon: 'ep:home-filled',

View File

@@ -48,57 +48,57 @@ export const useAppStore = defineStore('app', {
title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: false, // 路由跳转loading
breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单
breadcrumb: false, // 面包屑
breadcrumbIcon: false, // 面包屑图标
collapse: false, // 折叠菜单 - 不需要折叠功能
uniqueOpened: true, // 是否只保持一个子菜单的展开
hamburger: true, // 折叠图标
hamburger: false, // 折叠图标 - 删除折叠图标
screenfull: true, // 全屏图标
search: true, // 搜索图标
size: true, // 尺寸图标
locale: true, // 多语言图标
message: true, // 消息图标
tagsView: true, // 标签页
search: false, // 搜索图标
size: false, // 尺寸图标
locale: false, // 多语言图标
message: false, // 消息图标
tagsView: false, // 标签页
tagsViewImmerse: false, // 标签页沉浸
tagsViewIcon: true, // 是否显示标签图标
logo: true, // logo
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
logo: false, // logo - 删除默认logo自定义显示
fixedHeader: true, // 固定toolheader - 确保header固定
footer: false, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日 - 删除灰色模式
fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
layout: wsCache.get(CACHE_KEY.LAYOUT) || 'dashboard', // layout布局 - 改为dashboard
isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
currentSize: wsCache.get('default') || 'default', // 组件尺寸
theme: wsCache.get(CACHE_KEY.THEME) || {
theme: {
// 主题色
elColorPrimary: '#409eff',
elColorPrimary: '#3370FF',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
leftMenuBorderColor: '#e5e6eb',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
leftMenuBgColor: '#ffffff',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
leftMenuBgLightColor: '#f0f1f2',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
leftMenuBgActiveColor: '#eaf2ff',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
leftMenuCollapseBgActiveColor: '#eaf2ff',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
leftMenuTextColor: '#4e5969',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
leftMenuTextActiveColor: 'var(--el-color-primary)',
// logo字体颜色
logoTitleTextColor: '#fff',
logoTitleTextColor: '#333333',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
topHeaderBgColor: '#ffffff',
// 头部字体颜色
topHeaderTextColor: 'inherit',
topHeaderTextColor: '#4e5969',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
topHeaderHoverColor: '#f0f1f2',
// 头部边框颜色
topToolBorderColor: '#eee'
topToolBorderColor: '#e5e6eb'
}
}
},
@@ -155,7 +155,8 @@ export const useAppStore = defineStore('app', {
return this.pageLoading
},
getLayout(): LayoutType {
return this.layout
// 强制使用 dashboard 布局,移除所有左侧菜单
return 'dashboard'
},
getTitle(): string {
return this.title

View File

@@ -1 +1 @@
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu' | 'dashboard'

View File

@@ -1,391 +0,0 @@
<template>
<div>
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<el-row :gutter="16" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-20px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<!-- <div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }}20 - 32
</div> -->
</div>
</div>
</el-col>
<!-- <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
/>
</div>
<el-divider direction="vertical" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<el-divider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
/>
</div>
</div>
</el-col> -->
</el-row>
</el-skeleton>
</el-card>
</div>
<!-- <el-row class="mt-8px" :gutter="8" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card shadow="hover" class="mr-5px mt-5px">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-8px" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" />
<el-link type="default" :underline="false" @click="setWatermark(item.name)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row> -->
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
defineOptions({ name: 'Home' })
const { t } = useI18n()
const userStore = useUserStore()
const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill',
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date()
},
{
name: 'yudao-ui-admin-vue3',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus',
time: new Date()
},
{
name: 'yudao-ui-admin-vben',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben(antd)',
time: new Date()
},
{
name: 'yudao-cloud',
icon: 'akar-icons:github',
message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date()
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp',
time: new Date()
},
{
name: 'yudao-ui-admin-vue2',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Vue2 + element-ui',
time: new Date()
}
]
projects = Object.assign(projects, data)
}
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21Vue 2/3',
type: '通知',
keys: ['通知', '8', '17', '21', '2', '3'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '公告',
keys: ['公告', 'Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '通知',
keys: ['通知', '无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
type: '公告',
keys: ['公告', '最广泛'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 获取快捷入口
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill',
url: 'github.io'
},
{
name: 'Vue',
icon: 'logos:vue',
url: 'vuejs.org'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
url: 'https://vitejs.dev/'
},
{
name: 'Angular',
icon: 'logos:angular-icon',
url: 'github.io'
},
{
name: 'React',
icon: 'logos:react',
url: 'github.io'
},
{
name: 'Webpack',
icon: 'logos:webpack',
url: 'github.io'
}
]
shortcut = Object.assign(shortcut, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
}
getAllApi()
</script>

View File

@@ -1,319 +0,0 @@
<template>
<el-row :class="prefixCls" :gutter="20" justify="space-between">
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:peoples" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.newUser') }}
</div>
<CountTo
:duration="2600"
:end-val="102400"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:message" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.unreadInformation') }}
</div>
<CountTo
:duration="2600"
:end-val="81212"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:money" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.transactionAmount') }}
</div>
<CountTo
:duration="2600"
:end-val="9280"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:shopping" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.totalShopping') }}
</div>
<CountTo
:duration="2600"
:end-val="13600"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" justify="space-between">
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="pieOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="barOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="4" animated>
<Echart :height="350" :options="lineOptionsData" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { useDesign } from '@/hooks/web/useDesign'
import type { AnalysisTotalTypes } from './types'
import { barOptions, lineOptions, pieOptions } from './echarts-data'
defineOptions({ name: 'Home2' })
const { t } = useI18n()
const loading = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('panel')
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
let totalState = reactive<AnalysisTotalTypes>({
users: 0,
messages: 0,
moneys: 0,
shoppings: 0
})
const getCount = async () => {
const data = {
users: 102400,
messages: 81212,
moneys: 9280,
shoppings: 13600
}
totalState = Object.assign(totalState, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
set(pieOptionsData, 'series.data', data)
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
// 每月销售总额
const getMonthlySales = async () => {
const data = [
{ estimate: 100, actual: 120, name: 'analysis.january' },
{ estimate: 120, actual: 82, name: 'analysis.february' },
{ estimate: 161, actual: 91, name: 'analysis.march' },
{ estimate: 134, actual: 154, name: 'analysis.april' },
{ estimate: 105, actual: 162, name: 'analysis.may' },
{ estimate: 160, actual: 140, name: 'analysis.june' },
{ estimate: 165, actual: 145, name: 'analysis.july' },
{ estimate: 114, actual: 250, name: 'analysis.august' },
{ estimate: 163, actual: 134, name: 'analysis.september' },
{ estimate: 185, actual: 56, name: 'analysis.october' },
{ estimate: 118, actual: 99, name: 'analysis.november' },
{ estimate: 123, actual: 123, name: 'analysis.december' }
]
set(
lineOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: data.map((v) => v.actual),
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
const getAllApi = async () => {
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
loading.value = false
}
getAllApi()
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-panel;
.#{$prefix-cls} {
&__item {
&--peoples {
color: #40c9c6;
}
&--message {
color: #36a3f7;
}
&--money {
color: #f4516c;
}
&--shopping {
color: #34bfa3;
}
&:hover {
:deep(.#{$namespace}-icon) {
color: #fff !important;
}
.#{$prefix-cls}__item--icon {
transition: all 0.38s ease-out;
}
.#{$prefix-cls}__item--peoples {
background: #40c9c6;
}
.#{$prefix-cls}__item--message {
background: #36a3f7;
}
.#{$prefix-cls}__item--money {
background: #f4516c;
}
.#{$prefix-cls}__item--shopping {
background: #34bfa3;
}
}
}
}
</style>

View File

@@ -1,308 +0,0 @@
import { EChartsOption } from 'echarts'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
title: {
text: t('analysis.monthlySales'),
left: 'center'
},
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: [t('analysis.estimate'), t('analysis.actual')],
top: 50
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
]
}
export const pieOptions: EChartsOption = {
title: {
text: t('analysis.userAccessSource'),
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: [
t('analysis.directAccess'),
t('analysis.mailMarketing'),
t('analysis.allianceAdvertising'),
t('analysis.videoAdvertising'),
t('analysis.searchEngines')
]
},
series: [
{
name: t('analysis.userAccessSource'),
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: t('analysis.directAccess') },
{ value: 310, name: t('analysis.mailMarketing') },
{ value: 234, name: t('analysis.allianceAdvertising') },
{ value: 135, name: t('analysis.videoAdvertising') },
{ value: 1548, name: t('analysis.searchEngines') }
]
}
]
}
export const barOptions: EChartsOption = {
title: {
text: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
type: 'bar'
}
]
}
export const radarOption: EChartsOption = {
legend: {
data: [t('workplace.personal'), t('workplace.team')]
},
radar: {
// shape: 'circle',
indicator: [
{ name: t('workplace.quote'), max: 65 },
{ name: t('workplace.contribution'), max: 160 },
{ name: t('workplace.hot'), max: 300 },
{ name: t('workplace.yield'), max: 130 },
{ name: t('workplace.follow'), max: 100 }
]
},
series: [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: [42, 30, 20, 35, 80],
name: t('workplace.personal')
},
{
value: [50, 140, 290, 100, 90],
name: t('workplace.team')
}
]
}
]
}
export const wordOptions = {
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: 600,
height: 400,
drawOutOfBound: true,
textStyle: {
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') +
')'
)
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: [
{
name: 'Sam S Club',
value: 10000,
textStyle: {
color: 'black'
},
emphasis: {
textStyle: {
color: 'red'
}
}
},
{
name: 'Macys',
value: 6181
},
{
name: 'Amy Schumer',
value: 4386
},
{
name: 'Jurassic World',
value: 4055
},
{
name: 'Charter Communications',
value: 2467
},
{
name: 'Chick Fil A',
value: 2244
},
{
name: 'Planet Fitness',
value: 1898
},
{
name: 'Pitch Perfect',
value: 1484
},
{
name: 'Express',
value: 1112
},
{
name: 'Home',
value: 965
},
{
name: 'Johnny Depp',
value: 847
},
{
name: 'Lena Dunham',
value: 582
},
{
name: 'Lewis Hamilton',
value: 555
},
{
name: 'KXAN',
value: 550
},
{
name: 'Mary Ellen Mark',
value: 462
},
{
name: 'Farrah Abraham',
value: 366
},
{
name: 'Rita Ora',
value: 360
},
{
name: 'Serena Williams',
value: 282
},
{
name: 'NCAA baseball tournament',
value: 273
},
{
name: 'Point Break',
value: 265
}
]
}
]
}

View File

@@ -1,55 +0,0 @@
export type WorkplaceTotal = {
project: number
access: number
todo: number
}
export type Project = {
name: string
icon: string
message: string
personal: string
time: Date | number | string
}
export type Notice = {
title: string
type: string
keys: string[]
date: Date | number | string
}
export type Shortcut = {
name: string
icon: string
url: string
}
export type RadarData = {
personal: number
team: number
max: number
name: string
}
export type AnalysisTotalTypes = {
users: number
messages: number
moneys: number
shoppings: number
}
export type UserAccessSource = {
value: number
name: string
}
export type WeeklyUserActivity = {
value: number
name: string
}
export type MonthlySales = {
name: string
estimate: number
actual: number
}

View File

@@ -0,0 +1,474 @@
<template>
<div class="dashboard">
<el-card shadow="never" class="welcome-card">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-24px font-bold text-gray-800 mb-8px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<div class="text-14px text-gray-300">
欢迎使用教师端管理系统请选择功能模块开始使用
</div>
</div>
</div>
</el-card>
<!-- 常用工具 -->
<el-card v-if="favoriteMenuItems.length > 0" shadow="never" class="menu-card mb-24px">
<template #header>
<div class="h-3 flex justify-between items-center">
<span class="text-18px font-medium">常用工具</span>
<el-tag type="warning" size="small">{{ favoriteMenuItems.length }} 个常用功能</el-tag>
</div>
</template>
<el-row :gutter="16">
<el-col
v-for="item in favoriteMenuItems"
:key="item.path"
:xl="6"
:lg="8"
:md="12"
:sm="24"
:xs="24"
>
<el-card
shadow="hover"
class="mb-16px menu-item-card favorite-card"
@click="navigateTo(item.path)"
>
<div class="card-actions">
<el-button
icon="ep:star-filled"
type="warning"
size="small"
circle
@click.stop="toggleFavorite(item)"
title="取消常用"
/>
</div>
<div class="card-content">
<div class="icon-wrapper">
<Icon :icon="item.icon || 'ep:menu'" :size="32" />
</div>
<span class="card-title">{{ item.title }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
<!-- 按分组显示菜单 -->
<div v-if="Object.keys(groupedMenuItems).length > 0">
<el-card
v-for="(items, groupName) in groupedMenuItems"
:key="groupName"
shadow="never"
class="menu-card mb-24px"
>
<template #header>
<div class="h-3 flex justify-between items-center">
<span class="text-18px font-medium">{{ groupName }}</span>
<el-tag type="info" size="small">{{ items.length }} 个功能</el-tag>
</div>
</template>
<el-row :gutter="16">
<el-col v-for="item in items" :key="item.path" :xl="6" :lg="8" :md="12" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-16px menu-item-card" @click="navigateTo(item.path)">
<div class="card-actions">
<el-button
:icon="item.isFavorite ? 'ep:star-filled' : 'ep:star'"
:type="item.isFavorite ? 'warning' : 'info'"
size="small"
circle
@click.stop="toggleFavorite(item)"
:title="item.isFavorite ? '取消常用' : '标为常用'"
/>
</div>
<div class="card-content">
<div class="icon-wrapper">
<Icon :icon="item.icon || 'ep:menu'" :size="32" />
</div>
<span class="card-title">{{ item.title }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
<!-- 没有菜单项时显示提示 -->
<el-card v-else shadow="never" class="menu-card">
<div class="empty-state">
<el-empty description="暂无可用功能模块">
<template #image>
<Icon icon="ep:menu" :size="60" color="#dcdfe6" />
</template>
<el-button type="primary" @click="$router.push('/profile')">前往个人中心</el-button>
</el-empty>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { useI18n } from '@/hooks/web/useI18n'
import { useAppStore } from '@/store/modules/app'
import { Icon } from '@/components/Icon'
import { hasOneShowingChild } from '@/layout/components/Menu/src/helper'
import { isUrl } from '@/utils/is'
import { pathResolve } from '@/utils/routerHelper'
interface MenuItem {
path: string
title: string
icon?: string
groupName: string
originalTitle: string
parentTitles: string[]
isFavorite?: boolean
}
defineOptions({ name: 'Dashboard' })
const { t } = useI18n()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const router = useRouter()
const avatar = computed(() => userStore.getUser.avatar)
const username = computed(() => userStore.getUser.nickname)
// 从localStorage获取常用菜单
const favoriteMenuPaths = ref<string[]>([])
const loadFavorites = () => {
const saved = localStorage.getItem('dashboard-favorites')
favoriteMenuPaths.value = saved ? JSON.parse(saved) : []
}
const saveFavorites = () => {
localStorage.setItem('dashboard-favorites', JSON.stringify(favoriteMenuPaths.value))
}
// 获取当前布局类型确定使用哪个路由数据
const layout = computed(() => appStore.getLayout)
const routers = computed(() =>
layout.value === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
)
// 扁平化路由,提取所有可访问的菜单项(参考左侧菜单逻辑)
const flattenRoutes = (
routes: AppRouteRecordRaw[],
parentPath = '/',
parentTitles: string[] = []
): MenuItem[] => {
const items: MenuItem[] = []
routes
.filter((v) => !v.meta?.hidden)
.forEach((route) => {
const meta = route.meta ?? {}
const currentTitle = meta.title
? meta.title.includes('router.')
? t(meta.title)
: meta.title
: ''
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(route.children, route)
const fullPath = isUrl(route.path) ? route.path : pathResolve(parentPath, route.path)
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
// 叶子节点
const finalPath = onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath
const finalMeta = onlyOneChild ? onlyOneChild?.meta : meta
const finalTitle = finalMeta?.title
? finalMeta.title.includes('router.')
? t(finalMeta.title)
: finalMeta.title
: ''
if (finalTitle && finalPath) {
let displayTitle = finalTitle
let groupName = '系统工具'
if (parentTitles.length > 0) {
groupName = parentTitles[0]
if (parentTitles.length > 1) {
displayTitle = parentTitles.slice(1).join('-') + '-' + finalTitle
}
}
items.push({
path: finalPath,
title: displayTitle,
icon: finalMeta?.icon,
groupName: groupName,
originalTitle: finalTitle,
parentTitles: [...parentTitles],
isFavorite: favoriteMenuPaths.value.includes(finalPath)
})
}
} else if (route.children && route.children.length > 0) {
// 有子菜单,递归处理
const newParentTitles = currentTitle ? [...parentTitles, currentTitle] : parentTitles
items.push(...flattenRoutes(route.children, fullPath, newParentTitles))
}
})
return items
}
// 过滤不需要显示的菜单项
const filterMenuItems = (items: MenuItem[]) => {
return items.filter((item) => {
const excludePaths = [
'/dashboard',
'/profile',
'/403',
'/404',
'/500',
'/login',
'/redirect',
'/lock',
'/dict/',
'/codegen/',
'/create',
'/edit',
'/detail',
'/log'
]
const excludeTitles = ['首页', '访问首页', 'dashboard', 'Dashboard']
return (
!excludePaths.some((path) => item.path.includes(path)) &&
!excludeTitles.some((title) => item.title.includes(title))
)
})
}
// 获取所有菜单项
const allMenuItems = computed(() => {
const routes = routers.value
if (!routes || routes.length === 0) {
return []
}
const allItems = flattenRoutes(routes)
return filterMenuItems(allItems)
})
// 常用菜单项
const favoriteMenuItems = computed(() => {
return allMenuItems.value.filter((item) => item.isFavorite)
})
// 按分组的菜单项
const groupedMenuItems = computed(() => {
const items = allMenuItems.value // 不过滤常用项,保留原位置
const grouped: Record<string, MenuItem[]> = {}
items.forEach((item) => {
const groupName = item.groupName
if (!grouped[groupName]) {
grouped[groupName] = []
}
grouped[groupName].push(item)
})
return grouped
})
// 切换常用状态
const toggleFavorite = (item: MenuItem) => {
const index = favoriteMenuPaths.value.indexOf(item.path)
if (index > -1) {
favoriteMenuPaths.value.splice(index, 1)
} else {
favoriteMenuPaths.value.push(item.path)
}
// 更新菜单项的常用状态
item.isFavorite = !item.isFavorite
saveFavorites()
}
// 页面加载时恢复常用菜单
loadFavorites()
const navigateTo = (path: string) => {
router.push(path)
}
</script>
<style lang="scss" scoped>
.dashboard {
padding: 24px;
min-height: calc(100vh - 60px);
background-color: #f8f9fa;
padding-top: 24px; // 确保与固定header有适当间距
}
.welcome-card {
margin-bottom: 24px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
:deep(.el-card__body) {
padding: 30px;
}
.el-avatar {
border: 3px solid rgba(255, 255, 255, 0.3);
}
}
.menu-card {
border-radius: 12px;
border: none;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
:deep(.el-card__header) {
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
background-color: #fafafa;
}
:deep(.el-card__body) {
padding: 24px;
}
}
.menu-item-card {
cursor: pointer;
transition: all 0.3s ease;
border-radius: 12px;
height: 140px;
border: 1px solid #e8e9eb;
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
border-color: var(--el-color-primary);
.icon-wrapper {
background: var(--el-color-primary);
color: white;
transform: scale(1.1);
}
.card-actions {
opacity: 1;
}
}
&.favorite-card {
border-color: var(--el-color-warning);
background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%);
}
:deep(.el-card__body) {
padding: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.card-actions {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 10;
}
.icon-wrapper {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(var(--el-color-primary-rgb), 0.1);
color: var(--el-color-primary);
transition: all 0.3s ease;
margin: 0 auto 12px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
color: #1f2937;
line-height: 1.2;
text-align: center;
}
.el-card {
border: 1px solid #e8e9eb;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.dashboard {
padding: 16px;
padding-top: 16px; // 移动端也保持顶部间距
}
.welcome-card {
margin-bottom: 16px;
:deep(.el-card__body) {
padding: 20px;
}
}
.menu-item-card {
height: 120px;
.card-actions {
opacity: 1; // 移动端始终显示操作按钮
}
}
.icon-wrapper {
width: 50px;
height: 50px;
}
}
</style>

View File

@@ -91,6 +91,7 @@
>
<Icon icon="ep:download" />导出
</el-button>
<BackToDashboard text="返回桌面" type="info" />
</el-form-item>
</el-form>
</ContentWrap>
@@ -226,7 +227,7 @@ const queryParams = reactive({
mobile: undefined,
status: undefined,
deptId: undefined,
userType: "0",
userType: '0',
createTime: []
})
const queryFormRef = ref() // 搜索的表单

View File

@@ -24,6 +24,29 @@ ${selector}:hover {
.dark ${selector}:hover {
background-color: var(--el-bg-color-overlay);
}
`
}
],
[
/^custom-hover-exit$/,
([], { rawSelector }) => {
const selector = e(rawSelector)
return `
${selector} {
display: flex;
height: 100%;
padding: 0 10px;
cursor: pointer;
align-items: center;
transition: background var(--transition-time-02);
}
/* you can have multiple rules */
${selector}:hover {
background-color: #e41927;
}
.dark ${selector}:hover {
background-color: #e41927;
}
`
}
],

View File

@@ -27,7 +27,7 @@ export default ({command, mode}: ConfigEnv): UserConfig => {
server: {
port: env.VITE_PORT, // 端口号
host: "0.0.0.0",
open: env.VITE_OPEN === 'true',
open: env.VITE_OPEN === 'false',
// 本地跨域代理. 目前注释的原因暂时没有用途server 端已经支持跨域
// proxy: {
// ['/admin-api']: {