fix: 教师端样式第一版
2
.env
@@ -2,7 +2,7 @@
|
||||
VITE_APP_TITLE=万维智学考试通
|
||||
|
||||
# 项目本地运行端口号
|
||||
VITE_PORT=80
|
||||
VITE_PORT=5173
|
||||
|
||||
# open 运行 npm run dev 时自动打开浏览器
|
||||
VITE_OPEN=true
|
||||
|
@@ -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">
|
||||
|
@@ -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
@@ -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
After Width: | Height: | Size: 88 KiB |
4
src-tauri/.gitignore
vendored
Normal 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
25
src-tauri/Cargo.toml
Normal 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
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
33
src-tauri/capabilities/default.json
Normal 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
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
16
src-tauri/src/lib.rs
Normal 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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
37
src/components/BackToDashboard/BackToDashboard.vue
Normal 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>
|
3
src/components/BackToDashboard/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import BackToDashboard from './BackToDashboard.vue'
|
||||
|
||||
export { BackToDashboard }
|
@@ -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>
|
||||
|
79
src/components/WindowControls/WindowControls.vue
Normal 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>
|
50
src/components/WindowControls/index.vue
Normal 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>
|
@@ -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)
|
||||
}
|
||||
|
@@ -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({
|
||||
|
3
src/layout/components/Exit/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Exit from './src/Exit.vue'
|
||||
|
||||
export { Exit }
|
44
src/layout/components/Exit/src/Exit.vue
Normal 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>
|
@@ -99,7 +99,7 @@ export default defineComponent({
|
||||
>
|
||||
{{
|
||||
default: () => {
|
||||
const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
|
||||
const { renderMenuItem } = useRenderMenuItem()
|
||||
return renderMenuItem(unref(routers))
|
||||
}
|
||||
}}
|
||||
|
3
src/layout/components/Minimize/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Minimize from './src/Minimize.vue'
|
||||
|
||||
export { Minimize }
|
27
src/layout/components/Minimize/src/Minimize.vue
Normal 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>
|
@@ -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,9 +119,20 @@ const clear = () => {
|
||||
wsCache.delete(CACHE_KEY.IS_DARK)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 开放给外部组件调用
|
||||
const openSettings = () => {
|
||||
drawer.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openSettings
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 隐藏浮动按钮,改为通过个人中心菜单访问 -->
|
||||
<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"
|
||||
@@ -207,21 +140,18 @@ const clear = () => {
|
||||
>
|
||||
<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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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" />
|
||||
|
@@ -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
|
||||
|
@@ -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固定
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@@ -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 })
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
|
2
src/types/layout.d.ts
vendored
@@ -1 +1 @@
|
||||
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'
|
||||
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu' | 'dashboard'
|
||||
|
@@ -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/21,Vue 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>
|
@@ -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>
|
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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
|
||||
}
|
474
src/views/dashboard/index.vue
Normal 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>
|
@@ -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() // 搜索的表单
|
||||
|
@@ -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;
|
||||
}
|
||||
`
|
||||
}
|
||||
],
|
||||
|
@@ -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']: {
|
||||
|