chore: pnpm eslint . --fix

This commit is contained in:
pany 2024-11-18 19:40:44 +08:00
parent c8571a5f55
commit 7f02e18d37
93 changed files with 704 additions and 508 deletions

View File

@ -1,3 +1,41 @@
import antfu from '@antfu/eslint-config' import antfu from "@antfu/eslint-config"
export default antfu() // 更多自定义配置可查阅仓库https://github.com/antfu/eslint-config
export default antfu(
{
// 使用外部格式化程序格式化 css、html、markdown 等文件
formatters: true,
// 启用样式规则
stylistic: {
// 缩进级别
indent: 2,
// 引号风格 'single' | 'double'
quotes: "double",
// 是否启用分号
semi: false
},
// 忽略文件
ignores: []
},
{
// 对所有文件都生效的规则
rules: {
// vue
"vue/block-order": ["error", { order: ["script", "template", "style"] }],
// ts
"ts/no-use-before-define": "off",
// node
"node/prefer-global/process": "off",
// style
"style/comma-dangle": ["error", "never"],
"style/brace-style": "off",
// regexp
"regexp/no-unused-capturing-group": "off",
// other
"no-console": "off",
"no-debugger": "off",
"symbol-description": "off",
"antfu/if-newline": "off"
}
}
)

View File

@ -67,6 +67,7 @@
"@vitejs/plugin-vue-jsx": "4.1.0", "@vitejs/plugin-vue-jsx": "4.1.0",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"eslint": "9.15.0", "eslint": "9.15.0",
"eslint-plugin-format": "0.1.2",
"husky": "9.1.6", "husky": "9.1.6",
"jsdom": "25.0.1", "jsdom": "25.0.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",

68
pnpm-lock.yaml generated
View File

@ -65,7 +65,7 @@ importers:
devDependencies: devDependencies:
'@antfu/eslint-config': '@antfu/eslint-config':
specifier: 3.9.1 specifier: 3.9.1
version: 3.9.1(@typescript-eslint/utils@8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.78.0)) version: 3.9.1(@typescript-eslint/utils@8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.2(eslint@9.15.0(jiti@1.21.6)))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.78.0))
'@types/js-cookie': '@types/js-cookie':
specifier: 3.0.6 specifier: 3.0.6
version: 3.0.6 version: 3.0.6
@ -93,6 +93,9 @@ importers:
eslint: eslint:
specifier: 9.15.0 specifier: 9.15.0
version: 9.15.0(jiti@1.21.6) version: 9.15.0(jiti@1.21.6)
eslint-plugin-format:
specifier: 0.1.2
version: 0.1.2(eslint@9.15.0(jiti@1.21.6))
husky: husky:
specifier: 9.1.6 specifier: 9.1.6
version: 9.1.6 version: 9.1.6
@ -313,6 +316,15 @@ packages:
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@dprint/formatter@0.3.0':
resolution: {integrity: sha512-N9fxCxbaBOrDkteSOzaCqwWjso5iAe+WJPsHC021JfHNj2ThInPNEF13ORDKta3llq5D1TlclODCvOvipH7bWQ==}
'@dprint/markdown@0.17.8':
resolution: {integrity: sha512-ukHFOg+RpG284aPdIg7iPrCYmMs3Dqy43S1ejybnwlJoFiW02b+6Bbr5cfZKFRYNP3dKGM86BqHEnMzBOyLvvA==}
'@dprint/toml@0.6.3':
resolution: {integrity: sha512-zQ42I53sb4WVHA+5yoY1t59Zk++Ot02AvUgtNKLzTT8mPyVqVChFcePa3on/xIoKEgH+RoepgPHzqfk9837YFw==}
'@element-plus/icons-vue@2.3.1': '@element-plus/icons-vue@2.3.1':
resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==} resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
peerDependencies: peerDependencies:
@ -1808,6 +1820,11 @@ packages:
eslint-flat-config-utils@0.4.0: eslint-flat-config-utils@0.4.0:
resolution: {integrity: sha512-kfd5kQZC+BMO0YwTol6zxjKX1zAsk8JfSAopbKjKqmENTJcew+yBejuvccAg37cvOrN0Mh+DVbeyznuNWEjt4A==} resolution: {integrity: sha512-kfd5kQZC+BMO0YwTol6zxjKX1zAsk8JfSAopbKjKqmENTJcew+yBejuvccAg37cvOrN0Mh+DVbeyznuNWEjt4A==}
eslint-formatting-reporter@0.0.0:
resolution: {integrity: sha512-k9RdyTqxqN/wNYVaTk/ds5B5rA8lgoAmvceYN7bcZMBwU7TuXx5ntewJv81eF3pIL/CiJE+pJZm36llG8yhyyw==}
peerDependencies:
eslint: '>=8.40.0'
eslint-import-resolver-node@0.3.9: eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
@ -1827,6 +1844,9 @@ packages:
peerDependencies: peerDependencies:
eslint: '*' eslint: '*'
eslint-parser-plain@0.1.0:
resolution: {integrity: sha512-oOeA6FWU0UJT/Rxc3XF5Cq0nbIZbylm7j8+plqq0CZoE6m4u32OXJrR+9iy4srGMmF6v6pmgvP1zPxSRIGh3sg==}
eslint-plugin-antfu@2.7.0: eslint-plugin-antfu@2.7.0:
resolution: {integrity: sha512-gZM3jq3ouqaoHmUNszb1Zo2Ux7RckSvkGksjLWz9ipBYGSv1EwwBETN6AdiUXn+RpVHXTbEMPAPlXJazcA6+iA==} resolution: {integrity: sha512-gZM3jq3ouqaoHmUNszb1Zo2Ux7RckSvkGksjLWz9ipBYGSv1EwwBETN6AdiUXn+RpVHXTbEMPAPlXJazcA6+iA==}
peerDependencies: peerDependencies:
@ -1843,6 +1863,11 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=8' eslint: '>=8'
eslint-plugin-format@0.1.2:
resolution: {integrity: sha512-ZrcO3aiumgJ6ENAv65IWkPjtW77ML/5mp0YrRK0jdvvaZJb+4kKWbaQTMr/XbJo6CtELRmCApAziEKh7L2NbdQ==}
peerDependencies:
eslint: ^8.40.0 || ^9.0.0
eslint-plugin-import-x@4.4.2: eslint-plugin-import-x@4.4.2:
resolution: {integrity: sha512-mDRXPSLQ0UQZQw91QdG4/qZT6hgeW2MJTczAbgPseUZuPEtIjjdPOolXroRkulnOn3fzj6gNgvk+wchMJiHElg==} resolution: {integrity: sha512-mDRXPSLQ0UQZQw91QdG4/qZT6hgeW2MJTczAbgPseUZuPEtIjjdPOolXroRkulnOn3fzj6gNgvk+wchMJiHElg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -2025,6 +2050,9 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
fast-glob@3.3.2: fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@ -3150,6 +3178,10 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier-linter-helpers@1.0.0:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
prettier@3.3.3: prettier@3.3.3:
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3974,7 +4006,7 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@antfu/eslint-config@3.9.1(@typescript-eslint/utils@8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.78.0))': '@antfu/eslint-config@3.9.1(@typescript-eslint/utils@8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.2(eslint@9.15.0(jiti@1.21.6)))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.78.0))':
dependencies: dependencies:
'@antfu/install-pkg': 0.4.1 '@antfu/install-pkg': 0.4.1
'@clack/prompts': 0.7.0 '@clack/prompts': 0.7.0
@ -4012,6 +4044,8 @@ snapshots:
vue-eslint-parser: 9.4.3(eslint@9.15.0(jiti@1.21.6)) vue-eslint-parser: 9.4.3(eslint@9.15.0(jiti@1.21.6))
yaml-eslint-parser: 1.2.3 yaml-eslint-parser: 1.2.3
yargs: 17.7.2 yargs: 17.7.2
optionalDependencies:
eslint-plugin-format: 0.1.2(eslint@9.15.0(jiti@1.21.6))
transitivePeerDependencies: transitivePeerDependencies:
- '@eslint/json' - '@eslint/json'
- '@typescript-eslint/utils' - '@typescript-eslint/utils'
@ -4206,6 +4240,12 @@ snapshots:
'@ctrl/tinycolor@3.6.1': {} '@ctrl/tinycolor@3.6.1': {}
'@dprint/formatter@0.3.0': {}
'@dprint/markdown@0.17.8': {}
'@dprint/toml@0.6.3': {}
'@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.6.3))': '@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.6.3))':
dependencies: dependencies:
vue: 3.5.13(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3)
@ -5784,6 +5824,11 @@ snapshots:
dependencies: dependencies:
pathe: 1.1.2 pathe: 1.1.2
eslint-formatting-reporter@0.0.0(eslint@9.15.0(jiti@1.21.6)):
dependencies:
eslint: 9.15.0(jiti@1.21.6)
prettier-linter-helpers: 1.0.0
eslint-import-resolver-node@0.3.9: eslint-import-resolver-node@0.3.9:
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
@ -5802,6 +5847,8 @@ snapshots:
dependencies: dependencies:
eslint: 9.15.0(jiti@1.21.6) eslint: 9.15.0(jiti@1.21.6)
eslint-parser-plain@0.1.0: {}
eslint-plugin-antfu@2.7.0(eslint@9.15.0(jiti@1.21.6)): eslint-plugin-antfu@2.7.0(eslint@9.15.0(jiti@1.21.6)):
dependencies: dependencies:
'@antfu/utils': 0.7.10 '@antfu/utils': 0.7.10
@ -5819,6 +5866,17 @@ snapshots:
eslint: 9.15.0(jiti@1.21.6) eslint: 9.15.0(jiti@1.21.6)
eslint-compat-utils: 0.5.1(eslint@9.15.0(jiti@1.21.6)) eslint-compat-utils: 0.5.1(eslint@9.15.0(jiti@1.21.6))
eslint-plugin-format@0.1.2(eslint@9.15.0(jiti@1.21.6)):
dependencies:
'@dprint/formatter': 0.3.0
'@dprint/markdown': 0.17.8
'@dprint/toml': 0.6.3
eslint: 9.15.0(jiti@1.21.6)
eslint-formatting-reporter: 0.0.0(eslint@9.15.0(jiti@1.21.6))
eslint-parser-plain: 0.1.0
prettier: 3.3.3
synckit: 0.9.2
eslint-plugin-import-x@4.4.2(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): eslint-plugin-import-x@4.4.2(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3):
dependencies: dependencies:
'@typescript-eslint/utils': 8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) '@typescript-eslint/utils': 8.14.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)
@ -6110,6 +6168,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
fast-glob@3.3.2: fast-glob@3.3.2:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@ -7379,6 +7439,10 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
dependencies:
fast-diff: 1.3.0
prettier@3.3.3: {} prettier@3.3.3: {}
proto-list@1.2.4: {} proto-list@1.2.4: {}

View File

@ -1,5 +1,4 @@
// Tip: Simple judgments may not fully cover // Tip: Simple judgments may not fully cover
if (/MSIE\s|Trident\//.test(window.navigator.userAgent)) { if (/MSIE\s|Trident\//.test(window.navigator.userAgent)) {
document.body.innerHTML = document.body.innerHTML = "<strong>Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge.</strong>"
"<strong>Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge.</strong>"
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTheme } from "@/hooks/useTheme"
import { useGreyAndColorWeakness } from "@/hooks/useGreyAndColorWeakness" import { useGreyAndColorWeakness } from "@/hooks/useGreyAndColorWeakness"
import { useTheme } from "@/hooks/useTheme"
import { ElNotification } from "element-plus" import { ElNotification } from "element-plus"
import zhCn from "element-plus/es/locale/lang/zh-cn" // Element Plus import zhCn from "element-plus/es/locale/lang/zh-cn" // Element Plus

View File

@ -1,5 +1,5 @@
import { request } from "@/utils/service"
import type * as Login from "./types/login" import type * as Login from "./types/login"
import { request } from "@/utils/service"
/** 获取登录验证码 */ /** 获取登录验证码 */
export function getLoginCodeApi() { export function getLoginCodeApi() {

View File

@ -11,4 +11,4 @@ export type LoginCodeResponseData = ApiResponseData<string>
export type LoginResponseData = ApiResponseData<{ token: string }> export type LoginResponseData = ApiResponseData<{ token: string }>
export type UserInfoResponseData = ApiResponseData<{ username: string; roles: string[] }> export type UserInfoResponseData = ApiResponseData<{ username: string, roles: string[] }>

View File

@ -1,5 +1,5 @@
import { request } from "@/utils/service"
import type * as Table from "./types/table" import type * as Table from "./types/table"
import { request } from "@/utils/service"
/** 增 */ /** 增 */
export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) { export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) {

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type ListItem } from "./data" import type { ListItem } from "./data"
interface Props { interface Props {
list: ListItem[] list: ListItem[]
@ -10,7 +10,7 @@ const props = defineProps<Props>()
<template> <template>
<el-empty v-if="props.list.length === 0" /> <el-empty v-if="props.list.length === 0" />
<el-card v-else v-for="(item, index) in props.list" :key="index" shadow="never" class="card-container"> <el-card v-for="(item, index) in props.list" v-else :key="index" shadow="never" class="card-container">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<div> <div>
@ -18,10 +18,12 @@ const props = defineProps<Props>()
<span class="card-title">{{ item.title }}</span> <span class="card-title">{{ item.title }}</span>
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag> <el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
</span> </span>
<div class="card-time">{{ item.datetime }}</div> <div class="card-time">
{{ item.datetime }}
</div>
</div> </div>
<div v-if="item.avatar" class="card-avatar"> <div v-if="item.avatar" class="card-avatar">
<img :src="item.avatar" width="34" /> <img :src="item.avatar" width="34">
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from "vue" import type { ListItem } from "./data"
import { ElMessage } from "element-plus"
import { Bell } from "@element-plus/icons-vue" import { Bell } from "@element-plus/icons-vue"
import { ElMessage } from "element-plus"
import { computed, ref } from "vue"
import { messageData, notifyData, todoData } from "./data"
import NotifyList from "./NotifyList.vue" import NotifyList from "./NotifyList.vue"
import { type ListItem, notifyData, messageData, todoData } from "./data"
type TabName = "通知" | "消息" | "待办" type TabName = "通知" | "消息" | "待办"
@ -45,7 +46,7 @@ const data = ref<DataItem[]>([
} }
]) ])
const handleHistory = () => { function handleHistory() {
ElMessage.success(`跳转到${activeName.value}历史页面`) ElMessage.success(`跳转到${activeName.value}历史页面`)
} }
</script> </script>
@ -64,7 +65,7 @@ const handleHistory = () => {
</template> </template>
<template #default> <template #default>
<el-tabs v-model="activeName" class="demo-tabs" stretch> <el-tabs v-model="activeName" class="demo-tabs" stretch>
<el-tab-pane v-for="(item, index) in data" :name="item.name" :key="index"> <el-tab-pane v-for="(item, index) in data" :key="index" :name="item.name">
<template #label> <template #label>
{{ item.name }} {{ item.name }}
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" /> <el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
@ -75,7 +76,9 @@ const handleHistory = () => {
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="notify-history"> <div class="notify-history">
<el-button link @click="handleHistory">查看{{ activeName }}历史</el-button> <el-button link @click="handleHistory">
查看{{ activeName }}历史
</el-button>
</div> </div>
</template> </template>
</el-popover> </el-popover>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watchEffect } from "vue"
import { ElMessage } from "element-plus" import { ElMessage } from "element-plus"
import screenfull from "screenfull" import screenfull from "screenfull"
import { computed, ref, watchEffect } from "vue"
interface Props { interface Props {
/** 全屏的元素,默认是 html */ /** 全屏的元素,默认是 html */
@ -25,17 +25,17 @@ const CONTENT_LARGE = "content-large"
const CONTENT_FULL = "content-full" const CONTENT_FULL = "content-full"
const classList = document.body.classList const classList = document.body.classList
//#region // #region
const isEnabled = screenfull.isEnabled const isEnabled = screenfull.isEnabled
const isFullscreen = ref<boolean>(false) const isFullscreen = ref<boolean>(false)
const fullscreenTips = computed(() => (isFullscreen.value ? props.exitTips : props.openTips)) const fullscreenTips = computed(() => (isFullscreen.value ? props.exitTips : props.openTips))
const fullscreenSvgName = computed(() => (isFullscreen.value ? "fullscreen-exit" : "fullscreen")) const fullscreenSvgName = computed(() => (isFullscreen.value ? "fullscreen-exit" : "fullscreen"))
const handleFullscreenClick = () => { function handleFullscreenClick() {
const dom = document.querySelector(props.element) || undefined const dom = document.querySelector(props.element) || undefined
isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作") isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
} }
const handleFullscreenChange = () => { function handleFullscreenChange() {
isFullscreen.value = screenfull.isFullscreen isFullscreen.value = screenfull.isFullscreen
// 退 class // 退 class
isFullscreen.value || classList.remove(CONTENT_LARGE, CONTENT_FULL) isFullscreen.value || classList.remove(CONTENT_LARGE, CONTENT_FULL)
@ -48,18 +48,18 @@ watchEffect((onCleanup) => {
onCleanup(() => screenfull.off("change", handleFullscreenChange)) onCleanup(() => screenfull.off("change", handleFullscreenChange))
} }
}) })
//#endregion // #endregion
//#region // #region
const isContentLarge = ref<boolean>(false) const isContentLarge = ref<boolean>(false)
const contentLargeTips = computed(() => (isContentLarge.value ? "内容区复原" : "内容区放大")) const contentLargeTips = computed(() => (isContentLarge.value ? "内容区复原" : "内容区放大"))
const contentLargeSvgName = computed(() => (isContentLarge.value ? "fullscreen-exit" : "fullscreen")) const contentLargeSvgName = computed(() => (isContentLarge.value ? "fullscreen-exit" : "fullscreen"))
const handleContentLargeClick = () => { function handleContentLargeClick() {
isContentLarge.value = !isContentLarge.value isContentLarge.value = !isContentLarge.value
// //
classList.toggle(CONTENT_LARGE, isContentLarge.value) classList.toggle(CONTENT_LARGE, isContentLarge.value)
} }
const handleContentFullClick = () => { function handleContentFullClick() {
// //
isContentLarge.value && handleContentLargeClick() isContentLarge.value && handleContentLargeClick()
// //
@ -67,7 +67,7 @@ const handleContentFullClick = () => {
// //
handleFullscreenClick() handleFullscreenClick()
} }
//#endregion // #endregion
</script> </script>
<template> <template>
@ -82,9 +82,13 @@ const handleContentFullClick = () => {
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<!-- 内容区放大 --> <!-- 内容区放大 -->
<el-dropdown-item @click="handleContentLargeClick">{{ contentLargeTips }}</el-dropdown-item> <el-dropdown-item @click="handleContentLargeClick">
{{ contentLargeTips }}
</el-dropdown-item>
<!-- 内容区全屏 --> <!-- 内容区全屏 -->
<el-dropdown-item @click="handleContentFullClick">内容区全屏</el-dropdown-item> <el-dropdown-item @click="handleContentFullClick">
内容区全屏
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>

View File

@ -1,13 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, shallowRef } from "vue" import type { RouteRecordName, RouteRecordRaw } from "vue-router"
import { type RouteRecordName, type RouteRecordRaw, useRouter } from "vue-router" import { useDevice } from "@/hooks/useDevice"
import { usePermissionStore } from "@/store/modules/permission" import { usePermissionStore } from "@/store/modules/permission"
import SearchResult from "./SearchResult.vue" import { isExternal } from "@/utils/validate"
import SearchFooter from "./SearchFooter.vue"
import { ElMessage, ElScrollbar } from "element-plus" import { ElMessage, ElScrollbar } from "element-plus"
import { cloneDeep, debounce } from "lodash-es" import { cloneDeep, debounce } from "lodash-es"
import { useDevice } from "@/hooks/useDevice" import { computed, ref, shallowRef } from "vue"
import { isExternal } from "@/utils/validate" import { useRouter } from "vue-router"
import SearchFooter from "./SearchFooter.vue"
import SearchResult from "./SearchResult.vue"
/** 控制 modal 显隐 */ /** 控制 modal 显隐 */
const modelValue = defineModel<boolean>({ required: true }) const modelValue = defineModel<boolean>({ required: true })
@ -33,7 +34,7 @@ const menusData = computed(() => cloneDeep(usePermissionStore().routes))
/** 搜索(防抖) */ /** 搜索(防抖) */
const handleSearch = debounce(() => { const handleSearch = debounce(() => {
const flatMenusData = flatTree(menusData.value) const flatMenusData = flatTree(menusData.value)
resultList.value = flatMenusData.filter((menu) => resultList.value = flatMenusData.filter(menu =>
keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false
) )
// //
@ -42,7 +43,7 @@ const handleSearch = debounce(() => {
}, 500) }, 500)
/** 将树形菜单扁平化为一维数组,用于菜单搜索 */ /** 将树形菜单扁平化为一维数组,用于菜单搜索 */
const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => { function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
arr.forEach((item) => { arr.forEach((item) => {
result.push(item) result.push(item)
item.children && flatTree(item.children, result) item.children && flatTree(item.children, result)
@ -51,7 +52,7 @@ const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
} }
/** 关闭搜索对话框 */ /** 关闭搜索对话框 */
const handleClose = () => { function handleClose() {
modelValue.value = false modelValue.value = false
// //
setTimeout(() => { setTimeout(() => {
@ -61,7 +62,7 @@ const handleClose = () => {
} }
/** 根据下标位置进行滚动 */ /** 根据下标位置进行滚动 */
const scrollTo = (index: number) => { function scrollTo(index: number) {
if (!searchResultRef.value) return if (!searchResultRef.value) return
const scrollTop = searchResultRef.value.getScrollTop(index) const scrollTop = searchResultRef.value.getScrollTop(index)
// el-scrollbar // el-scrollbar
@ -69,12 +70,12 @@ const scrollTo = (index: number) => {
} }
/** 键盘上键 */ /** 键盘上键 */
const handleUp = () => { function handleUp() {
isPressUpOrDown.value = true isPressUpOrDown.value = true
const { length } = resultList.value const { length } = resultList.value
if (length === 0) return if (length === 0) return
// name // name
const index = resultList.value.findIndex((item) => item.name === activeRouteName.value) const index = resultList.value.findIndex(item => item.name === activeRouteName.value)
// //
if (index === 0) { if (index === 0) {
const bottomName = resultList.value[length - 1].name const bottomName = resultList.value[length - 1].name
@ -94,12 +95,12 @@ const handleUp = () => {
} }
/** 键盘下键 */ /** 键盘下键 */
const handleDown = () => { function handleDown() {
isPressUpOrDown.value = true isPressUpOrDown.value = true
const { length } = resultList.value const { length } = resultList.value
if (length === 0) return if (length === 0) return
// name name // name name
const index = resultList.value.map((item) => item.name).lastIndexOf(activeRouteName.value) const index = resultList.value.map(item => item.name).lastIndexOf(activeRouteName.value)
// //
if (index === length - 1) { if (index === length - 1) {
const topName = resultList.value[0].name const topName = resultList.value[0].name
@ -119,11 +120,11 @@ const handleDown = () => {
} }
/** 键盘回车键 */ /** 键盘回车键 */
const handleEnter = () => { function handleEnter() {
const { length } = resultList.value const { length } = resultList.value
if (length === 0) return if (length === 0) return
const name = activeRouteName.value const name = activeRouteName.value
const path = resultList.value.find((item) => item.name === name)?.path const path = resultList.value.find(item => item.name === name)?.path
if (path && isExternal(path)) { if (path && isExternal(path)) {
window.open(path, "_blank", "noopener, noreferrer") window.open(path, "_blank", "noopener, noreferrer")
return return
@ -134,7 +135,8 @@ const handleEnter = () => {
} }
try { try {
router.push({ name }) router.push({ name })
} catch { }
catch {
ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入") ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入")
return return
} }
@ -142,7 +144,7 @@ const handleEnter = () => {
} }
/** 释放上键或下键 */ /** 释放上键或下键 */
const handleReleaseUpOrDown = () => { function handleReleaseUpOrDown() {
isPressUpOrDown.value = false isPressUpOrDown.value = false
} }
</script> </script>
@ -150,19 +152,19 @@ const handleReleaseUpOrDown = () => {
<template> <template>
<el-dialog <el-dialog
v-model="modelValue" v-model="modelValue"
:before-close="handleClose"
:width="modalWidth"
top="5vh"
class="search-modal__private"
append-to-body
@opened="inputRef?.focus()" @opened="inputRef?.focus()"
@closed="inputRef?.blur()" @closed="inputRef?.blur()"
@keydown.up="handleUp" @keydown.up="handleUp"
@keydown.down="handleDown" @keydown.down="handleDown"
@keydown.enter="handleEnter" @keydown.enter="handleEnter"
@keyup.up.down="handleReleaseUpOrDown" @keyup.up.down="handleReleaseUpOrDown"
:before-close="handleClose"
:width="modalWidth"
top="5vh"
class="search-modal__private"
append-to-body
> >
<el-input ref="inputRef" v-model="keyword" @input="handleSearch" placeholder="搜索菜单" size="large" clearable> <el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单" size="large" clearable @input="handleSearch">
<template #prefix> <template #prefix>
<SvgIcon name="search" /> <SvgIcon name="search" />
</template> </template>
@ -170,15 +172,15 @@ const handleReleaseUpOrDown = () => {
<el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" /> <el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
<template v-else> <template v-else>
<p>搜索结果</p> <p>搜索结果</p>
<el-scrollbar ref="scrollbarRef" max-height="40vh" always> <ElScrollbar ref="scrollbarRef" max-height="40vh" always>
<SearchResult <SearchResult
ref="searchResultRef" ref="searchResultRef"
v-model="activeRouteName" v-model="activeRouteName"
:list="resultList" :list="resultList"
:isPressUpOrDown="isPressUpOrDown" :is-press-up-or-down="isPressUpOrDown"
@click="handleEnter" @click="handleEnter"
/> />
</el-scrollbar> </ElScrollbar>
</template> </template>
<template #footer> <template #footer>
<SearchFooter :total="resultList.length" /> <SearchFooter :total="resultList.length" />

View File

@ -1,21 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RouteRecordName, RouteRecordRaw } from "vue-router"
import { getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue" import { getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue"
import { type RouteRecordName, type RouteRecordRaw } from "vue-router"
interface Props { interface Props {
list: RouteRecordRaw[] list: RouteRecordRaw[]
isPressUpOrDown: boolean isPressUpOrDown: boolean
} }
const props = defineProps<Props>()
/** 选中的菜单 */ /** 选中的菜单 */
const modelValue = defineModel<RouteRecordName | undefined>({ required: true }) const modelValue = defineModel<RouteRecordName | undefined>({ required: true })
const props = defineProps<Props>()
const instance = getCurrentInstance() const instance = getCurrentInstance()
const scrollbarHeight = ref<number>(0) const scrollbarHeight = ref<number>(0)
/** 菜单的样式 */ /** 菜单的样式 */
const itemStyle = (item: RouteRecordRaw) => { function itemStyle(item: RouteRecordRaw) {
const flag = item.name === modelValue.value const flag = item.name === modelValue.value
return { return {
background: flag ? "var(--el-color-primary)" : "", background: flag ? "var(--el-color-primary)" : "",
@ -24,20 +23,20 @@ const itemStyle = (item: RouteRecordRaw) => {
} }
/** 鼠标移入 */ /** 鼠标移入 */
const handleMouseenter = (item: RouteRecordRaw) => { function handleMouseenter(item: RouteRecordRaw) {
// mouseenter // mouseenter
if (props.isPressUpOrDown) return if (props.isPressUpOrDown) return
modelValue.value = item.name modelValue.value = item.name
} }
/** 计算滚动可视区高度 */ /** 计算滚动可视区高度 */
const getScrollbarHeight = () => { function getScrollbarHeight() {
// el-scrollbar max-height="40vh" // el-scrollbar max-height="40vh"
scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1)) scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1))
} }
/** 根据下标计算到顶部的距离 */ /** 根据下标计算到顶部的距离 */
const getScrollTop = (index: number) => { function getScrollTop(index: number) {
const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[] const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[]
if (!currentInstance) return 0 if (!currentInstance) return 0
const currentRef = currentInstance[0] const currentRef = currentInstance[0]
@ -75,7 +74,7 @@ defineExpose({ getScrollTop })
@mouseenter="handleMouseenter(item)" @mouseenter="handleMouseenter(item)"
> >
<SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" /> <SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" />
<component v-else-if="item.meta?.elIcon" :is="item.meta.elIcon" class="el-icon" /> <component :is="item.meta.elIcon" v-else-if="item.meta?.elIcon" class="el-icon" />
<span class="result-item-title"> <span class="result-item-title">
{{ item.meta?.title }} {{ item.meta?.title }}
</span> </span>

View File

@ -5,7 +5,7 @@ import SearchModal from "./SearchModal.vue"
/** 控制 modal 显隐 */ /** 控制 modal 显隐 */
const modalVisible = ref<boolean>(false) const modalVisible = ref<boolean>(false)
/** 打开 modal */ /** 打开 modal */
const handleOpen = () => { function handleOpen() {
modalVisible.value = true modalVisible.value = true
} }
</script> </script>

View File

@ -1,18 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type ThemeName, useTheme } from "@/hooks/useTheme" import type { ThemeName } from "@/hooks/useTheme"
import { useTheme } from "@/hooks/useTheme"
import { MagicStick } from "@element-plus/icons-vue" import { MagicStick } from "@element-plus/icons-vue"
const { themeList, activeThemeName, setTheme } = useTheme() const { themeList, activeThemeName, setTheme } = useTheme()
const handleChangeTheme = ({ clientX, clientY }: MouseEvent, themeName: ThemeName) => { function handleChangeTheme({ clientX, clientY }: MouseEvent, themeName: ThemeName) {
const maxRadius = Math.hypot( const maxRadius = Math.hypot(
Math.max(clientX, window.innerWidth - clientX), Math.max(clientX, window.innerWidth - clientX),
Math.max(clientY, window.innerHeight - clientY) Math.max(clientY, window.innerHeight - clientY)
) )
const style = document.documentElement.style const style = document.documentElement.style
style.setProperty("--v3-theme-x", clientX + "px") style.setProperty("--v3-theme-x", `${clientX}px`)
style.setProperty("--v3-theme-y", clientY + "px") style.setProperty("--v3-theme-y", `${clientY}px`)
style.setProperty("--v3-theme-r", maxRadius + "px") style.setProperty("--v3-theme-r", `${maxRadius}px`)
const handler = () => { const handler = () => {
setTheme(themeName) setTheme(themeName)
} }

View File

@ -1,5 +1,5 @@
import { getConfigLayout } from "@/utils/cache/local-storage"
import { LayoutModeEnum } from "@/constants/app-key" import { LayoutModeEnum } from "@/constants/app-key"
import { getConfigLayout } from "@/utils/cache/local-storage"
/** 项目配置类型 */ /** 项目配置类型 */
export interface LayoutSettings { export interface LayoutSettings {

View File

@ -6,7 +6,8 @@ interface RouteSettings {
* 2. dynamic: false * 2. dynamic: false
*/ */
dynamic: boolean dynamic: boolean
/** /**
*
* 1. 访 * 1. 访
* 2. * 2.
*/ */

View File

@ -1,4 +1,4 @@
import { type RouteLocationNormalized, type RouteRecordNameGeneric } from "vue-router" import type { RouteLocationNormalized, RouteRecordNameGeneric } from "vue-router"
/** 免登录白名单(匹配路由 path */ /** 免登录白名单(匹配路由 path */
const whiteListByPath: string[] = ["/login"] const whiteListByPath: string[] = ["/login"]
@ -7,9 +7,9 @@ const whiteListByPath: string[] = ["/login"]
const whiteListByName: RouteRecordNameGeneric[] = [] const whiteListByName: RouteRecordNameGeneric[] = []
/** 判断是否在白名单 */ /** 判断是否在白名单 */
const isWhiteList = (to: RouteLocationNormalized) => { function isWhiteList(to: RouteLocationNormalized) {
// path 和 name 任意一个匹配上即可 // path 和 name 任意一个匹配上即可
return whiteListByPath.indexOf(to.path) !== -1 || whiteListByName.indexOf(to.name) !== -1 return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name)
} }
export default isWhiteList export default isWhiteList

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
import { permission } from "./permission" import { permission } from "./permission"
/** 挂载自定义指令 */ /** 挂载自定义指令 */

View File

@ -1,4 +1,4 @@
import { type Directive } from "vue" import type { Directive } from "vue"
import { useUserStore } from "@/store/modules/user" import { useUserStore } from "@/store/modules/user"
/** 权限指令,和权限判断函数 checkPermission 功能类似 */ /** 权限指令,和权限判断函数 checkPermission 功能类似 */
@ -7,7 +7,7 @@ export const permission: Directive = {
const { value: permissionRoles } = binding const { value: permissionRoles } = binding
const { roles } = useUserStore() const { roles } = useUserStore()
if (Array.isArray(permissionRoles) && permissionRoles.length > 0) { if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
const hasPermission = roles.some((role) => permissionRoles.includes(role)) const hasPermission = roles.some(role => permissionRoles.includes(role))
// hasPermission || (el.style.display = "none") // 隐藏 // hasPermission || (el.style.display = "none") // 隐藏
hasPermission || el.parentNode?.removeChild(el) // 销毁 hasPermission || el.parentNode?.removeChild(el) // 销毁
} else { } else {

View File

@ -1,6 +1,6 @@
import { computed } from "vue"
import { useAppStore } from "@/store/modules/app"
import { DeviceEnum } from "@/constants/app-key" import { DeviceEnum } from "@/constants/app-key"
import { useAppStore } from "@/store/modules/app"
import { computed } from "vue"
const appStore = useAppStore() const appStore = useAppStore()
const isMobile = computed(() => appStore.device === DeviceEnum.Mobile) const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)

View File

@ -1,4 +1,4 @@
import { ref, onMounted } from "vue" import { onMounted, ref } from "vue"
type OptionValue = string | number type OptionValue = string | number

View File

@ -1,4 +1,4 @@
import { type LoadingOptions, ElLoading } from "element-plus" import { ElLoading, type LoadingOptions } from "element-plus"
const defaultOptions = { const defaultOptions = {
lock: true, lock: true,
@ -28,7 +28,8 @@ export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) =>
try { try {
loadingInstance = ElLoading.service({ ...defaultOptions, ...options }) loadingInstance = ElLoading.service({ ...defaultOptions, ...options })
return await fn(...args) return await fn(...args)
} finally { }
finally {
loadingInstance?.close() loadingInstance?.close()
} }
} }

View File

@ -1,12 +1,12 @@
import { watchEffect } from "vue"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { watchEffect } from "vue"
const GREY_MODE = "grey-mode" const GREY_MODE = "grey-mode"
const COLOR_WEAKNESS = "color-weakness" const COLOR_WEAKNESS = "color-weakness"
const classList = document.documentElement.classList const classList = document.documentElement.classList
/** 初始化 */ /** 初始化 */
const initGreyAndColorWeakness = () => { function initGreyAndColorWeakness() {
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
watchEffect(() => { watchEffect(() => {
classList.toggle(GREY_MODE, settingsStore.showGreyMode) classList.toggle(GREY_MODE, settingsStore.showGreyMode)

View File

@ -1,13 +1,13 @@
import { computed } from "vue"
import { useSettingsStore } from "@/store/modules/settings"
import { LayoutModeEnum } from "@/constants/app-key" import { LayoutModeEnum } from "@/constants/app-key"
import { useSettingsStore } from "@/store/modules/settings"
import { computed } from "vue"
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left) const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left)
const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top) const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top)
const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop) const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop)
const setLayoutMode = (mode: LayoutModeEnum) => { function setLayoutMode(mode: LayoutModeEnum) {
settingsStore.layoutMode = mode settingsStore.layoutMode = mode
} }

View File

@ -1,6 +1,6 @@
import { onBeforeUnmount } from "vue" import type { RouteLocationNormalized } from "vue-router"
import mitt, { type Handler } from "mitt" import mitt, { type Handler } from "mitt"
import { type RouteLocationNormalized } from "vue-router" import { onBeforeUnmount } from "vue"
/** 回调函数的类型 */ /** 回调函数的类型 */
type Callback = (route: RouteLocationNormalized) => void type Callback = (route: RouteLocationNormalized) => void
@ -10,7 +10,7 @@ const key = Symbol("ROUTE_CHANGE")
let latestRoute: RouteLocationNormalized let latestRoute: RouteLocationNormalized
/** 设置最新的路由信息,触发路由变化事件 */ /** 设置最新的路由信息,触发路由变化事件 */
export const setRouteChange = (to: RouteLocationNormalized) => { export function setRouteChange(to: RouteLocationNormalized) {
// 触发事件 // 触发事件
emitter.emit(key, to) emitter.emit(key, to)
// 缓存最新的路由信息 // 缓存最新的路由信息

View File

@ -1,5 +1,5 @@
import { ref, watchEffect } from "vue"
import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage" import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage"
import { ref, watchEffect } from "vue"
const DEFAULT_THEME_NAME = "normal" const DEFAULT_THEME_NAME = "normal"
type DefaultThemeName = typeof DEFAULT_THEME_NAME type DefaultThemeName = typeof DEFAULT_THEME_NAME
@ -32,23 +32,23 @@ const themeList: ThemeList[] = [
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME) const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
/** 设置主题 */ /** 设置主题 */
const setTheme = (value: ThemeName) => { function setTheme(value: ThemeName) {
activeThemeName.value = value activeThemeName.value = value
} }
/** 在 html 根元素上挂载 class */ /** 在 html 根元素上挂载 class */
const addHtmlClass = (value: ThemeName) => { function addHtmlClass(value: ThemeName) {
document.documentElement.classList.add(value) document.documentElement.classList.add(value)
} }
/** 在 html 根元素上移除其他主题 class */ /** 在 html 根元素上移除其他主题 class */
const removeHtmlClass = (value: ThemeName) => { function removeHtmlClass(value: ThemeName) {
const otherThemeNameList = themeList.map((item) => item.name).filter((name) => name !== value) const otherThemeNameList = themeList.map(item => item.name).filter(name => name !== value)
document.documentElement.classList.remove(...otherThemeNameList) document.documentElement.classList.remove(...otherThemeNameList)
} }
/** 初始化 */ /** 初始化 */
const initTheme = () => { function initTheme() {
// watchEffect 来收集副作用 // watchEffect 来收集副作用
watchEffect(() => { watchEffect(() => {
const value = activeThemeName.value const value = activeThemeName.value

View File

@ -7,7 +7,7 @@ const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
const dynamicTitle = ref<string>("") const dynamicTitle = ref<string>("")
/** 设置标题 */ /** 设置标题 */
const setTitle = (title?: string) => { function setTitle(title?: string) {
dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
} }

View File

@ -1,7 +1,7 @@
import { type Ref, onBeforeUnmount, ref } from "vue"
import { debounce } from "lodash-es" import { debounce } from "lodash-es"
import { onBeforeUnmount, type Ref, ref } from "vue"
type Observer = { interface Observer {
watermarkElMutationObserver?: MutationObserver watermarkElMutationObserver?: MutationObserver
parentElMutationObserver?: MutationObserver parentElMutationObserver?: MutationObserver
parentElResizeObserver?: ResizeObserver parentElResizeObserver?: ResizeObserver
@ -124,10 +124,12 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
// 移除水印元素 // 移除水印元素
try { try {
parentEl.value.removeChild(watermarkEl) parentEl.value.removeChild(watermarkEl)
} catch { }
catch {
// 比如在无防御情况下,用户打开控制台删除了这个元素 // 比如在无防御情况下,用户打开控制台删除了这个元素
console.warn("水印元素已不存在,请重新创建") console.warn("水印元素已不存在,请重新创建")
} finally { }
finally {
watermarkEl = null watermarkEl = null
} }
} }

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
import SvgIcon from "@/components/SvgIcon/index.vue" // Svg Component import SvgIcon from "@/components/SvgIcon/index.vue" // Svg Component
import "virtual:svg-icons-register" import "virtual:svg-icons-register"

View File

@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue" import { useDevice } from "@/hooks/useDevice"
import { storeToRefs } from "pinia"
import { useAppStore } from "@/store/modules/app" import { useAppStore } from "@/store/modules/app"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { storeToRefs } from "pinia"
import { computed } from "vue"
import { AppMain, NavigationBar, Sidebar, TagsView } from "./components" import { AppMain, NavigationBar, Sidebar, TagsView } from "./components"
import { useDevice } from "@/hooks/useDevice"
const { isMobile } = useDevice() const { isMobile } = useDevice()
const appStore = useAppStore() const appStore = useAppStore()
@ -22,7 +22,7 @@ const layoutClasses = computed(() => {
}) })
/** 用于处理点击 mobile 端侧边栏遮罩层的事件 */ /** 用于处理点击 mobile 端侧边栏遮罩层的事件 */
const handleClickOutside = () => { function handleClickOutside() {
appStore.closeSidebar(false) appStore.closeSidebar(false)
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"
import { storeToRefs } from "pinia"
import { useAppStore } from "@/store/modules/app" import { useAppStore } from "@/store/modules/app"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { AppMain, NavigationBar, Sidebar, TagsView, Logo } from "./components" import { storeToRefs } from "pinia"
import { computed } from "vue"
import { AppMain, Logo, NavigationBar, Sidebar, TagsView } from "./components"
const appStore = useAppStore() const appStore = useAppStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { storeToRefs } from "pinia"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { AppMain, NavigationBar, TagsView, Logo } from "./components" import { storeToRefs } from "pinia"
import { AppMain, Logo, NavigationBar, TagsView } from "./components"
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const { showTagsView, showLogo } = storeToRefs(settingsStore) const { showTagsView, showLogo } = storeToRefs(settingsStore)

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTagsViewStore } from "@/store/modules/tags-view"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { useTagsViewStore } from "@/store/modules/tags-view"
import Footer from "./Footer/index.vue" import Footer from "./Footer/index.vue"
const tagsViewStore = useTagsViewStore() const tagsViewStore = useTagsViewStore()

View File

@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue" import type { RouteLocationMatched } from "vue-router"
import { type RouteLocationMatched, useRoute, useRouter } from "vue-router"
import { useRouteListener } from "@/hooks/useRouteListener" import { useRouteListener } from "@/hooks/useRouteListener"
import { compile } from "path-to-regexp" import { compile } from "path-to-regexp"
import { ref } from "vue"
import { useRoute, useRouter } from "vue-router"
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -12,18 +13,18 @@ const { listenerRouteChange } = useRouteListener()
const breadcrumbs = ref<RouteLocationMatched[]>([]) const breadcrumbs = ref<RouteLocationMatched[]>([])
/** 获取面包屑导航信息 */ /** 获取面包屑导航信息 */
const getBreadcrumb = () => { function getBreadcrumb() {
breadcrumbs.value = route.matched.filter((item) => item.meta?.title && item.meta?.breadcrumb !== false) breadcrumbs.value = route.matched.filter(item => item.meta?.title && item.meta?.breadcrumb !== false)
} }
/** 编译路由路径 */ /** 编译路由路径 */
const pathCompile = (path: string) => { function pathCompile(path: string) {
const toPath = compile(path) const toPath = compile(path)
return toPath(route.params) return toPath(route.params)
} }
/** 处理面包屑导航点击事件 */ /** 处理面包屑导航点击事件 */
const handleLink = (item: RouteLocationMatched) => { function handleLink(item: RouteLocationMatched) {
const { redirect, path } = item const { redirect, path } = item
if (redirect) { if (redirect) {
router.push(redirect as string) router.push(redirect as string)

View File

@ -3,7 +3,9 @@ const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE
</script> </script>
<template> <template>
<footer class="layout-footer">MIT © 2021-PRESENT {{ VITE_APP_TITLE }}</footer> <footer class="layout-footer">
MIT © 2021-PRESENT {{ VITE_APP_TITLE }}
</footer>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -14,7 +14,7 @@ const emit = defineEmits<{
toggleClick: [] toggleClick: []
}>() }>()
const toggleClick = () => { function toggleClick() {
emit("toggleClick") emit("toggleClick")
} }
</script> </script>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLayoutMode } from "@/hooks/useLayoutMode"
import logo from "@/assets/layouts/logo.png?url" import logo from "@/assets/layouts/logo.png?url"
import logoText1 from "@/assets/layouts/logo-text-1.png?url" import logoText1 from "@/assets/layouts/logo-text-1.png?url"
import logoText2 from "@/assets/layouts/logo-text-2.png?url" import logoText2 from "@/assets/layouts/logo-text-2.png?url"
import { useLayoutMode } from "@/hooks/useLayoutMode"
interface Props { interface Props {
collapse?: boolean collapse?: boolean
@ -16,13 +16,13 @@ const { isLeft, isTop } = useLayoutMode()
</script> </script>
<template> <template>
<div class="layout-logo-container" :class="{ collapse: props.collapse, 'layout-mode-top': isTop }"> <div class="layout-logo-container" :class="{ 'collapse': props.collapse, 'layout-mode-top': isTop }">
<transition name="layout-logo-fade"> <transition name="layout-logo-fade">
<router-link v-if="props.collapse" key="collapse" to="/"> <router-link v-if="props.collapse" key="collapse" to="/">
<img :src="logo" class="layout-logo" /> <img :src="logo" class="layout-logo">
</router-link> </router-link>
<router-link v-else key="expand" to="/"> <router-link v-else key="expand" to="/">
<img :src="!isLeft ? logoText2 : logoText1" class="layout-logo-text" /> <img :src="!isLeft ? logoText2 : logoText1" class="layout-logo-text">
</router-link> </router-link>
</transition> </transition>
</div> </div>

View File

@ -1,19 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useRouter } from "vue-router" import Notify from "@/components/Notify/index.vue"
import { storeToRefs } from "pinia" import Screenfull from "@/components/Screenfull/index.vue"
import SearchMenu from "@/components/SearchMenu/index.vue"
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
import { useDevice } from "@/hooks/useDevice"
import { useLayoutMode } from "@/hooks/useLayoutMode"
import { useAppStore } from "@/store/modules/app" import { useAppStore } from "@/store/modules/app"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import { useUserStore } from "@/store/modules/user" import { useUserStore } from "@/store/modules/user"
import { UserFilled } from "@element-plus/icons-vue" import { UserFilled } from "@element-plus/icons-vue"
import Hamburger from "../Hamburger/index.vue" import { storeToRefs } from "pinia"
import { useRouter } from "vue-router"
import Breadcrumb from "../Breadcrumb/index.vue" import Breadcrumb from "../Breadcrumb/index.vue"
import Hamburger from "../Hamburger/index.vue"
import Sidebar from "../Sidebar/index.vue" import Sidebar from "../Sidebar/index.vue"
import Notify from "@/components/Notify/index.vue"
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
import Screenfull from "@/components/Screenfull/index.vue"
import SearchMenu from "@/components/SearchMenu/index.vue"
import { useDevice } from "@/hooks/useDevice"
import { useLayoutMode } from "@/hooks/useLayoutMode"
const { isMobile } = useDevice() const { isMobile } = useDevice()
const { isTop } = useLayoutMode() const { isTop } = useLayoutMode()
@ -24,12 +24,12 @@ const settingsStore = useSettingsStore()
const { showNotify, showThemeSwitch, showScreenfull, showSearchMenu } = storeToRefs(settingsStore) const { showNotify, showThemeSwitch, showScreenfull, showSearchMenu } = storeToRefs(settingsStore)
/** 切换侧边栏 */ /** 切换侧边栏 */
const toggleSidebar = () => { function toggleSidebar() {
appStore.toggleSidebar(false) appStore.toggleSidebar(false)
} }
/** 登出 */ /** 登出 */
const logout = () => { function logout() {
userStore.logout() userStore.logout()
router.push("/login") router.push("/login")
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"
import { Setting } from "@element-plus/icons-vue" import { Setting } from "@element-plus/icons-vue"
import { ref } from "vue"
interface Props { interface Props {
buttonTop?: number buttonTop?: number
@ -10,7 +10,7 @@ const props = withDefaults(defineProps<Props>(), {
buttonTop: 350 buttonTop: 350
}) })
const buttonTopCss = props.buttonTop + "px" const buttonTopCss = `${props.buttonTop}px`
const show = ref(false) const show = ref(false)
</script> </script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLayoutMode } from "@/hooks/useLayoutMode"
import { LayoutModeEnum } from "@/constants/app-key" import { LayoutModeEnum } from "@/constants/app-key"
import { useLayoutMode } from "@/hooks/useLayoutMode"
const { isLeft, isTop, isLeftTop, setLayoutMode } = useLayoutMode() const { isLeft, isTop, isLeftTop, setLayoutMode } = useLayoutMode()
</script> </script>

View File

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watchEffect } from "vue"
import { storeToRefs } from "pinia"
import { useSettingsStore } from "@/store/modules/settings"
import { useLayoutMode } from "@/hooks/useLayoutMode" import { useLayoutMode } from "@/hooks/useLayoutMode"
import { useSettingsStore } from "@/store/modules/settings"
import { removeConfigLayout } from "@/utils/cache/local-storage" import { removeConfigLayout } from "@/utils/cache/local-storage"
import SelectLayoutMode from "./SelectLayoutMode.vue"
import { Refresh } from "@element-plus/icons-vue" import { Refresh } from "@element-plus/icons-vue"
import { storeToRefs } from "pinia"
import { watchEffect } from "vue"
import SelectLayoutMode from "./SelectLayoutMode.vue"
const { isLeft } = useLayoutMode() const { isLeft } = useLayoutMode()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
@ -28,18 +28,18 @@ const {
/** 定义 switch 设置项 */ /** 定义 switch 设置项 */
const switchSettings = { const switchSettings = {
显示标签栏: showTagsView, "显示标签栏": showTagsView,
"显示 Logo": showLogo, "显示 Logo": showLogo,
"固定 Header": fixedHeader, "固定 Header": fixedHeader,
"显示页脚 Footer": showFooter, "显示页脚 Footer": showFooter,
显示消息通知: showNotify, "显示消息通知": showNotify,
显示切换主题按钮: showThemeSwitch, "显示切换主题按钮": showThemeSwitch,
显示全屏按钮: showScreenfull, "显示全屏按钮": showScreenfull,
显示搜索按钮: showSearchMenu, "显示搜索按钮": showSearchMenu,
是否缓存标签栏: cacheTagsView, "是否缓存标签栏": cacheTagsView,
开启系统水印: showWatermark, "开启系统水印": showWatermark,
显示灰色模式: showGreyMode, "显示灰色模式": showGreyMode,
显示色弱模式: showColorWeakness "显示色弱模式": showColorWeakness
} }
/** 非左侧模式时Header 都是 fixed 布局 */ /** 非左侧模式时Header 都是 fixed 布局 */
@ -48,7 +48,7 @@ watchEffect(() => {
}) })
/** 重置项目配置 */ /** 重置项目配置 */
const resetConfigLayout = () => { function resetConfigLayout() {
removeConfigLayout() removeConfigLayout()
location.reload() location.reload()
} }
@ -60,11 +60,13 @@ const resetConfigLayout = () => {
<SelectLayoutMode /> <SelectLayoutMode />
<el-divider /> <el-divider />
<h4>功能配置</h4> <h4>功能配置</h4>
<div class="setting-item" v-for="(settingValue, settingName, index) in switchSettings" :key="index"> <div v-for="(settingValue, settingName, index) in switchSettings" :key="index" class="setting-item">
<span class="setting-name">{{ settingName }}</span> <span class="setting-name">{{ settingName }}</span>
<el-switch v-model="settingValue.value" :disabled="!isLeft && settingName === '固定 Header'" /> <el-switch v-model="settingValue.value" :disabled="!isLeft && settingName === '固定 Header'" />
</div> </div>
<el-button type="danger" :icon="Refresh" @click="resetConfigLayout"> </el-button> <el-button type="danger" :icon="Refresh" @click="resetConfigLayout">
</el-button>
</div> </div>
</template> </template>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue" import type { RouteRecordRaw } from "vue-router"
import { type RouteRecordRaw } from "vue-router"
import SidebarItemLink from "./SidebarItemLink.vue"
import { isExternal } from "@/utils/validate" import { isExternal } from "@/utils/validate"
import path from "path-browserify" import path from "path-browserify"
import { computed } from "vue"
import SidebarItemLink from "./SidebarItemLink.vue"
interface Props { interface Props {
item: RouteRecordRaw item: RouteRecordRaw
@ -19,7 +19,7 @@ const alwaysShowRootMenu = computed(() => props.item.meta?.alwaysShow)
/** 显示的子菜单 */ /** 显示的子菜单 */
const showingChildren = computed(() => { const showingChildren = computed(() => {
return props.item.children?.filter((child) => !child.meta?.hidden) ?? [] return props.item.children?.filter(child => !child.meta?.hidden) ?? []
}) })
/** 显示的子菜单数量 */ /** 显示的子菜单数量 */
@ -41,7 +41,7 @@ const theOnlyOneChild = computed(() => {
}) })
/** 解析路径 */ /** 解析路径 */
const resolvePath = (routePath: string) => { function resolvePath(routePath: string) {
switch (true) { switch (true) {
case isExternal(routePath): case isExternal(routePath):
return routePath return routePath
@ -58,7 +58,7 @@ const resolvePath = (routePath: string) => {
<SidebarItemLink v-if="theOnlyOneChild.meta" :to="resolvePath(theOnlyOneChild.path)"> <SidebarItemLink v-if="theOnlyOneChild.meta" :to="resolvePath(theOnlyOneChild.path)">
<el-menu-item :index="resolvePath(theOnlyOneChild.path)"> <el-menu-item :index="resolvePath(theOnlyOneChild.path)">
<SvgIcon v-if="theOnlyOneChild.meta.svgIcon" :name="theOnlyOneChild.meta.svgIcon" /> <SvgIcon v-if="theOnlyOneChild.meta.svgIcon" :name="theOnlyOneChild.meta.svgIcon" />
<component v-else-if="theOnlyOneChild.meta.elIcon" :is="theOnlyOneChild.meta.elIcon" class="el-icon" /> <component :is="theOnlyOneChild.meta.elIcon" v-else-if="theOnlyOneChild.meta.elIcon" class="el-icon" />
<template v-if="theOnlyOneChild.meta.title" #title> <template v-if="theOnlyOneChild.meta.title" #title>
{{ theOnlyOneChild.meta.title }} {{ theOnlyOneChild.meta.title }}
</template> </template>
@ -68,7 +68,7 @@ const resolvePath = (routePath: string) => {
<el-sub-menu v-else :index="resolvePath(props.item.path)" teleported> <el-sub-menu v-else :index="resolvePath(props.item.path)" teleported>
<template #title> <template #title>
<SvgIcon v-if="props.item.meta?.svgIcon" :name="props.item.meta.svgIcon" /> <SvgIcon v-if="props.item.meta?.svgIcon" :name="props.item.meta.svgIcon" />
<component v-else-if="props.item.meta?.elIcon" :is="props.item.meta.elIcon" class="el-icon" /> <component :is="props.item.meta.elIcon" v-else-if="props.item.meta?.elIcon" class="el-icon" />
<span v-if="props.item.meta?.title">{{ props.item.meta.title }}</span> <span v-if="props.item.meta?.title">{{ props.item.meta.title }}</span>
</template> </template>
<template v-if="props.item.children"> <template v-if="props.item.children">

View File

@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue" import { useDevice } from "@/hooks/useDevice"
import { useRoute } from "vue-router" import { useLayoutMode } from "@/hooks/useLayoutMode"
import { useAppStore } from "@/store/modules/app" import { useAppStore } from "@/store/modules/app"
import { usePermissionStore } from "@/store/modules/permission" import { usePermissionStore } from "@/store/modules/permission"
import { useSettingsStore } from "@/store/modules/settings" import { useSettingsStore } from "@/store/modules/settings"
import SidebarItem from "./SidebarItem.vue"
import Logo from "../Logo/index.vue"
import { useDevice } from "@/hooks/useDevice"
import { useLayoutMode } from "@/hooks/useLayoutMode"
import { getCssVar } from "@/utils/css" import { getCssVar } from "@/utils/css"
import { computed } from "vue"
import { useRoute } from "vue-router"
import Logo from "../Logo/index.vue"
import SidebarItem from "./SidebarItem.vue"
const v3SidebarMenuBgColor = getCssVar("--v3-sidebar-menu-bg-color") const v3SidebarMenuBgColor = getCssVar("--v3-sidebar-menu-bg-color")
const v3SidebarMenuTextColor = getCssVar("--v3-sidebar-menu-text-color") const v3SidebarMenuTextColor = getCssVar("--v3-sidebar-menu-text-color")
@ -26,9 +26,9 @@ const activeMenu = computed(() => {
meta: { activeMenu }, meta: { activeMenu },
path path
} = route } = route
return activeMenu ? activeMenu : path return activeMenu || path
}) })
const noHiddenRoutes = computed(() => permissionStore.routes.filter((item) => !item.meta?.hidden)) const noHiddenRoutes = computed(() => permissionStore.routes.filter(item => !item.meta?.hidden))
const isCollapse = computed(() => !appStore.sidebar.opened) const isCollapse = computed(() => !appStore.sidebar.opened)
const isLogo = computed(() => isLeft.value && settingsStore.showLogo) const isLogo = computed(() => isLeft.value && settingsStore.showLogo)
const backgroundColor = computed(() => (isLeft.value ? v3SidebarMenuBgColor : undefined)) const backgroundColor = computed(() => (isLeft.value ? v3SidebarMenuBgColor : undefined))
@ -63,7 +63,12 @@ const hiddenScrollbarVerticalBar = computed(() => {
:collapse-transition="false" :collapse-transition="false"
:mode="isTop && !isMobile ? 'horizontal' : 'vertical'" :mode="isTop && !isMobile ? 'horizontal' : 'vertical'"
> >
<SidebarItem v-for="route in noHiddenRoutes" :key="route.path" :item="route" :base-path="route.path" /> <SidebarItem
v-for="noHiddenRoute in noHiddenRoutes"
:key="noHiddenRoute.path"
:item="noHiddenRoute"
:base-path="noHiddenRoute.path"
/>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</div> </div>

View File

@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, nextTick } from "vue" import type { RouterLink } from "vue-router"
import { RouterLink, useRoute } from "vue-router"
import { useSettingsStore } from "@/store/modules/settings"
import { useRouteListener } from "@/hooks/useRouteListener"
import Screenfull from "@/components/Screenfull/index.vue" import Screenfull from "@/components/Screenfull/index.vue"
import { ElScrollbar } from "element-plus" import { useRouteListener } from "@/hooks/useRouteListener"
import { useSettingsStore } from "@/store/modules/settings"
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue" import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue"
import { ElScrollbar } from "element-plus"
import { nextTick, ref } from "vue"
import { useRoute } from "vue-router"
interface Props { interface Props {
tagRefs: InstanceType<typeof RouterLink>[] tagRefs: InstanceType<typeof RouterLink>[]
@ -28,13 +29,13 @@ let currentScrollLeft = 0
const translateDistance = 200 const translateDistance = 200
/** 滚动时触发 */ /** 滚动时触发 */
const scroll = ({ scrollLeft }: { scrollLeft: number }) => { function scroll({ scrollLeft }: { scrollLeft: number }) {
currentScrollLeft = scrollLeft currentScrollLeft = scrollLeft
} }
/** 鼠标滚轮滚动时触发 */ /** 鼠标滚轮滚动时触发 */
const wheelScroll = ({ deltaY }: WheelEvent) => { function wheelScroll({ deltaY }: WheelEvent) {
if (/^-/.test(deltaY.toString())) { if (deltaY.toString().startsWith("-")) {
scrollTo("left") scrollTo("left")
} else { } else {
scrollTo("right") scrollTo("right")
@ -42,7 +43,7 @@ const wheelScroll = ({ deltaY }: WheelEvent) => {
} }
/** 获取可能需要的宽度 */ /** 获取可能需要的宽度 */
const getWidth = () => { function getWidth() {
/** 可滚动内容的长度 */ /** 可滚动内容的长度 */
const scrollbarContentRefWidth = scrollbarContentRef.value!.clientWidth const scrollbarContentRefWidth = scrollbarContentRef.value!.clientWidth
/** 滚动可视区宽度 */ /** 滚动可视区宽度 */
@ -54,7 +55,7 @@ const getWidth = () => {
} }
/** 左右滚动 */ /** 左右滚动 */
const scrollTo = (direction: "left" | "right", distance: number = translateDistance) => { function scrollTo(direction: "left" | "right", distance: number = translateDistance) {
let scrollLeft = 0 let scrollLeft = 0
const { scrollbarContentRefWidth, scrollbarRefWidth, lastDistance } = getWidth() const { scrollbarContentRefWidth, scrollbarRefWidth, lastDistance } = getWidth()
// //
@ -68,12 +69,12 @@ const scrollTo = (direction: "left" | "right", distance: number = translateDista
} }
/** 移动到目标位置 */ /** 移动到目标位置 */
const moveTo = () => { function moveTo() {
const tagRefs = props.tagRefs const tagRefs = props.tagRefs
for (let i = 0; i < tagRefs.length; i++) { for (let i = 0; i < tagRefs.length; i++) {
// @ts-ignore // @ts-expect-error ignore
if (route.path === tagRefs[i].$props.to.path) { if (route.path === tagRefs[i].$props.to.path) {
// @ts-ignore // @ts-expect-error ignore
const el: HTMLElement = tagRefs[i].$el const el: HTMLElement = tagRefs[i].$el
const offsetWidth = el.offsetWidth const offsetWidth = el.offsetWidth
const offsetLeft = el.offsetLeft const offsetLeft = el.offsetLeft
@ -106,11 +107,11 @@ listenerRouteChange(() => {
<el-icon class="arrow left" @click="scrollTo('left')"> <el-icon class="arrow left" @click="scrollTo('left')">
<ArrowLeft /> <ArrowLeft />
</el-icon> </el-icon>
<el-scrollbar ref="scrollbarRef" @wheel.passive="wheelScroll" @scroll="scroll"> <ElScrollbar ref="scrollbarRef" @wheel.passive="wheelScroll" @scroll="scroll">
<div ref="scrollbarContentRef" class="scrollbar-content"> <div ref="scrollbarContentRef" class="scrollbar-content">
<slot /> <slot />
</div> </div>
</el-scrollbar> </ElScrollbar>
<el-icon class="arrow right" @click="scrollTo('right')"> <el-icon class="arrow right" @click="scrollTo('right')">
<ArrowRight /> <ArrowRight />
</el-icon> </el-icon>

View File

@ -1,12 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from "vue" import type { TagView } from "@/store/modules/tags-view"
import { type RouteLocationNormalizedLoaded, type RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router" import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from "vue-router"
import { type TagView, useTagsViewStore } from "@/store/modules/tags-view"
import { usePermissionStore } from "@/store/modules/permission"
import { useRouteListener } from "@/hooks/useRouteListener" import { useRouteListener } from "@/hooks/useRouteListener"
import path from "path-browserify" import { usePermissionStore } from "@/store/modules/permission"
import ScrollPane from "./ScrollPane.vue" import { useTagsViewStore } from "@/store/modules/tags-view"
import { Close } from "@element-plus/icons-vue" import { Close } from "@element-plus/icons-vue"
import path from "path-browserify"
import { ref, watch } from "vue"
import { RouterLink, useRoute, useRouter } from "vue-router"
import ScrollPane from "./ScrollPane.vue"
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -29,17 +31,17 @@ const selectedTag = ref<TagView>({})
let affixTags: TagView[] = [] let affixTags: TagView[] = []
/** 判断标签页是否激活 */ /** 判断标签页是否激活 */
const isActive = (tag: TagView) => { function isActive(tag: TagView) {
return tag.path === route.path return tag.path === route.path
} }
/** 判断标签页是否固定 */ /** 判断标签页是否固定 */
const isAffix = (tag: TagView) => { function isAffix(tag: TagView) {
return tag.meta?.affix return tag.meta?.affix
} }
/** 筛选出固定标签页 */ /** 筛选出固定标签页 */
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => { function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") {
const tags: TagView[] = [] const tags: TagView[] = []
routes.forEach((route) => { routes.forEach((route) => {
if (isAffix(route)) { if (isAffix(route)) {
@ -60,7 +62,7 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
} }
/** 初始化标签页 */ /** 初始化标签页 */
const initTags = () => { function initTags() {
affixTags = filterAffixTags(permissionStore.routes) affixTags = filterAffixTags(permissionStore.routes)
for (const tag of affixTags) { for (const tag of affixTags) {
// name // name
@ -69,7 +71,7 @@ const initTags = () => {
} }
/** 添加标签页 */ /** 添加标签页 */
const addTags = (route: RouteLocationNormalizedLoaded) => { function addTags(route: RouteLocationNormalizedLoaded) {
if (route.name) { if (route.name) {
tagsViewStore.addVisitedView(route) tagsViewStore.addVisitedView(route)
tagsViewStore.addCachedView(route) tagsViewStore.addCachedView(route)
@ -77,20 +79,20 @@ const addTags = (route: RouteLocationNormalizedLoaded) => {
} }
/** 刷新当前正在右键操作的标签页 */ /** 刷新当前正在右键操作的标签页 */
const refreshSelectedTag = (view: TagView) => { function refreshSelectedTag(view: TagView) {
tagsViewStore.delCachedView(view) tagsViewStore.delCachedView(view)
router.replace({ path: "/redirect" + view.path, query: view.query }) router.replace({ path: `/redirect${view.path}`, query: view.query })
} }
/** 关闭当前正在右键操作的标签页 */ /** 关闭当前正在右键操作的标签页 */
const closeSelectedTag = (view: TagView) => { function closeSelectedTag(view: TagView) {
tagsViewStore.delVisitedView(view) tagsViewStore.delVisitedView(view)
tagsViewStore.delCachedView(view) tagsViewStore.delCachedView(view)
isActive(view) && toLastView(tagsViewStore.visitedViews, view) isActive(view) && toLastView(tagsViewStore.visitedViews, view)
} }
/** 关闭其他标签页 */ /** 关闭其他标签页 */
const closeOthersTags = () => { function closeOthersTags() {
const fullPath = selectedTag.value.fullPath const fullPath = selectedTag.value.fullPath
if (fullPath !== route.path && fullPath !== undefined) { if (fullPath !== route.path && fullPath !== undefined) {
router.push(fullPath) router.push(fullPath)
@ -100,15 +102,15 @@ const closeOthersTags = () => {
} }
/** 关闭所有标签页 */ /** 关闭所有标签页 */
const closeAllTags = (view: TagView) => { function closeAllTags(view: TagView) {
tagsViewStore.delAllVisitedViews() tagsViewStore.delAllVisitedViews()
tagsViewStore.delAllCachedViews() tagsViewStore.delAllCachedViews()
if (affixTags.some((tag) => tag.path === route.path)) return if (affixTags.some(tag => tag.path === route.path)) return
toLastView(tagsViewStore.visitedViews, view) toLastView(tagsViewStore.visitedViews, view)
} }
/** 跳转到最后一个标签页 */ /** 跳转到最后一个标签页 */
const toLastView = (visitedViews: TagView[], view: TagView) => { function toLastView(visitedViews: TagView[], view: TagView) {
const latestView = visitedViews.slice(-1)[0] const latestView = visitedViews.slice(-1)[0]
const fullPath = latestView?.fullPath const fullPath = latestView?.fullPath
if (fullPath !== undefined) { if (fullPath !== undefined) {
@ -117,7 +119,7 @@ const toLastView = (visitedViews: TagView[], view: TagView) => {
// TagsView // TagsView
if (view.name === "Dashboard") { if (view.name === "Dashboard") {
// //
router.push({ path: "/redirect" + view.path, query: view.query }) router.push({ path: `/redirect${view.path}`, query: view.query })
} else { } else {
router.push("/") router.push("/")
} }
@ -125,7 +127,7 @@ const toLastView = (visitedViews: TagView[], view: TagView) => {
} }
/** 打开右键菜单面板 */ /** 打开右键菜单面板 */
const openMenu = (tag: TagView, e: MouseEvent) => { function openMenu(tag: TagView, e: MouseEvent) {
const menuMinWidth = 100 const menuMinWidth = 100
// //
const offsetWidth = document.body.offsetWidth const offsetWidth = document.body.offsetWidth
@ -142,7 +144,7 @@ const openMenu = (tag: TagView, e: MouseEvent) => {
} }
/** 关闭右键菜单面板 */ /** 关闭右键菜单面板 */
const closeMenu = () => { function closeMenu() {
visible.value = false visible.value = false
} }
@ -161,9 +163,9 @@ listenerRouteChange((route) => {
<template> <template>
<div class="tags-view-container"> <div class="tags-view-container">
<ScrollPane class="tags-view-wrapper" :tag-refs="tagRefs"> <ScrollPane class="tags-view-wrapper" :tag-refs="tagRefs">
<router-link <RouterLink
ref="tagRefs"
v-for="tag in tagsViewStore.visitedViews" v-for="tag in tagsViewStore.visitedViews"
ref="tagRefs"
:key="tag.path" :key="tag.path"
:class="{ active: isActive(tag) }" :class="{ active: isActive(tag) }"
class="tags-view-item" class="tags-view-item"
@ -175,13 +177,21 @@ listenerRouteChange((route) => {
<el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)"> <el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)">
<Close /> <Close />
</el-icon> </el-icon>
</router-link> </RouterLink>
</ScrollPane> </ScrollPane>
<ul v-show="visible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }"> <ul v-show="visible" class="contextmenu" :style="{ left: `${left}px`, top: `${top}px` }">
<li @click="refreshSelectedTag(selectedTag)">刷新</li> <li @click="refreshSelectedTag(selectedTag)">
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li> 刷新
<li @click="closeOthersTags">关闭其它</li> </li>
<li @click="closeAllTags(selectedTag)">关闭所有</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
关闭
</li>
<li @click="closeOthersTags">
关闭其它
</li>
<li @click="closeAllTags(selectedTag)">
关闭所有
</li>
</ul> </ul>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
export { default as AppMain } from "./AppMain.vue" export { default as AppMain } from "./AppMain.vue"
export { default as Logo } from "./Logo/index.vue"
export { default as NavigationBar } from "./NavigationBar/index.vue" export { default as NavigationBar } from "./NavigationBar/index.vue"
export { default as RightPanel } from "./RightPanel/index.vue"
export { default as Settings } from "./Settings/index.vue" export { default as Settings } from "./Settings/index.vue"
export { default as Sidebar } from "./Sidebar/index.vue" export { default as Sidebar } from "./Sidebar/index.vue"
export { default as TagsView } from "./TagsView/index.vue" export { default as TagsView } from "./TagsView/index.vue"
export { default as RightPanel } from "./RightPanel/index.vue"
export { default as Logo } from "./Logo/index.vue"

View File

@ -1,7 +1,7 @@
import { onBeforeMount, onMounted, onBeforeUnmount } from "vue"
import { useAppStore } from "@/store/modules/app"
import { useRouteListener } from "@/hooks/useRouteListener"
import { DeviceEnum } from "@/constants/app-key" import { DeviceEnum } from "@/constants/app-key"
import { useRouteListener } from "@/hooks/useRouteListener"
import { useAppStore } from "@/store/modules/app"
import { onBeforeMount, onBeforeUnmount, onMounted } from "vue"
/** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */ /** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */
const MAX_MOBILE_WIDTH = 992 const MAX_MOBILE_WIDTH = 992

View File

@ -1,16 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watchEffect } from "vue"
import { storeToRefs } from "pinia"
import { useSettingsStore } from "@/store/modules/settings"
import useResize from "./hooks/useResize"
import { useWatermark } from "@/hooks/useWatermark"
import { useDevice } from "@/hooks/useDevice" import { useDevice } from "@/hooks/useDevice"
import { useLayoutMode } from "@/hooks/useLayoutMode" import { useLayoutMode } from "@/hooks/useLayoutMode"
import LeftMode from "./LeftMode.vue" import { useWatermark } from "@/hooks/useWatermark"
import TopMode from "./TopMode.vue" import { useSettingsStore } from "@/store/modules/settings"
import LeftTopMode from "./LeftTopMode.vue"
import { Settings, RightPanel } from "./components"
import { getCssVar, setCssVar } from "@/utils/css" import { getCssVar, setCssVar } from "@/utils/css"
import { storeToRefs } from "pinia"
import { watchEffect } from "vue"
import { RightPanel, Settings } from "./components"
import useResize from "./hooks/useResize"
import LeftMode from "./LeftMode.vue"
import LeftTopMode from "./LeftTopMode.vue"
import TopMode from "./TopMode.vue"
/** Layout 布局响应式 */ /** Layout 布局响应式 */
useResize() useResize()
@ -21,13 +21,13 @@ const { isLeft, isTop, isLeftTop } = useLayoutMode()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const { showSettings, showTagsView, showWatermark } = storeToRefs(settingsStore) const { showSettings, showTagsView, showWatermark } = storeToRefs(settingsStore)
//#region Logo Header // #region Logo Header
const cssVarName = "--v3-tagsview-height" const cssVarName = "--v3-tagsview-height"
const v3TagsviewHeight = getCssVar(cssVarName) const v3TagsviewHeight = getCssVar(cssVarName)
watchEffect(() => { watchEffect(() => {
showTagsView.value ? setCssVar(cssVarName, v3TagsviewHeight) : setCssVar(cssVarName, "0px") showTagsView.value ? setCssVar(cssVarName, v3TagsviewHeight) : setCssVar(cssVarName, "0px")
}) })
//#endregion // #endregion
/** 开启或关闭系统水印 */ /** 开启或关闭系统水印 */
watchEffect(() => { watchEffect(() => {

View File

@ -1,13 +1,13 @@
// core // core
import App from "@/App.vue" import App from "@/App.vue"
import { createApp } from "vue" import { loadDirectives } from "@/directives"
import { pinia } from "@/store"
import { router } from "@/router" import { router } from "@/router"
import { pinia } from "@/store"
import { createApp } from "vue"
import "@/router/permission" import "@/router/permission"
// load // load
import { loadSvg } from "@/icons" import { loadSvg } from "@/icons"
import { loadPlugins } from "@/plugins" import { loadPlugins } from "@/plugins"
import { loadDirectives } from "@/directives"
// css // css
import "uno.css" import "uno.css"
import "normalize.css" import "normalize.css"

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
import * as ElementPlusIconsVue from "@element-plus/icons-vue" import * as ElementPlusIconsVue from "@element-plus/icons-vue"
export function loadElementPlusIcon(app: App) { export function loadElementPlusIcon(app: App) {

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
import ElementPlus from "element-plus" import ElementPlus from "element-plus"
export function loadElementPlus(app: App) { export function loadElementPlus(app: App) {

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
import { loadElementPlus } from "./element-plus" import { loadElementPlus } from "./element-plus"
import { loadElementPlusIcon } from "./element-plus-icon" import { loadElementPlusIcon } from "./element-plus-icon"
import { loadVxeTable } from "./vxe-table" import { loadVxeTable } from "./vxe-table"

View File

@ -1,4 +1,4 @@
import { type App } from "vue" import type { App } from "vue"
// https://vxetable.cn/#/table/start/install // https://vxetable.cn/#/table/start/install
import VXETable from "vxe-table" import VXETable from "vxe-table"
// https://github.com/x-extends/vxe-table-plugin-element // https://github.com/x-extends/vxe-table-plugin-element

View File

@ -1,21 +1,21 @@
import { cloneDeep, omit } from "lodash-es"
import { import {
type Router,
type RouteRecordNormalized,
type RouteRecordRaw,
createRouter, createRouter,
createWebHashHistory, createWebHashHistory,
createWebHistory createWebHistory,
type Router,
type RouteRecordNormalized,
type RouteRecordRaw
} from "vue-router" } from "vue-router"
import { cloneDeep, omit } from "lodash-es"
/** 路由模式 */ /** 路由模式 */
export const history = export const history
import.meta.env.VITE_ROUTER_HISTORY === "hash" = import.meta.env.VITE_ROUTER_HISTORY === "hash"
? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH) ? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH)
: createWebHistory(import.meta.env.VITE_PUBLIC_PATH) : createWebHistory(import.meta.env.VITE_PUBLIC_PATH)
/** 路由降级(把三级及其以上的路由转化为二级路由) */ /** 路由降级(把三级及其以上的路由转化为二级路由) */
export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => { export function flatMultiLevelRoutes(routes: RouteRecordRaw[]) {
const routesMirror = cloneDeep(routes) const routesMirror = cloneDeep(routes)
routesMirror.forEach((route) => { routesMirror.forEach((route) => {
// 如果路由是三级及其以上路由,对其进行降级处理 // 如果路由是三级及其以上路由,对其进行降级处理
@ -25,17 +25,17 @@ export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
} }
/** 判断路由层级是否大于 2 */ /** 判断路由层级是否大于 2 */
const isMultipleRoute = (route: RouteRecordRaw) => { function isMultipleRoute(route: RouteRecordRaw) {
const children = route.children const children = route.children
if (children?.length) { if (children?.length) {
// 只要有一个子路由的 children 长度大于 0就说明是三级及其以上路由 // 只要有一个子路由的 children 长度大于 0就说明是三级及其以上路由
return children.some((child) => child.children?.length) return children.some(child => child.children?.length)
} }
return false return false
} }
/** 生成二级路由 */ /** 生成二级路由 */
const promoteRouteLevel = (route: RouteRecordRaw) => { function promoteRouteLevel(route: RouteRecordRaw) {
// 创建 router 实例是为了获取到当前传入的 route 的所有路由信息 // 创建 router 实例是为了获取到当前传入的 route 的所有路由信息
let router: Router | null = createRouter({ let router: Router | null = createRouter({
history, history,
@ -46,13 +46,13 @@ const promoteRouteLevel = (route: RouteRecordRaw) => {
addToChildren(routes, route.children || [], route) addToChildren(routes, route.children || [], route)
router = null router = null
// 转为二级路由后,去除所有子路由中的 children // 转为二级路由后,去除所有子路由中的 children
route.children = route.children?.map((item) => omit(item, "children") as RouteRecordRaw) route.children = route.children?.map(item => omit(item, "children") as RouteRecordRaw)
} }
/** 将给定的子路由添加到指定的路由模块中 */ /** 将给定的子路由添加到指定的路由模块中 */
const addToChildren = (routes: RouteRecordNormalized[], children: RouteRecordRaw[], routeModule: RouteRecordRaw) => { function addToChildren(routes: RouteRecordNormalized[], children: RouteRecordRaw[], routeModule: RouteRecordRaw) {
children.forEach((child) => { children.forEach((child) => {
const route = routes.find((item) => item.name === child.name) const route = routes.find(item => item.name === child.name)
if (route) { if (route) {
// 初始化 routeModule 的 children // 初始化 routeModule 的 children
routeModule.children = routeModule.children || [] routeModule.children = routeModule.children || []

View File

@ -1,6 +1,6 @@
import { type RouteRecordRaw, createRouter } from "vue-router"
import { history, flatMultiLevelRoutes } from "./helper"
import routeSettings from "@/config/route" import routeSettings from "@/config/route"
import { createRouter, type RouteRecordRaw } from "vue-router"
import { flatMultiLevelRoutes, history } from "./helper"
const Layouts = () => import("@/layouts/index.vue") const Layouts = () => import("@/layouts/index.vue")
@ -304,7 +304,8 @@ export function resetRouter() {
router.hasRoute(name) && router.removeRoute(name) router.hasRoute(name) && router.removeRoute(name)
} }
}) })
} catch { }
catch {
// 强制刷新浏览器也行,只是交互体验不是很好 // 强制刷新浏览器也行,只是交互体验不是很好
window.location.reload() window.location.reload()
} }

View File

@ -1,12 +1,12 @@
import { router } from "@/router"
import { useUserStoreHook } from "@/store/modules/user"
import { usePermissionStoreHook } from "@/store/modules/permission"
import { ElMessage } from "element-plus"
import { setRouteChange } from "@/hooks/useRouteListener"
import { useTitle } from "@/hooks/useTitle"
import { getToken } from "@/utils/cache/cookies"
import routeSettings from "@/config/route" import routeSettings from "@/config/route"
import isWhiteList from "@/config/white-list" import isWhiteList from "@/config/white-list"
import { setRouteChange } from "@/hooks/useRouteListener"
import { useTitle } from "@/hooks/useTitle"
import { router } from "@/router"
import { usePermissionStoreHook } from "@/store/modules/permission"
import { useUserStoreHook } from "@/store/modules/user"
import { getToken } from "@/utils/cache/cookies"
import { ElMessage } from "element-plus"
import NProgress from "nprogress" import NProgress from "nprogress"
import "nprogress/nprogress.css" import "nprogress/nprogress.css"
@ -41,10 +41,11 @@ router.beforeEach(async (to, _from, next) => {
// 生成可访问的 Routes // 生成可访问的 Routes
routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes() routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
// 将 "有访问权限的动态路由" 添加到 Router 中 // 将 "有访问权限的动态路由" 添加到 Router 中
permissionStore.addRoutes.forEach((route) => router.addRoute(route)) permissionStore.addRoutes.forEach(route => router.addRoute(route))
// 设置 replace: true, 因此导航将不会留下历史记录 // 设置 replace: true, 因此导航将不会留下历史记录
next({ ...to, replace: true }) next({ ...to, replace: true })
} catch (error) { }
catch (error) {
// 过程中发生任何错误,都直接重置 Token并重定向到登录页面 // 过程中发生任何错误,都直接重置 Token并重定向到登录页面
userStore.resetToken() userStore.resetToken()
ElMessage.error((error as Error).message || "路由守卫过程发生错误") ElMessage.error((error as Error).message || "路由守卫过程发生错误")

View File

@ -1,8 +1,8 @@
import { reactive, ref, watch } from "vue" import { DeviceEnum, SIDEBAR_CLOSED, SIDEBAR_OPENED } from "@/constants/app-key"
import { pinia } from "@/store" import { pinia } from "@/store"
import { defineStore } from "pinia"
import { getSidebarStatus, setSidebarStatus } from "@/utils/cache/local-storage" import { getSidebarStatus, setSidebarStatus } from "@/utils/cache/local-storage"
import { DeviceEnum, SIDEBAR_OPENED, SIDEBAR_CLOSED } from "@/constants/app-key" import { defineStore } from "pinia"
import { reactive, ref, watch } from "vue"
interface Sidebar { interface Sidebar {
opened: boolean opened: boolean
@ -26,7 +26,7 @@ export const useAppStore = defineStore("app", () => {
/** 监听侧边栏 opened 状态 */ /** 监听侧边栏 opened 状态 */
watch( watch(
() => sidebar.opened, () => sidebar.opened,
(opened) => handleSidebarStatus(opened) opened => handleSidebarStatus(opened)
) )
/** 切换侧边栏 */ /** 切换侧边栏 */

View File

@ -1,17 +1,17 @@
import { ref } from "vue" import type { RouteRecordRaw } from "vue-router"
import { pinia } from "@/store" import routeSettings from "@/config/route"
import { defineStore } from "pinia"
import { type RouteRecordRaw } from "vue-router"
import { constantRoutes, dynamicRoutes } from "@/router" import { constantRoutes, dynamicRoutes } from "@/router"
import { flatMultiLevelRoutes } from "@/router/helper" import { flatMultiLevelRoutes } from "@/router/helper"
import routeSettings from "@/config/route" import { pinia } from "@/store"
import { defineStore } from "pinia"
import { ref } from "vue"
const hasPermission = (roles: string[], route: RouteRecordRaw) => { function hasPermission(roles: string[], route: RouteRecordRaw) {
const routeRoles = route.meta?.roles const routeRoles = route.meta?.roles
return routeRoles ? roles.some((role) => routeRoles.includes(role)) : true return routeRoles ? roles.some(role => routeRoles.includes(role)) : true
} }
const filterDynamicRoutes = (routes: RouteRecordRaw[], roles: string[]) => { function filterDynamicRoutes(routes: RouteRecordRaw[], roles: string[]) {
const res: RouteRecordRaw[] = [] const res: RouteRecordRaw[] = []
routes.forEach((route) => { routes.forEach((route) => {
const tempRoute = { ...route } const tempRoute = { ...route }

View File

@ -1,8 +1,10 @@
import { type Ref, ref, watch } from "vue" import type { LayoutSettings } from "@/config/layouts"
import type { Ref } from "vue"
import { layoutSettings } from "@/config/layouts"
import { pinia } from "@/store" import { pinia } from "@/store"
import { defineStore } from "pinia"
import { type LayoutSettings, layoutSettings } from "@/config/layouts"
import { setConfigLayout } from "@/utils/cache/local-storage" import { setConfigLayout } from "@/utils/cache/local-storage"
import { defineStore } from "pinia"
import { ref, watch } from "vue"
type SettingsStore = { type SettingsStore = {
// 使用映射类型来遍历 layoutSettings 对象的键 // 使用映射类型来遍历 layoutSettings 对象的键
@ -18,7 +20,7 @@ export const useSettingsStore = defineStore("settings", () => {
for (const [key, value] of Object.entries(layoutSettings)) { for (const [key, value] of Object.entries(layoutSettings)) {
// 使用类型断言来指定 key 的类型,将 value 包装在 ref 函数中,创建一个响应式变量 // 使用类型断言来指定 key 的类型,将 value 包装在 ref 函数中,创建一个响应式变量
const refValue = ref(value) const refValue = ref(value)
// @ts-ignore // @ts-expect-error ignore
state[key as SettingsStoreKey] = refValue state[key as SettingsStoreKey] = refValue
// 监听每个响应式变量 // 监听每个响应式变量
watch(refValue, () => { watch(refValue, () => {
@ -31,7 +33,7 @@ export const useSettingsStore = defineStore("settings", () => {
const _getCacheData = () => { const _getCacheData = () => {
const settings = {} as LayoutSettings const settings = {} as LayoutSettings
for (const [key, value] of Object.entries(state)) { for (const [key, value] of Object.entries(state)) {
// @ts-ignore // @ts-expect-error ignore
settings[key as SettingsStoreKey] = value.value settings[key as SettingsStoreKey] = value.value
} }
return settings return settings

View File

@ -1,9 +1,9 @@
import { ref, watchEffect } from "vue" import type { RouteLocationNormalized } from "vue-router"
import { pinia } from "@/store" import { pinia } from "@/store"
import { getCachedViews, getVisitedViews, setCachedViews, setVisitedViews } from "@/utils/cache/local-storage"
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { ref, watchEffect } from "vue"
import { useSettingsStore } from "./settings" import { useSettingsStore } from "./settings"
import { type RouteLocationNormalized } from "vue-router"
import { getVisitedViews, setVisitedViews, getCachedViews, setCachedViews } from "@/utils/cache/local-storage"
export type TagView = Partial<RouteLocationNormalized> export type TagView = Partial<RouteLocationNormalized>
@ -18,10 +18,10 @@ export const useTagsViewStore = defineStore("tags-view", () => {
setCachedViews(cachedViews.value) setCachedViews(cachedViews.value)
}) })
//#region add // #region add
const addVisitedView = (view: TagView) => { const addVisitedView = (view: TagView) => {
// 检查是否已经存在相同的 visitedView // 检查是否已经存在相同的 visitedView
const index = visitedViews.value.findIndex((v) => v.path === view.path) const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index !== -1) { if (index !== -1) {
// 防止 query 参数丢失 // 防止 query 参数丢失
visitedViews.value[index].fullPath !== view.fullPath && (visitedViews.value[index] = { ...view }) visitedViews.value[index].fullPath !== view.fullPath && (visitedViews.value[index] = { ...view })
@ -34,24 +34,27 @@ export const useTagsViewStore = defineStore("tags-view", () => {
const addCachedView = (view: TagView) => { const addCachedView = (view: TagView) => {
if (typeof view.name !== "string") return if (typeof view.name !== "string") return
if (cachedViews.value.includes(view.name)) return if (cachedViews.value.includes(view.name)) return
if (view.meta?.keepAlive) cachedViews.value.push(view.name) if (view.meta?.keepAlive)
cachedViews.value.push(view.name)
} }
//#endregion // #endregion
//#region del // #region del
const delVisitedView = (view: TagView) => { const delVisitedView = (view: TagView) => {
const index = visitedViews.value.findIndex((v) => v.path === view.path) const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index !== -1) visitedViews.value.splice(index, 1) if (index !== -1)
visitedViews.value.splice(index, 1)
} }
const delCachedView = (view: TagView) => { const delCachedView = (view: TagView) => {
if (typeof view.name !== "string") return if (typeof view.name !== "string") return
const index = cachedViews.value.indexOf(view.name) const index = cachedViews.value.indexOf(view.name)
if (index !== -1) cachedViews.value.splice(index, 1) if (index !== -1)
cachedViews.value.splice(index, 1)
} }
//#endregion // #endregion
//#region delOthers // #region delOthers
const delOthersVisitedViews = (view: TagView) => { const delOthersVisitedViews = (view: TagView) => {
visitedViews.value = visitedViews.value.filter((v) => { visitedViews.value = visitedViews.value.filter((v) => {
return v.meta?.affix || v.path === view.path return v.meta?.affix || v.path === view.path
@ -68,18 +71,18 @@ export const useTagsViewStore = defineStore("tags-view", () => {
cachedViews.value = [] cachedViews.value = []
} }
} }
//#endregion // #endregion
//#region delAll // #region delAll
const delAllVisitedViews = () => { const delAllVisitedViews = () => {
// 保留固定的 tags // 保留固定的 tags
visitedViews.value = visitedViews.value.filter((tag) => tag.meta?.affix) visitedViews.value = visitedViews.value.filter(tag => tag.meta?.affix)
} }
const delAllCachedViews = () => { const delAllCachedViews = () => {
cachedViews.value = [] cachedViews.value = []
} }
//#endregion // #endregion
return { return {
visitedViews, visitedViews,

View File

@ -1,13 +1,13 @@
import { ref } from "vue" import type { LoginRequestData } from "@/api/login/types/login"
import { pinia } from "@/store" import { getUserInfoApi, loginApi } from "@/api/login"
import { defineStore } from "pinia"
import { useTagsViewStore } from "./tags-view"
import { useSettingsStore } from "./settings"
import { getToken, removeToken, setToken } from "@/utils/cache/cookies"
import { resetRouter } from "@/router"
import { loginApi, getUserInfoApi } from "@/api/login"
import { type LoginRequestData } from "@/api/login/types/login"
import routeSettings from "@/config/route" import routeSettings from "@/config/route"
import { resetRouter } from "@/router"
import { pinia } from "@/store"
import { getToken, removeToken, setToken } from "@/utils/cache/cookies"
import { defineStore } from "pinia"
import { ref } from "vue"
import { useSettingsStore } from "./settings"
import { useTagsViewStore } from "./tags-view"
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const token = ref<string>(getToken() || "") const token = ref<string>(getToken() || "")
@ -32,7 +32,7 @@ export const useUserStore = defineStore("user", () => {
} }
/** 模拟角色变化 */ /** 模拟角色变化 */
const changeRoles = async (role: string) => { const changeRoles = async (role: string) => {
const newToken = "token-" + role const newToken = `token-${role}`
token.value = newToken token.value = newToken
setToken(newToken) setToken(newToken)
// 用刷新页面代替重新登录 // 用刷新页面代替重新登录

View File

@ -3,12 +3,12 @@
import CacheKey from "@/constants/cache-key" import CacheKey from "@/constants/cache-key"
import Cookies from "js-cookie" import Cookies from "js-cookie"
export const getToken = () => { export function getToken() {
return Cookies.get(CacheKey.TOKEN) return Cookies.get(CacheKey.TOKEN)
} }
export const setToken = (token: string) => { export function setToken(token: string) {
Cookies.set(CacheKey.TOKEN, token) Cookies.set(CacheKey.TOKEN, token)
} }
export const removeToken = () => { export function removeToken() {
Cookies.remove(CacheKey.TOKEN) Cookies.remove(CacheKey.TOKEN)
} }

View File

@ -1,48 +1,48 @@
/** 统一处理 localStorage */ /** 统一处理 localStorage */
import type { LayoutSettings } from "@/config/layouts"
import type { SidebarClosed, SidebarOpened } from "@/constants/app-key"
import type { ThemeName } from "@/hooks/useTheme"
import type { TagView } from "@/store/modules/tags-view"
import CacheKey from "@/constants/cache-key" import CacheKey from "@/constants/cache-key"
import { type SidebarOpened, type SidebarClosed } from "@/constants/app-key"
import { type ThemeName } from "@/hooks/useTheme"
import { type TagView } from "@/store/modules/tags-view"
import { type LayoutSettings } from "@/config/layouts"
//#region 系统布局配置 // #region 系统布局配置
export const getConfigLayout = () => { export function getConfigLayout() {
const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT) const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT)
return json ? (JSON.parse(json) as LayoutSettings) : null return json ? (JSON.parse(json) as LayoutSettings) : null
} }
export const setConfigLayout = (settings: LayoutSettings) => { export function setConfigLayout(settings: LayoutSettings) {
localStorage.setItem(CacheKey.CONFIG_LAYOUT, JSON.stringify(settings)) localStorage.setItem(CacheKey.CONFIG_LAYOUT, JSON.stringify(settings))
} }
export const removeConfigLayout = () => { export function removeConfigLayout() {
localStorage.removeItem(CacheKey.CONFIG_LAYOUT) localStorage.removeItem(CacheKey.CONFIG_LAYOUT)
} }
//#endregion // #endregion
//#region 侧边栏状态 // #region 侧边栏状态
export const getSidebarStatus = () => { export function getSidebarStatus() {
return localStorage.getItem(CacheKey.SIDEBAR_STATUS) return localStorage.getItem(CacheKey.SIDEBAR_STATUS)
} }
export const setSidebarStatus = (sidebarStatus: SidebarOpened | SidebarClosed) => { export function setSidebarStatus(sidebarStatus: SidebarOpened | SidebarClosed) {
localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus) localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus)
} }
//#endregion // #endregion
//#region 正在应用的主题名称 // #region 正在应用的主题名称
export const getActiveThemeName = () => { export function getActiveThemeName() {
return localStorage.getItem(CacheKey.ACTIVE_THEME_NAME) as ThemeName | null return localStorage.getItem(CacheKey.ACTIVE_THEME_NAME) as ThemeName | null
} }
export const setActiveThemeName = (themeName: ThemeName) => { export function setActiveThemeName(themeName: ThemeName) {
localStorage.setItem(CacheKey.ACTIVE_THEME_NAME, themeName) localStorage.setItem(CacheKey.ACTIVE_THEME_NAME, themeName)
} }
//#endregion // #endregion
//#region 标签栏 // #region 标签栏
export const getVisitedViews = () => { export function getVisitedViews() {
const json = localStorage.getItem(CacheKey.VISITED_VIEWS) const json = localStorage.getItem(CacheKey.VISITED_VIEWS)
return JSON.parse(json ?? "[]") as TagView[] return JSON.parse(json ?? "[]") as TagView[]
} }
export const setVisitedViews = (views: TagView[]) => { export function setVisitedViews(views: TagView[]) {
views.forEach((view) => { views.forEach((view) => {
// 删除不必要的属性,防止 JSON.stringify 处理到循环引用 // 删除不必要的属性,防止 JSON.stringify 处理到循环引用
delete view.matched delete view.matched
@ -50,11 +50,11 @@ export const setVisitedViews = (views: TagView[]) => {
}) })
localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views)) localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views))
} }
export const getCachedViews = () => { export function getCachedViews() {
const json = localStorage.getItem(CacheKey.CACHED_VIEWS) const json = localStorage.getItem(CacheKey.CACHED_VIEWS)
return JSON.parse(json ?? "[]") as string[] return JSON.parse(json ?? "[]") as string[]
} }
export const setCachedViews = (views: string[]) => { export function setCachedViews(views: string[]) {
localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views)) localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views))
} }
//#endregion // #endregion

View File

@ -1,5 +1,5 @@
/** 获取指定元素(默认全局)上的 CSS 变量的值 */ /** 获取指定元素(默认全局)上的 CSS 变量的值 */
export const getCssVar = (varName: string, element: HTMLElement = document.documentElement) => { export function getCssVar(varName: string, element: HTMLElement = document.documentElement) {
if (!varName?.startsWith("--")) { if (!varName?.startsWith("--")) {
console.warn("CSS 变量名应以 '--' 开头") console.warn("CSS 变量名应以 '--' 开头")
return "" return ""
@ -9,7 +9,7 @@ export const getCssVar = (varName: string, element: HTMLElement = document.docum
} }
/** 设置指定元素(默认全局)上的 CSS 变量的值 */ /** 设置指定元素(默认全局)上的 CSS 变量的值 */
export const setCssVar = (varName: string, value: string, element: HTMLElement = document.documentElement) => { export function setCssVar(varName: string, value: string, element: HTMLElement = document.documentElement) {
if (!varName?.startsWith("--")) { if (!varName?.startsWith("--")) {
console.warn("CSS 变量名应以 '--' 开头") console.warn("CSS 变量名应以 '--' 开头")
return return

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs"
const INVALID_DATE = "N/A" const INVALID_DATE = "N/A"
/** 格式化日期时间 */ /** 格式化日期时间 */
export const formatDateTime = (datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") => { export function formatDateTime(datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") {
const day = dayjs(datetime) const day = dayjs(datetime)
return day.isValid() ? day.format(template) : INVALID_DATE return day.isValid() ? day.format(template) : INVALID_DATE
} }

View File

@ -1,10 +1,10 @@
import { useUserStore } from "@/store/modules/user" import { useUserStore } from "@/store/modules/user"
/** 全局权限判断函数,和权限指令 v-permission 功能类似 */ /** 全局权限判断函数,和权限指令 v-permission 功能类似 */
export const checkPermission = (permissionRoles: string[]): boolean => { export function checkPermission(permissionRoles: string[]): boolean {
if (Array.isArray(permissionRoles) && permissionRoles.length > 0) { if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
const { roles } = useUserStore() const { roles } = useUserStore()
return roles.some((role) => permissionRoles.includes(role)) return roles.some(role => permissionRoles.includes(role))
} else { } else {
console.error("need roles! Like checkPermission(['admin','editor'])") console.error("need roles! Like checkPermission(['admin','editor'])")
return false return false

View File

@ -1,5 +1,5 @@
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"
import { useUserStore } from "@/store/modules/user" import { useUserStore } from "@/store/modules/user"
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"
import { ElMessage } from "element-plus" import { ElMessage } from "element-plus"
import { get, merge } from "lodash-es" import { get, merge } from "lodash-es"
import { getToken } from "./cache/cookies" import { getToken } from "./cache/cookies"
@ -16,9 +16,9 @@ function createService() {
const service = axios.create() const service = axios.create()
// 请求拦截 // 请求拦截
service.interceptors.request.use( service.interceptors.request.use(
(config) => config, config => config,
// 发送失败 // 发送失败
(error) => Promise.reject(error) error => Promise.reject(error)
) )
// 响应拦截(可根据具体业务作出相应的调整) // 响应拦截(可根据具体业务作出相应的调整)
service.interceptors.response.use( service.interceptors.response.use(
@ -27,7 +27,8 @@ function createService() {
const apiData = response.data const apiData = response.data
// 二进制数据则直接返回 // 二进制数据则直接返回
const responseType = response.request?.responseType const responseType = response.request?.responseType
if (responseType === "blob" || responseType === "arraybuffer") return apiData if (responseType === "blob" || responseType === "arraybuffer")
return apiData
// 这个 code 是和后端约定的业务 code // 这个 code 是和后端约定的业务 code
const code = apiData.code const code = apiData.code
// 如果没有 code, 代表这不是项目后端开发的 api // 如果没有 code, 代表这不是项目后端开发的 api
@ -103,7 +104,7 @@ function createRequest(service: AxiosInstance) {
const defaultConfig = { const defaultConfig = {
headers: { headers: {
// 携带 Token // 携带 Token
Authorization: token ? `Bearer ${token}` : undefined, "Authorization": token ? `Bearer ${token}` : undefined,
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
timeout: 5000, timeout: 5000,

View File

@ -1,84 +1,84 @@
/** 判断是否为数组 */ /** 判断是否为数组 */
export const isArray = <T>(arg: T) => { export function isArray<T>(arg: T) {
return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]" return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]"
} }
/** 判断是否为字符串 */ /** 判断是否为字符串 */
export const isString = <T>(str: T) => { export function isString<T>(str: T) {
return typeof str === "string" || str instanceof String return typeof str === "string" || str instanceof String
} }
/** 判断是否为外链 */ /** 判断是否为外链 */
export const isExternal = (path: string) => { export function isExternal(path: string) {
const reg = /^(https?:|mailto:|tel:)/ const reg = /^(https?:|mailto:|tel:)/
return reg.test(path) return reg.test(path)
} }
/** 判断是否为网址(带协议) */ /** 判断是否为网址(带协议) */
export const isUrl = (url: string) => { export function isUrl(url: string) {
const reg = /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/ const reg = /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{1,64})?\.)+[a-z]{2,6}\/?/
return reg.test(url) return reg.test(url)
} }
/** 判断是否为网址或 IP带端口 */ /** 判断是否为网址或 IP带端口 */
export const isUrlPort = (url: string) => { export function isUrlPort(url: string) {
const reg = /^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/ const reg = /^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/
return reg.test(url) return reg.test(url)
} }
/** 判断是否为域名(不带协议) */ /** 判断是否为域名(不带协议) */
export const isDomain = (domain: string) => { export function isDomain(domain: string) {
const reg = /^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/ const reg = /^([0-9a-z-]+\.)+([a-z]{2,})$/i
return reg.test(domain) return reg.test(domain)
} }
/** 判断版本号格式是否为 X.Y.Z */ /** 判断版本号格式是否为 X.Y.Z */
export const isVersion = (version: string) => { export function isVersion(version: string) {
const reg = /^\d+(?:\.\d+){2}$/ const reg = /^\d+(?:\.\d+){2}$/
return reg.test(version) return reg.test(version)
} }
/** 判断时间格式是否为 24 小时制HH:mm:ss */ /** 判断时间格式是否为 24 小时制HH:mm:ss */
export const is24H = (time: string) => { export function is24H(time: string) {
const reg = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/ const reg = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
return reg.test(time) return reg.test(time)
} }
/** 判断是否为手机号1 开头) */ /** 判断是否为手机号1 开头) */
export const isPhoneNumber = (str: string) => { export function isPhoneNumber(str: string) {
const reg = /^(?:(?:\+|00)86)?1\d{10}$/ const reg = /^(?:(?:\+|00)86)?1\d{10}$/
return reg.test(str) return reg.test(str)
} }
/** 判断是否为第二代身份证18 位) */ /** 判断是否为第二代身份证18 位) */
export const isChineseIdCard = (str: string) => { export function isChineseIdCard(str: string) {
const reg = /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/ const reg = /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[12]\d|30|31)\d{3}[\dX]$/i
return reg.test(str) return reg.test(str)
} }
/** 判断是否为 Email支持中文邮箱 */ /** 判断是否为 Email支持中文邮箱 */
export const isEmail = (email: string) => { export function isEmail(email: string) {
const reg = /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ const reg = /^[A-Z0-9\u4E00-\u9FA5]+@[\w-]+(\.[\w-]+)+$/i
return reg.test(email) return reg.test(email)
} }
/** 判断是否为 MAC 地址 */ /** 判断是否为 MAC 地址 */
export const isMAC = (mac: string) => { export function isMAC(mac: string) {
const reg = const reg
/^(([a-f0-9][0,2,4,6,8,a,c,e]:([a-f0-9]{2}:){4})|([a-f0-9][0,2,4,6,8,a,c,e]-([a-f0-9]{2}-){4}))[a-f0-9]{2}$/i = /^(([a-f0-9][0,2468ace]:([a-f0-9]{2}:){4})|([a-f0-9][0,2468ace]-([a-f0-9]{2}-){4}))[a-f0-9]{2}$/i
return reg.test(mac) return reg.test(mac)
} }
/** 判断是否为 IPv4 地址 */ /** 判断是否为 IPv4 地址 */
export const isIPv4 = (ip: string) => { export function isIPv4(ip: string) {
const reg = const reg
/^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/ = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))?$/
return reg.test(ip) return reg.test(ip)
} }
/** 判断是否为车牌(兼容新能源车牌) */ /** 判断是否为车牌(兼容新能源车牌) */
export const isLicensePlate = (str: string) => { export function isLicensePlate(str: string) {
const reg = const reg
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/ = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/
return reg.test(str) return reg.test(str)
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
import Svg403 from "@/assets/error-page/403.svg?component" // vite-svg-loader import Svg403 from "@/assets/error-page/403.svg?component" // vite-svg-loader
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
</script> </script>
<template> <template>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
import Svg404 from "@/assets/error-page/404.svg?component" // vite-svg-loader import Svg404 from "@/assets/error-page/404.svg?component" // vite-svg-loader
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
</script> </script>
<template> <template>

View File

@ -4,7 +4,9 @@
<slot /> <slot />
</div> </div>
<router-link to="/"> <router-link to="/">
<el-button type="primary">回到首页</el-button> <el-button type="primary">
回到首页
</el-button>
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFetchSelect } from "@/hooks/useFetchSelect"
import { getSelectDataApi } from "@/api/hook-demo/use-fetch-select" import { getSelectDataApi } from "@/api/hook-demo/use-fetch-select"
import { useFetchSelect } from "@/hooks/useFetchSelect"
const { loading, options, value } = useFetchSelect({ const { loading, options, value } = useFetchSelect({
api: getSelectDataApi api: getSelectDataApi
@ -11,10 +11,10 @@ const { loading, options, value } = useFetchSelect({
<div class="app-container"> <div class="app-container">
<h4>该示例是演示通过 hook 自动调用 api 后拿到 Select 组件需要的数据并传递给 Select 组件</h4> <h4>该示例是演示通过 hook 自动调用 api 后拿到 Select 组件需要的数据并传递给 Select 组件</h4>
<h5>Select 示例</h5> <h5>Select 示例</h5>
<el-select :loading="loading" v-model="value" filterable> <el-select v-model="value" :loading="loading" filterable>
<el-option v-for="(item, index) in options" v-bind="item" :key="index" placeholder="请选择" /> <el-option v-for="(item, index) in options" v-bind="item" :key="index" placeholder="请选择" />
</el-select> </el-select>
<h5>Select V2 示例如果数据量过多可以选择该组件</h5> <h5>Select V2 示例如果数据量过多可以选择该组件</h5>
<el-select-v2 :loading="loading" v-model="value" :options="options" filterable placeholder="请选择" /> <el-select-v2 v-model="value" :loading="loading" :options="options" filterable placeholder="请选择" />
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getErrorApi, getSuccessApi } from "@/api/hook-demo/use-fullscreen-loading"
import { useFullscreenLoading } from "@/hooks/useFullscreenLoading" import { useFullscreenLoading } from "@/hooks/useFullscreenLoading"
import { getSuccessApi, getErrorApi } from "@/api/hook-demo/use-fullscreen-loading"
import { ElMessage } from "element-plus" import { ElMessage } from "element-plus"
const svg = ` const svg = `
@ -21,7 +21,7 @@ const options = {
svgViewBox: "-10, -10, 50, 50" svgViewBox: "-10, -10, 50, 50"
} }
const querySuccess = async () => { async function querySuccess() {
// //
// 1. getSuccessApi // 1. getSuccessApi
// 2. getSuccessApi getSuccessApi // 2. getSuccessApi getSuccessApi
@ -29,10 +29,11 @@ const querySuccess = async () => {
ElMessage.success(`${res.message},传参为 ${res.data.list.toString()}`) ElMessage.success(`${res.message},传参为 ${res.data.list.toString()}`)
} }
const queryError = async () => { async function queryError() {
try { try {
await useFullscreenLoading(getErrorApi, options)() await useFullscreenLoading(getErrorApi, options)()
} catch (error) { }
catch (error) {
ElMessage.error((error as Error).message) ElMessage.error((error as Error).message)
} }
} }
@ -41,7 +42,11 @@ const queryError = async () => {
<template> <template>
<div class="app-container"> <div class="app-container">
<h4>该示例是演示通过将要执行的函数传递给 hook hook 自动开启全屏 loading函数执行结束后自动关闭 loading</h4> <h4>该示例是演示通过将要执行的函数传递给 hook hook 自动开启全屏 loading函数执行结束后自动关闭 loading</h4>
<el-button type="primary" @click="querySuccess">查询成功</el-button> <el-button type="primary" @click="querySuccess">
<el-button type="danger" @click="queryError">查询失败</el-button> 查询成功
</el-button>
<el-button type="danger" @click="queryError">
查询失败
</el-button>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"
import { useWatermark } from "@/hooks/useWatermark" import { useWatermark } from "@/hooks/useWatermark"
import { ref } from "vue"
const localRef = ref<HTMLElement | null>(null) const localRef = ref<HTMLElement | null>(null)
const { setWatermark, clearWatermark } = useWatermark(localRef) const { setWatermark, clearWatermark } = useWatermark(localRef)
@ -15,21 +15,29 @@ const { setWatermark: setGlobalWatermark, clearWatermark: clearGlobalWatermark }
</h4> </h4>
<div ref="localRef" class="local" /> <div ref="localRef" class="local" />
<el-button-group> <el-button-group>
<el-button type="primary" @click="setWatermark('局部水印', { color: '#409eff' })">创建局部水印</el-button> <el-button type="primary" @click="setWatermark('局部水印', { color: '#409eff' })">
创建局部水印
</el-button>
<el-button type="warning" @click="setWatermark('没有防御功能的局部水印', { color: '#e6a23c', defense: false })"> <el-button type="warning" @click="setWatermark('没有防御功能的局部水印', { color: '#e6a23c', defense: false })">
关闭防御功能 关闭防御功能
</el-button> </el-button>
<el-button type="danger" @click="clearWatermark">清除局部水印</el-button> <el-button type="danger" @click="clearWatermark">
清除局部水印
</el-button>
</el-button-group> </el-button-group>
<el-button-group> <el-button-group>
<el-button type="primary" @click="setGlobalWatermark('全局水印', { color: '#409eff' })">创建全局水印</el-button> <el-button type="primary" @click="setGlobalWatermark('全局水印', { color: '#409eff' })">
创建全局水印
</el-button>
<el-button <el-button
type="warning" type="warning"
@click="setGlobalWatermark('没有防御功能的全局水印', { color: '#e6a23c', defense: false })" @click="setGlobalWatermark('没有防御功能的全局水印', { color: '#e6a23c', defense: false })"
> >
关闭防御功能 关闭防御功能
</el-button> </el-button>
<el-button type="danger" @click="clearGlobalWatermark">清除全局水印</el-button> <el-button type="danger" @click="clearGlobalWatermark">
清除全局水印
</el-button>
</el-button-group> </el-button-group>
</div> </div>
</template> </template>

View File

@ -1,12 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginRequestData } from "@/api/login/types/login"
import type { FormInstance, FormRules } from "element-plus"
import { getLoginCodeApi } from "@/api/login"
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
import { useUserStore } from "@/store/modules/user"
import { Key, Loading, Lock, Picture, User } from "@element-plus/icons-vue"
import { reactive, ref } from "vue" import { reactive, ref } from "vue"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { useUserStore } from "@/store/modules/user"
import { type FormInstance, type FormRules } from "element-plus"
import { User, Lock, Key, Picture, Loading } from "@element-plus/icons-vue"
import { getLoginCodeApi } from "@/api/login"
import { type LoginRequestData } from "@/api/login/types/login"
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
import Owl from "./components/Owl.vue" import Owl from "./components/Owl.vue"
import { useFocus } from "./hooks/useFocus" import { useFocus } from "./hooks/useFocus"
@ -36,7 +36,7 @@ const loginFormRules: FormRules = {
code: [{ required: true, message: "请输入验证码", trigger: "blur" }] code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
} }
/** 登录逻辑 */ /** 登录逻辑 */
const handleLogin = () => { function handleLogin() {
loginFormRef.value?.validate((valid: boolean, fields) => { loginFormRef.value?.validate((valid: boolean, fields) => {
if (valid) { if (valid) {
loading.value = true loading.value = true
@ -58,7 +58,7 @@ const handleLogin = () => {
}) })
} }
/** 创建验证码 */ /** 创建验证码 */
const createCode = () => { function createCode() {
// //
loginFormData.code = "" loginFormData.code = ""
// //
@ -78,7 +78,7 @@ createCode()
<Owl :close-eyes="isFocus" /> <Owl :close-eyes="isFocus" />
<div class="login-card"> <div class="login-card">
<div class="title"> <div class="title">
<img src="@/assets/layouts/logo-text-2.png" /> <img src="@/assets/layouts/logo-text-2.png">
</div> </div>
<div class="content"> <div class="content">
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @keyup.enter="handleLogin"> <el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @keyup.enter="handleLogin">
@ -116,7 +116,7 @@ createCode()
size="large" size="large"
> >
<template #append> <template #append>
<el-image :src="codeUrl" @click="createCode" draggable="false"> <el-image :src="codeUrl" draggable="false" @click="createCode">
<template #placeholder> <template #placeholder>
<el-icon> <el-icon>
<Picture /> <Picture />
@ -131,7 +131,9 @@ createCode()
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-button :loading="loading" type="primary" size="large" @click.prevent="handleLogin"> </el-button> <el-button :loading="loading" type="primary" size="large" @click.prevent="handleLogin">
</el-button>
</el-form> </el-form>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import { ref } from "vue" import { ref } from "vue"
defineOptions({ defineOptions({
name: "Menu1-1" name: "Menu11"
}) })
const text = ref("") const text = ref("")

View File

@ -2,7 +2,7 @@
import { ref } from "vue" import { ref } from "vue"
defineOptions({ defineOptions({
name: "Menu1-2-1" name: "Menu121"
}) })
const text = ref("") const text = ref("")

View File

@ -2,7 +2,7 @@
import { ref } from "vue" import { ref } from "vue"
defineOptions({ defineOptions({
name: "Menu1-2-2" name: "Menu122"
}) })
const text = ref("") const text = ref("")

View File

@ -2,7 +2,7 @@
import { ref } from "vue" import { ref } from "vue"
defineOptions({ defineOptions({
name: "Menu1-3" name: "Menu13"
}) })
const text = ref("") const text = ref("")

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from "vue"
import { useUserStore } from "@/store/modules/user" import { useUserStore } from "@/store/modules/user"
import { ref, watch } from "vue"
const userStore = useUserStore() const userStore = useUserStore()
const switchRoles = ref(userStore.roles[0]) const switchRoles = ref(userStore.roles[0])

View File

@ -5,7 +5,9 @@ import SwitchRoles from "./components/SwitchRoles.vue"
<template> <template>
<div class="app-container"> <div class="app-container">
<SwitchRoles /> <SwitchRoles />
<el-tag type="warning" size="large">当前页面只有 admin 角色可见切换角色后将不能进入该页面</el-tag> <el-tag type="warning" size="large">
当前页面只有 admin 角色可见切换角色后将不能进入该页面
</el-tag>
</div> </div>
</template> </template>

View File

@ -4,7 +4,7 @@ import { useRoute, useRouter } from "vue-router"
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
router.replace({ path: "/" + route.params.path, query: route.query }) router.replace({ path: `/${route.params.path}`, query: route.query })
</script> </script>
<template> <template>

View File

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, watch } from "vue" import type { CreateOrUpdateTableRequestData, TableData } from "@/api/table/types/table"
import { createTableDataApi, deleteTableDataApi, updateTableDataApi, getTableDataApi } from "@/api/table" import { createTableDataApi, deleteTableDataApi, getTableDataApi, updateTableDataApi } from "@/api/table"
import { type CreateOrUpdateTableRequestData, type TableData } from "@/api/table/types/table"
import { type FormInstance, type FormRules, ElMessage, ElMessageBox } from "element-plus"
import { Search, Refresh, CirclePlus, Delete, Download, RefreshRight } from "@element-plus/icons-vue"
import { usePagination } from "@/hooks/usePagination" import { usePagination } from "@/hooks/usePagination"
import { CirclePlus, Delete, Download, Refresh, RefreshRight, Search } from "@element-plus/icons-vue"
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { reactive, ref, watch } from "vue"
defineOptions({ defineOptions({
// //
@ -15,7 +15,7 @@ defineOptions({
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination() const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
//#region // #region
const DEFAULT_FORM_DATA: CreateOrUpdateTableRequestData = { const DEFAULT_FORM_DATA: CreateOrUpdateTableRequestData = {
id: undefined, id: undefined,
username: "", username: "",
@ -28,7 +28,7 @@ const formRules: FormRules<CreateOrUpdateTableRequestData> = {
username: [{ required: true, trigger: "blur", message: "请输入用户名" }], username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }] password: [{ required: true, trigger: "blur", message: "请输入密码" }]
} }
const handleCreateOrUpdate = () => { function handleCreateOrUpdate() {
formRef.value?.validate((valid: boolean, fields) => { formRef.value?.validate((valid: boolean, fields) => {
if (!valid) return console.error("表单校验不通过", fields) if (!valid) return console.error("表单校验不通过", fields)
loading.value = true loading.value = true
@ -44,14 +44,14 @@ const handleCreateOrUpdate = () => {
}) })
}) })
} }
const resetForm = () => { function resetForm() {
formRef.value?.clearValidate() formRef.value?.clearValidate()
formData.value = cloneDeep(DEFAULT_FORM_DATA) formData.value = cloneDeep(DEFAULT_FORM_DATA)
} }
//#endregion // #endregion
//#region // #region
const handleDelete = (row: TableData) => { function handleDelete(row: TableData) {
ElMessageBox.confirm(`正在删除用户:${row.username},确认删除?`, "提示", { ElMessageBox.confirm(`正在删除用户:${row.username},确认删除?`, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
@ -63,23 +63,23 @@ const handleDelete = (row: TableData) => {
}) })
}) })
} }
//#endregion // #endregion
//#region // #region
const handleUpdate = (row: TableData) => { function handleUpdate(row: TableData) {
dialogVisible.value = true dialogVisible.value = true
formData.value = cloneDeep(row) formData.value = cloneDeep(row)
} }
//#endregion // #endregion
//#region // #region
const tableData = ref<TableData[]>([]) const tableData = ref<TableData[]>([])
const searchFormRef = ref<FormInstance | null>(null) const searchFormRef = ref<FormInstance | null>(null)
const searchData = reactive({ const searchData = reactive({
username: "", username: "",
phone: "" phone: ""
}) })
const getTableData = () => { function getTableData() {
loading.value = true loading.value = true
getTableDataApi({ getTableDataApi({
currentPage: paginationData.currentPage, currentPage: paginationData.currentPage,
@ -98,14 +98,14 @@ const getTableData = () => {
loading.value = false loading.value = false
}) })
} }
const handleSearch = () => { function handleSearch() {
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1) paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
} }
const resetSearch = () => { function resetSearch() {
searchFormRef.value?.resetFields() searchFormRef.value?.resetFields()
handleSearch() handleSearch()
} }
//#endregion // #endregion
/** 监听分页参数的变化 */ /** 监听分页参数的变化 */
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true }) watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
@ -122,16 +122,24 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
<el-input v-model="searchData.phone" placeholder="请输入" /> <el-input v-model="searchData.phone" placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button> <el-button type="primary" :icon="Search" @click="handleSearch">
<el-button :icon="Refresh" @click="resetSearch">重置</el-button> 查询
</el-button>
<el-button :icon="Refresh" @click="resetSearch">
重置
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<el-card v-loading="loading" shadow="never"> <el-card v-loading="loading" shadow="never">
<div class="toolbar-wrapper"> <div class="toolbar-wrapper">
<div> <div>
<el-button type="primary" :icon="CirclePlus" @click="dialogVisible = true">新增用户</el-button> <el-button type="primary" :icon="CirclePlus" @click="dialogVisible = true">
<el-button type="danger" :icon="Delete">批量删除</el-button> 新增用户
</el-button>
<el-button type="danger" :icon="Delete">
批量删除
</el-button>
</div> </div>
<div> <div>
<el-tooltip content="下载"> <el-tooltip content="下载">
@ -151,22 +159,32 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
<el-tag v-if="scope.row.roles === 'admin'" type="primary" effect="plain" disable-transitions> <el-tag v-if="scope.row.roles === 'admin'" type="primary" effect="plain" disable-transitions>
admin admin
</el-tag> </el-tag>
<el-tag v-else type="warning" effect="plain" disable-transitions>{{ scope.row.roles }}</el-tag> <el-tag v-else type="warning" effect="plain" disable-transitions>
{{ scope.row.roles }}
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="phone" label="手机号" align="center" /> <el-table-column prop="phone" label="手机号" align="center" />
<el-table-column prop="email" label="邮箱" align="center" /> <el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="status" label="状态" align="center"> <el-table-column prop="status" label="状态" align="center">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status" type="success" effect="plain" disable-transitions>启用</el-tag> <el-tag v-if="scope.row.status" type="success" effect="plain" disable-transitions>
<el-tag v-else type="danger" effect="plain" disable-transitions>禁用</el-tag> 启用
</el-tag>
<el-tag v-else type="danger" effect="plain" disable-transitions>
禁用
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" align="center" /> <el-table-column prop="createTime" label="创建时间" align="center" />
<el-table-column fixed="right" label="操作" width="150" align="center"> <el-table-column fixed="right" label="操作" width="150" align="center">
<template #default="scope"> <template #default="scope">
<el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">修改</el-button> <el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">删除</el-button> 修改
</el-button>
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">
删除
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -178,7 +196,7 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
:page-sizes="paginationData.pageSizes" :page-sizes="paginationData.pageSizes"
:total="paginationData.total" :total="paginationData.total"
:page-size="paginationData.pageSize" :page-size="paginationData.pageSize"
:currentPage="paginationData.currentPage" :current-page="paginationData.currentPage"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
/> />
@ -188,20 +206,24 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
<el-dialog <el-dialog
v-model="dialogVisible" v-model="dialogVisible"
:title="formData.id === undefined ? '新增用户' : '修改用户'" :title="formData.id === undefined ? '新增用户' : '修改用户'"
@closed="resetForm"
width="30%" width="30%"
@closed="resetForm"
> >
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" label-position="left"> <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" label-position="left">
<el-form-item prop="username" label="用户名"> <el-form-item prop="username" label="用户名">
<el-input v-model="formData.username" placeholder="请输入" /> <el-input v-model="formData.username" placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item prop="password" label="密码" v-if="formData.id === undefined"> <el-form-item v-if="formData.id === undefined" prop="password" label="密码">
<el-input v-model="formData.password" placeholder="请输入" /> <el-input v-model="formData.password" placeholder="请输入" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">
<el-button type="primary" @click="handleCreateOrUpdate" :loading="loading">确认</el-button> 取消
</el-button>
<el-button type="primary" :loading="loading" @click="handleCreateOrUpdate">
确认
</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>

View File

@ -1,25 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, reactive, ref } from "vue" import type { TableResponseData } from "@/api/table/types/table"
import { type ElMessageBoxOptions, ElMessageBox, ElMessage } from "element-plus" import type { ElMessageBoxOptions } from "element-plus"
import type { VxeFormInstance, VxeFormProps, VxeGridInstance, VxeGridProps, VxeModalInstance, VxeModalProps } from "vxe-table"
import { deleteTableDataApi, getTableDataApi } from "@/api/table" import { deleteTableDataApi, getTableDataApi } from "@/api/table"
import { type TableResponseData } from "@/api/table/types/table" import { ElMessage, ElMessageBox } from "element-plus"
import { nextTick, reactive, ref } from "vue"
import RoleColumnSolts from "./tsx/RoleColumnSolts" import RoleColumnSolts from "./tsx/RoleColumnSolts"
import StatusColumnSolts from "./tsx/StatusColumnSolts" import StatusColumnSolts from "./tsx/StatusColumnSolts"
import {
type VxeGridInstance,
type VxeGridProps,
type VxeModalInstance,
type VxeModalProps,
type VxeFormInstance,
type VxeFormProps
} from "vxe-table"
defineOptions({ defineOptions({
// //
name: "VxeTable" name: "VxeTable"
}) })
//#region vxe-grid // #region vxe-grid
interface RowMeta { interface RowMeta {
id: string id: string
username: string username: string
@ -168,9 +162,9 @@ const xGridOpt: VxeGridProps = reactive({
} }
} }
}) })
//#endregion // #endregion
//#region vxe-modal // #region vxe-modal
const xModalDom = ref<VxeModalInstance>() const xModalDom = ref<VxeModalInstance>()
const xModalOpt: VxeModalProps = reactive({ const xModalOpt: VxeModalProps = reactive({
title: "", title: "",
@ -182,9 +176,9 @@ const xModalOpt: VxeModalProps = reactive({
return Promise.resolve() return Promise.resolve()
} }
}) })
//#endregion // #endregion
//#region vxe-form // #region vxe-form
const xFormDom = ref<VxeFormInstance>() const xFormDom = ref<VxeFormInstance>()
const xFormOpt: VxeFormProps = reactive({ const xFormOpt: VxeFormProps = reactive({
span: 24, span: 24,
@ -253,9 +247,9 @@ const xFormOpt: VxeFormProps = reactive({
] ]
} }
}) })
//#endregion // #endregion
//#region // #region
const crudStore = reactive({ const crudStore = reactive({
/** 表单类型true 表示修改false 表示新增 */ /** 表单类型true 表示修改false 表示新增 */
isUpdate: true, isUpdate: true,
@ -346,7 +340,7 @@ const crudStore = reactive({
/** 更多自定义方法 */ /** 更多自定义方法 */
moreFn: () => {} moreFn: () => {}
}) })
//#endregion // #endregion
</script> </script>
<template> <template>
@ -355,13 +349,21 @@ const crudStore = reactive({
<vxe-grid ref="xGridDom" v-bind="xGridOpt"> <vxe-grid ref="xGridDom" v-bind="xGridOpt">
<!-- 左侧按钮列表 --> <!-- 左侧按钮列表 -->
<template #toolbar-btns> <template #toolbar-btns>
<vxe-button status="primary" icon="vxe-icon-add" @click="crudStore.onShowModal()">新增用户</vxe-button> <vxe-button status="primary" icon="vxe-icon-add" @click="crudStore.onShowModal()">
<vxe-button status="danger" icon="vxe-icon-delete">批量删除</vxe-button> 新增用户
</vxe-button>
<vxe-button status="danger" icon="vxe-icon-delete">
批量删除
</vxe-button>
</template> </template>
<!-- 操作 --> <!-- 操作 -->
<template #row-operate="{ row }"> <template #row-operate="{ row }">
<el-button link type="primary" @click="crudStore.onShowModal(row)">修改</el-button> <el-button link type="primary" @click="crudStore.onShowModal(row)">
<el-button link type="danger" @click="crudStore.onDelete(row)">删除</el-button> 修改
</el-button>
<el-button link type="danger" @click="crudStore.onDelete(row)">
删除
</el-button>
</template> </template>
</vxe-grid> </vxe-grid>
<!-- 弹窗 --> <!-- 弹窗 -->

View File

@ -1,4 +1,4 @@
import { type VxeColumnPropTypes } from "vxe-table/types/column" import type { VxeColumnPropTypes } from "vxe-table/types/column"
const solts: VxeColumnPropTypes.Slots = { const solts: VxeColumnPropTypes.Slots = {
default: ({ row, column }) => { default: ({ row, column }) => {

View File

@ -1,4 +1,4 @@
import { type VxeColumnPropTypes } from "vxe-table/types/column" import type { VxeColumnPropTypes } from "vxe-table/types/column"
const solts: VxeColumnPropTypes.Slots = { const solts: VxeColumnPropTypes.Slots = {
default: ({ row, column }) => { default: ({ row, column }) => {

View File

@ -2,10 +2,16 @@
<div h-full uno-padding-20> <div h-full uno-padding-20>
<div h-full text-center flex select-none all:transition-400> <div h-full text-center flex select-none all:transition-400>
<div ma> <div ma>
<div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-1s>UnoCSS</div> <div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-1s>
<div op30 dark:op60 text-lg fw300 m1>该页面是一个 UnoCSS 的使用案例其他页面依旧采用 Scss</div> UnoCSS
</div>
<div op30 dark:op60 text-lg fw300 m1>
该页面是一个 UnoCSS 的使用案例其他页面依旧采用 Scss
</div>
<div m2 flex justify-center text-lg op30 dark:op60 hover="op80" dark:hover="op80"> <div m2 flex justify-center text-lg op30 dark:op60 hover="op80" dark:hover="op80">
<a href="https://antfu.me/posts/reimagine-atomic-css-zh" target="_blank">推荐阅读重新构想原子化 CSS</a> <a href="https://antfu.me/posts/reimagine-atomic-css-zh" target="_blank">
推荐阅读重新构想原子化 CSS
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,17 @@
import { shallowMount } from "@vue/test-utils"
import { describe, expect, it } from "vitest"
import Notify from "@/components/Notify/index.vue" import Notify from "@/components/Notify/index.vue"
import NotifyList from "@/components/Notify/NotifyList.vue" import NotifyList from "@/components/Notify/NotifyList.vue"
import { shallowMount } from "@vue/test-utils"
import { describe, expect, it } from "vitest"
describe("Notify", () => { describe("notify", () => {
it("正常渲染", () => { it("正常渲染", () => {
const wrapper = shallowMount(Notify) const wrapper = shallowMount(Notify)
expect(wrapper.classes("notify")).toBe(true) expect(wrapper.classes("notify")).toBe(true)
}) })
}) })
describe("NotifyList", () => { describe("notifyList", () => {
it("List 长度为 0", () => { it("list 长度为 0", () => {
const wrapper = shallowMount(NotifyList, { const wrapper = shallowMount(NotifyList, {
props: { props: {
list: [] list: []
@ -19,7 +19,7 @@ describe("NotifyList", () => {
}) })
expect(wrapper.find("el-empty").exists()).toBe(true) expect(wrapper.find("el-empty").exists()).toBe(true)
}) })
it("List 长度不为 0", () => { it("list 长度不为 0", () => {
const wrapper = shallowMount(NotifyList, { const wrapper = shallowMount(NotifyList, {
props: { props: {
list: [ list: [

View File

@ -1,32 +1,32 @@
import { describe, expect, it } from "vitest"
import { isArray } from "@/utils/validate" import { isArray } from "@/utils/validate"
import { describe, expect, it } from "vitest"
describe("isArray", () => { describe("isArray", () => {
it("String", () => { it("string", () => {
expect(isArray("")).toBe(false) expect(isArray("")).toBe(false)
}) })
it("Number", () => { it("number", () => {
expect(isArray(1)).toBe(false) expect(isArray(1)).toBe(false)
}) })
it("Boolean", () => { it("boolean", () => {
expect(isArray(true)).toBe(false) expect(isArray(true)).toBe(false)
}) })
it("Null", () => { it("null", () => {
expect(isArray(null)).toBe(false) expect(isArray(null)).toBe(false)
}) })
it("Undefined", () => { it("undefined", () => {
expect(isArray(undefined)).toBe(false) expect(isArray(undefined)).toBe(false)
}) })
it("Symbol", () => { it("symbol", () => {
expect(isArray(Symbol())).toBe(false) expect(isArray(Symbol())).toBe(false)
}) })
it("BigInt", () => { it("bigInt", () => {
expect(isArray(BigInt(1))).toBe(false) expect(isArray(BigInt(1))).toBe(false)
}) })
it("Object", () => { it("object", () => {
expect(isArray({})).toBe(false) expect(isArray({})).toBe(false)
}) })
it("Array Object", () => { it("array object", () => {
expect(isArray([])).toBe(true) expect(isArray([])).toBe(true)
}) })
}) })

View File

@ -1,24 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "esnext",
/** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "bundler",
/** TS */
"strict": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "vue", "jsxImportSource": "vue",
"importHelpers": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"resolveJsonModule": true,
/** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"skipLibCheck": true, /** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
"useDefineForClassFields": true,
"experimentalDecorators": true,
/** baseUrl 使 */
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
/** baseUrl */
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": [ "types": [
"node", "node",
"vite/client", "vite/client",
@ -26,12 +23,15 @@
"element-plus/global", "element-plus/global",
"vitest" "vitest"
], ],
/** baseUrl 使 */ /** TS */
"baseUrl": ".", "strict": true,
/** baseUrl */ "importHelpers": true,
"paths": { "sourceMap": true,
"@/*": ["src/*"] "allowSyntheticDefaultImports": true,
} "esModuleInterop": true,
/** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
"isolatedModules": true,
"skipLibCheck": true
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",

View File

@ -1,4 +1,4 @@
import SvgIcon from "@/components/SvgIcon/index.vue" import type SvgIcon from "@/components/SvgIcon/index.vue"
/** 由 app.component 全局注册的组件需要在这里声明 TS 类型才能获得 Volar 插件提供的类型提示) */ /** 由 app.component 全局注册的组件需要在这里声明 TS 类型才能获得 Volar 插件提供的类型提示) */
declare module "vue" { declare module "vue" {

View File

@ -1,12 +1,13 @@
/// <reference types="vitest" /> /// <reference types="vitest" />
import { type ConfigEnv, type UserConfigExport, loadEnv } from "vite" import type { ConfigEnv, UserConfigExport } from "vite"
import path, { resolve } from "path" import path, { resolve } from "node:path"
import vue from "@vitejs/plugin-vue" import vue from "@vitejs/plugin-vue"
import vueJsx from "@vitejs/plugin-vue-jsx" import vueJsx from "@vitejs/plugin-vue-jsx"
import UnoCSS from "unocss/vite"
import { loadEnv } from "vite"
import { createSvgIconsPlugin } from "vite-plugin-svg-icons" import { createSvgIconsPlugin } from "vite-plugin-svg-icons"
import svgLoader from "vite-svg-loader" import svgLoader from "vite-svg-loader"
import UnoCSS from "unocss/vite"
/** 配置项文档https://cn.vitejs.dev/config */ /** 配置项文档https://cn.vitejs.dev/config */
export default ({ mode }: ConfigEnv): UserConfigExport => { export default ({ mode }: ConfigEnv): UserConfigExport => {