mirror of
https://github.com/un-pany/v3-admin-vite.git
synced 2025-04-20 10:59:21 +08:00
chore: pnpm eslint . --fix
This commit is contained in:
parent
c8571a5f55
commit
7f02e18d37
@ -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"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -67,6 +67,7 @@
|
||||
"@vitejs/plugin-vue-jsx": "4.1.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"eslint": "9.15.0",
|
||||
"eslint-plugin-format": "0.1.2",
|
||||
"husky": "9.1.6",
|
||||
"jsdom": "25.0.1",
|
||||
"lint-staged": "15.2.10",
|
||||
|
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@ -65,7 +65,7 @@ importers:
|
||||
devDependencies:
|
||||
'@antfu/eslint-config':
|
||||
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':
|
||||
specifier: 3.0.6
|
||||
version: 3.0.6
|
||||
@ -93,6 +93,9 @@ importers:
|
||||
eslint:
|
||||
specifier: 9.15.0
|
||||
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:
|
||||
specifier: 9.1.6
|
||||
version: 9.1.6
|
||||
@ -313,6 +316,15 @@ packages:
|
||||
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
|
||||
peerDependencies:
|
||||
@ -1808,6 +1820,11 @@ packages:
|
||||
eslint-flat-config-utils@0.4.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
||||
|
||||
@ -1827,6 +1844,9 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '*'
|
||||
|
||||
eslint-parser-plain@0.1.0:
|
||||
resolution: {integrity: sha512-oOeA6FWU0UJT/Rxc3XF5Cq0nbIZbylm7j8+plqq0CZoE6m4u32OXJrR+9iy4srGMmF6v6pmgvP1zPxSRIGh3sg==}
|
||||
|
||||
eslint-plugin-antfu@2.7.0:
|
||||
resolution: {integrity: sha512-gZM3jq3ouqaoHmUNszb1Zo2Ux7RckSvkGksjLWz9ipBYGSv1EwwBETN6AdiUXn+RpVHXTbEMPAPlXJazcA6+iA==}
|
||||
peerDependencies:
|
||||
@ -1843,6 +1863,11 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-mDRXPSLQ0UQZQw91QdG4/qZT6hgeW2MJTczAbgPseUZuPEtIjjdPOolXroRkulnOn3fzj6gNgvk+wchMJiHElg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -2025,6 +2050,9 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-diff@1.3.0:
|
||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@ -3150,6 +3178,10 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
|
||||
engines: {node: '>=14'}
|
||||
@ -3974,7 +4006,7 @@ snapshots:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@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:
|
||||
'@antfu/install-pkg': 0.4.1
|
||||
'@clack/prompts': 0.7.0
|
||||
@ -4012,6 +4044,8 @@ snapshots:
|
||||
vue-eslint-parser: 9.4.3(eslint@9.15.0(jiti@1.21.6))
|
||||
yaml-eslint-parser: 1.2.3
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
eslint-plugin-format: 0.1.2(eslint@9.15.0(jiti@1.21.6))
|
||||
transitivePeerDependencies:
|
||||
- '@eslint/json'
|
||||
- '@typescript-eslint/utils'
|
||||
@ -4206,6 +4240,12 @@ snapshots:
|
||||
|
||||
'@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))':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.6.3)
|
||||
@ -5784,6 +5824,11 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@ -5802,6 +5847,8 @@ snapshots:
|
||||
dependencies:
|
||||
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)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
@ -5819,6 +5866,17 @@ snapshots:
|
||||
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):
|
||||
dependencies:
|
||||
'@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-diff@1.3.0: {}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@ -7379,6 +7439,10 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.0:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.3.3: {}
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Tip: Simple judgments may not fully cover
|
||||
if (/MSIE\s|Trident\//.test(window.navigator.userAgent)) {
|
||||
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>"
|
||||
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>"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTheme } from "@/hooks/useTheme"
|
||||
import { useGreyAndColorWeakness } from "@/hooks/useGreyAndColorWeakness"
|
||||
import { useTheme } from "@/hooks/useTheme"
|
||||
import { ElNotification } from "element-plus"
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn" // Element Plus 中文包
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { request } from "@/utils/service"
|
||||
import type * as Login from "./types/login"
|
||||
import { request } from "@/utils/service"
|
||||
|
||||
/** 获取登录验证码 */
|
||||
export function getLoginCodeApi() {
|
||||
|
@ -11,4 +11,4 @@ export type LoginCodeResponseData = ApiResponseData<string>
|
||||
|
||||
export type LoginResponseData = ApiResponseData<{ token: string }>
|
||||
|
||||
export type UserInfoResponseData = ApiResponseData<{ username: string; roles: string[] }>
|
||||
export type UserInfoResponseData = ApiResponseData<{ username: string, roles: string[] }>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { request } from "@/utils/service"
|
||||
import type * as Table from "./types/table"
|
||||
import { request } from "@/utils/service"
|
||||
|
||||
/** 增 */
|
||||
export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { type ListItem } from "./data"
|
||||
import type { ListItem } from "./data"
|
||||
|
||||
interface Props {
|
||||
list: ListItem[]
|
||||
@ -10,7 +10,7 @@ const props = defineProps<Props>()
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
@ -18,10 +18,12 @@ const props = defineProps<Props>()
|
||||
<span class="card-title">{{ item.title }}</span>
|
||||
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
|
||||
</span>
|
||||
<div class="card-time">{{ item.datetime }}</div>
|
||||
<div class="card-time">
|
||||
{{ item.datetime }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.avatar" class="card-avatar">
|
||||
<img :src="item.avatar" width="34" />
|
||||
<img :src="item.avatar" width="34">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue"
|
||||
import { ElMessage } from "element-plus"
|
||||
import type { ListItem } from "./data"
|
||||
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 { type ListItem, notifyData, messageData, todoData } from "./data"
|
||||
|
||||
type TabName = "通知" | "消息" | "待办"
|
||||
|
||||
@ -45,7 +46,7 @@ const data = ref<DataItem[]>([
|
||||
}
|
||||
])
|
||||
|
||||
const handleHistory = () => {
|
||||
function handleHistory() {
|
||||
ElMessage.success(`跳转到${activeName.value}历史页面`)
|
||||
}
|
||||
</script>
|
||||
@ -64,7 +65,7 @@ const handleHistory = () => {
|
||||
</template>
|
||||
<template #default>
|
||||
<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>
|
||||
{{ item.name }}
|
||||
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
|
||||
@ -75,7 +76,9 @@ const handleHistory = () => {
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="notify-history">
|
||||
<el-button link @click="handleHistory">查看{{ activeName }}历史</el-button>
|
||||
<el-button link @click="handleHistory">
|
||||
查看{{ activeName }}历史
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watchEffect } from "vue"
|
||||
import { ElMessage } from "element-plus"
|
||||
import screenfull from "screenfull"
|
||||
import { computed, ref, watchEffect } from "vue"
|
||||
|
||||
interface Props {
|
||||
/** 全屏的元素,默认是 html */
|
||||
@ -25,17 +25,17 @@ const CONTENT_LARGE = "content-large"
|
||||
const CONTENT_FULL = "content-full"
|
||||
const classList = document.body.classList
|
||||
|
||||
//#region 全屏
|
||||
// #region 全屏
|
||||
const isEnabled = screenfull.isEnabled
|
||||
const isFullscreen = ref<boolean>(false)
|
||||
const fullscreenTips = computed(() => (isFullscreen.value ? props.exitTips : props.openTips))
|
||||
const fullscreenSvgName = computed(() => (isFullscreen.value ? "fullscreen-exit" : "fullscreen"))
|
||||
|
||||
const handleFullscreenClick = () => {
|
||||
function handleFullscreenClick() {
|
||||
const dom = document.querySelector(props.element) || undefined
|
||||
isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
|
||||
}
|
||||
const handleFullscreenChange = () => {
|
||||
function handleFullscreenChange() {
|
||||
isFullscreen.value = screenfull.isFullscreen
|
||||
// 退出全屏时清除相关的 class
|
||||
isFullscreen.value || classList.remove(CONTENT_LARGE, CONTENT_FULL)
|
||||
@ -48,18 +48,18 @@ watchEffect((onCleanup) => {
|
||||
onCleanup(() => screenfull.off("change", handleFullscreenChange))
|
||||
}
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 内容区
|
||||
// #region 内容区
|
||||
const isContentLarge = ref<boolean>(false)
|
||||
const contentLargeTips = computed(() => (isContentLarge.value ? "内容区复原" : "内容区放大"))
|
||||
const contentLargeSvgName = computed(() => (isContentLarge.value ? "fullscreen-exit" : "fullscreen"))
|
||||
const handleContentLargeClick = () => {
|
||||
function handleContentLargeClick() {
|
||||
isContentLarge.value = !isContentLarge.value
|
||||
// 内容区放大时,将不需要的组件隐藏
|
||||
classList.toggle(CONTENT_LARGE, isContentLarge.value)
|
||||
}
|
||||
const handleContentFullClick = () => {
|
||||
function handleContentFullClick() {
|
||||
// 取消内容区放大
|
||||
isContentLarge.value && handleContentLargeClick()
|
||||
// 内容区全屏时,将不需要的组件隐藏
|
||||
@ -67,7 +67,7 @@ const handleContentFullClick = () => {
|
||||
// 开启全屏
|
||||
handleFullscreenClick()
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -82,9 +82,13 @@ const handleContentFullClick = () => {
|
||||
<template #dropdown>
|
||||
<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>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
@ -1,13 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from "vue"
|
||||
import { type RouteRecordName, type RouteRecordRaw, useRouter } from "vue-router"
|
||||
import type { RouteRecordName, RouteRecordRaw } from "vue-router"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import SearchResult from "./SearchResult.vue"
|
||||
import SearchFooter from "./SearchFooter.vue"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import { ElMessage, ElScrollbar } from "element-plus"
|
||||
import { cloneDeep, debounce } from "lodash-es"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import { computed, ref, shallowRef } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import SearchFooter from "./SearchFooter.vue"
|
||||
import SearchResult from "./SearchResult.vue"
|
||||
|
||||
/** 控制 modal 显隐 */
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
@ -33,7 +34,7 @@ const menusData = computed(() => cloneDeep(usePermissionStore().routes))
|
||||
/** 搜索(防抖) */
|
||||
const handleSearch = debounce(() => {
|
||||
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
|
||||
)
|
||||
// 默认选中搜索结果的第一项
|
||||
@ -42,7 +43,7 @@ const handleSearch = debounce(() => {
|
||||
}, 500)
|
||||
|
||||
/** 将树形菜单扁平化为一维数组,用于菜单搜索 */
|
||||
const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
|
||||
function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
|
||||
arr.forEach((item) => {
|
||||
result.push(item)
|
||||
item.children && flatTree(item.children, result)
|
||||
@ -51,7 +52,7 @@ const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
|
||||
}
|
||||
|
||||
/** 关闭搜索对话框 */
|
||||
const handleClose = () => {
|
||||
function handleClose() {
|
||||
modelValue.value = false
|
||||
// 延时处理防止用户看到重置数据的操作
|
||||
setTimeout(() => {
|
||||
@ -61,7 +62,7 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
/** 根据下标位置进行滚动 */
|
||||
const scrollTo = (index: number) => {
|
||||
function scrollTo(index: number) {
|
||||
if (!searchResultRef.value) return
|
||||
const scrollTop = searchResultRef.value.getScrollTop(index)
|
||||
// 手动控制 el-scrollbar 滚动条滚动,设置滚动条到顶部的距离
|
||||
@ -69,12 +70,12 @@ const scrollTo = (index: number) => {
|
||||
}
|
||||
|
||||
/** 键盘上键 */
|
||||
const handleUp = () => {
|
||||
function handleUp() {
|
||||
isPressUpOrDown.value = true
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
// 获取该 name 在菜单中第一次出现的位置
|
||||
const index = resultList.value.findIndex((item) => item.name === activeRouteName.value)
|
||||
const index = resultList.value.findIndex(item => item.name === activeRouteName.value)
|
||||
// 如果已处在顶部
|
||||
if (index === 0) {
|
||||
const bottomName = resultList.value[length - 1].name
|
||||
@ -94,12 +95,12 @@ const handleUp = () => {
|
||||
}
|
||||
|
||||
/** 键盘下键 */
|
||||
const handleDown = () => {
|
||||
function handleDown() {
|
||||
isPressUpOrDown.value = true
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
// 获取该 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) {
|
||||
const topName = resultList.value[0].name
|
||||
@ -119,11 +120,11 @@ const handleDown = () => {
|
||||
}
|
||||
|
||||
/** 键盘回车键 */
|
||||
const handleEnter = () => {
|
||||
function handleEnter() {
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
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)) {
|
||||
window.open(path, "_blank", "noopener, noreferrer")
|
||||
return
|
||||
@ -134,7 +135,8 @@ const handleEnter = () => {
|
||||
}
|
||||
try {
|
||||
router.push({ name })
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入")
|
||||
return
|
||||
}
|
||||
@ -142,7 +144,7 @@ const handleEnter = () => {
|
||||
}
|
||||
|
||||
/** 释放上键或下键 */
|
||||
const handleReleaseUpOrDown = () => {
|
||||
function handleReleaseUpOrDown() {
|
||||
isPressUpOrDown.value = false
|
||||
}
|
||||
</script>
|
||||
@ -150,19 +152,19 @@ const handleReleaseUpOrDown = () => {
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="modelValue"
|
||||
:before-close="handleClose"
|
||||
:width="modalWidth"
|
||||
top="5vh"
|
||||
class="search-modal__private"
|
||||
append-to-body
|
||||
@opened="inputRef?.focus()"
|
||||
@closed="inputRef?.blur()"
|
||||
@keydown.up="handleUp"
|
||||
@keydown.down="handleDown"
|
||||
@keydown.enter="handleEnter"
|
||||
@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>
|
||||
<SvgIcon name="search" />
|
||||
</template>
|
||||
@ -170,15 +172,15 @@ const handleReleaseUpOrDown = () => {
|
||||
<el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
|
||||
<template v-else>
|
||||
<p>搜索结果</p>
|
||||
<el-scrollbar ref="scrollbarRef" max-height="40vh" always>
|
||||
<ElScrollbar ref="scrollbarRef" max-height="40vh" always>
|
||||
<SearchResult
|
||||
ref="searchResultRef"
|
||||
v-model="activeRouteName"
|
||||
:list="resultList"
|
||||
:isPressUpOrDown="isPressUpOrDown"
|
||||
:is-press-up-or-down="isPressUpOrDown"
|
||||
@click="handleEnter"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SearchFooter :total="resultList.length" />
|
||||
|
@ -1,21 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RouteRecordName, RouteRecordRaw } from "vue-router"
|
||||
import { getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue"
|
||||
import { type RouteRecordName, type RouteRecordRaw } from "vue-router"
|
||||
|
||||
interface Props {
|
||||
list: RouteRecordRaw[]
|
||||
isPressUpOrDown: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
/** 选中的菜单 */
|
||||
const modelValue = defineModel<RouteRecordName | undefined>({ required: true })
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const scrollbarHeight = ref<number>(0)
|
||||
|
||||
/** 菜单的样式 */
|
||||
const itemStyle = (item: RouteRecordRaw) => {
|
||||
function itemStyle(item: RouteRecordRaw) {
|
||||
const flag = item.name === modelValue.value
|
||||
return {
|
||||
background: flag ? "var(--el-color-primary)" : "",
|
||||
@ -24,20 +23,20 @@ const itemStyle = (item: RouteRecordRaw) => {
|
||||
}
|
||||
|
||||
/** 鼠标移入 */
|
||||
const handleMouseenter = (item: RouteRecordRaw) => {
|
||||
function handleMouseenter(item: RouteRecordRaw) {
|
||||
// 如果上键或下键与 mouseenter 事件同时生效,则以上下键为准,不执行该函数的赋值逻辑
|
||||
if (props.isPressUpOrDown) return
|
||||
modelValue.value = item.name
|
||||
}
|
||||
|
||||
/** 计算滚动可视区高度 */
|
||||
const getScrollbarHeight = () => {
|
||||
function getScrollbarHeight() {
|
||||
// el-scrollbar max-height="40vh"
|
||||
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[]
|
||||
if (!currentInstance) return 0
|
||||
const currentRef = currentInstance[0]
|
||||
@ -75,7 +74,7 @@ defineExpose({ getScrollTop })
|
||||
@mouseenter="handleMouseenter(item)"
|
||||
>
|
||||
<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">
|
||||
{{ item.meta?.title }}
|
||||
</span>
|
||||
|
@ -5,7 +5,7 @@ import SearchModal from "./SearchModal.vue"
|
||||
/** 控制 modal 显隐 */
|
||||
const modalVisible = ref<boolean>(false)
|
||||
/** 打开 modal */
|
||||
const handleOpen = () => {
|
||||
function handleOpen() {
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
@ -1,18 +1,19 @@
|
||||
<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"
|
||||
|
||||
const { themeList, activeThemeName, setTheme } = useTheme()
|
||||
|
||||
const handleChangeTheme = ({ clientX, clientY }: MouseEvent, themeName: ThemeName) => {
|
||||
function handleChangeTheme({ clientX, clientY }: MouseEvent, themeName: ThemeName) {
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(clientX, window.innerWidth - clientX),
|
||||
Math.max(clientY, window.innerHeight - clientY)
|
||||
)
|
||||
const style = document.documentElement.style
|
||||
style.setProperty("--v3-theme-x", clientX + "px")
|
||||
style.setProperty("--v3-theme-y", clientY + "px")
|
||||
style.setProperty("--v3-theme-r", maxRadius + "px")
|
||||
style.setProperty("--v3-theme-x", `${clientX}px`)
|
||||
style.setProperty("--v3-theme-y", `${clientY}px`)
|
||||
style.setProperty("--v3-theme-r", `${maxRadius}px`)
|
||||
const handler = () => {
|
||||
setTheme(themeName)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getConfigLayout } from "@/utils/cache/local-storage"
|
||||
import { LayoutModeEnum } from "@/constants/app-key"
|
||||
import { getConfigLayout } from "@/utils/cache/local-storage"
|
||||
|
||||
/** 项目配置类型 */
|
||||
export interface LayoutSettings {
|
||||
|
@ -6,7 +6,8 @@ interface RouteSettings {
|
||||
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false
|
||||
*/
|
||||
dynamic: boolean
|
||||
/** 当动态路由功能关闭时:
|
||||
/**
|
||||
* 当动态路由功能关闭时:
|
||||
* 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的)
|
||||
* 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
|
||||
*/
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type RouteLocationNormalized, type RouteRecordNameGeneric } from "vue-router"
|
||||
import type { RouteLocationNormalized, RouteRecordNameGeneric } from "vue-router"
|
||||
|
||||
/** 免登录白名单(匹配路由 path) */
|
||||
const whiteListByPath: string[] = ["/login"]
|
||||
@ -7,9 +7,9 @@ const whiteListByPath: string[] = ["/login"]
|
||||
const whiteListByName: RouteRecordNameGeneric[] = []
|
||||
|
||||
/** 判断是否在白名单 */
|
||||
const isWhiteList = (to: RouteLocationNormalized) => {
|
||||
function isWhiteList(to: RouteLocationNormalized) {
|
||||
// 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
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
import { permission } from "./permission"
|
||||
|
||||
/** 挂载自定义指令 */
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type Directive } from "vue"
|
||||
import type { Directive } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
|
||||
/** 权限指令,和权限判断函数 checkPermission 功能类似 */
|
||||
@ -7,7 +7,7 @@ export const permission: Directive = {
|
||||
const { value: permissionRoles } = binding
|
||||
const { roles } = useUserStore()
|
||||
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.parentNode?.removeChild(el) // 销毁
|
||||
} else {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { computed } from "vue"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { DeviceEnum } from "@/constants/app-key"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { computed } from "vue"
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ref, onMounted } from "vue"
|
||||
import { onMounted, ref } from "vue"
|
||||
|
||||
type OptionValue = string | number
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type LoadingOptions, ElLoading } from "element-plus"
|
||||
import { ElLoading, type LoadingOptions } from "element-plus"
|
||||
|
||||
const defaultOptions = {
|
||||
lock: true,
|
||||
@ -28,7 +28,8 @@ export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) =>
|
||||
try {
|
||||
loadingInstance = ElLoading.service({ ...defaultOptions, ...options })
|
||||
return await fn(...args)
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
loadingInstance?.close()
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { watchEffect } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { watchEffect } from "vue"
|
||||
|
||||
const GREY_MODE = "grey-mode"
|
||||
const COLOR_WEAKNESS = "color-weakness"
|
||||
const classList = document.documentElement.classList
|
||||
|
||||
/** 初始化 */
|
||||
const initGreyAndColorWeakness = () => {
|
||||
function initGreyAndColorWeakness() {
|
||||
const settingsStore = useSettingsStore()
|
||||
watchEffect(() => {
|
||||
classList.toggle(GREY_MODE, settingsStore.showGreyMode)
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { computed } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { LayoutModeEnum } from "@/constants/app-key"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { computed } from "vue"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left)
|
||||
const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top)
|
||||
const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop)
|
||||
|
||||
const setLayoutMode = (mode: LayoutModeEnum) => {
|
||||
function setLayoutMode(mode: LayoutModeEnum) {
|
||||
settingsStore.layoutMode = mode
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { onBeforeUnmount } from "vue"
|
||||
import type { RouteLocationNormalized } from "vue-router"
|
||||
import mitt, { type Handler } from "mitt"
|
||||
import { type RouteLocationNormalized } from "vue-router"
|
||||
import { onBeforeUnmount } from "vue"
|
||||
|
||||
/** 回调函数的类型 */
|
||||
type Callback = (route: RouteLocationNormalized) => void
|
||||
@ -10,7 +10,7 @@ const key = Symbol("ROUTE_CHANGE")
|
||||
let latestRoute: RouteLocationNormalized
|
||||
|
||||
/** 设置最新的路由信息,触发路由变化事件 */
|
||||
export const setRouteChange = (to: RouteLocationNormalized) => {
|
||||
export function setRouteChange(to: RouteLocationNormalized) {
|
||||
// 触发事件
|
||||
emitter.emit(key, to)
|
||||
// 缓存最新的路由信息
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ref, watchEffect } from "vue"
|
||||
import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage"
|
||||
import { ref, watchEffect } from "vue"
|
||||
|
||||
const DEFAULT_THEME_NAME = "normal"
|
||||
type DefaultThemeName = typeof DEFAULT_THEME_NAME
|
||||
@ -32,23 +32,23 @@ const themeList: ThemeList[] = [
|
||||
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
|
||||
|
||||
/** 设置主题 */
|
||||
const setTheme = (value: ThemeName) => {
|
||||
function setTheme(value: ThemeName) {
|
||||
activeThemeName.value = value
|
||||
}
|
||||
|
||||
/** 在 html 根元素上挂载 class */
|
||||
const addHtmlClass = (value: ThemeName) => {
|
||||
function addHtmlClass(value: ThemeName) {
|
||||
document.documentElement.classList.add(value)
|
||||
}
|
||||
|
||||
/** 在 html 根元素上移除其他主题 class */
|
||||
const removeHtmlClass = (value: ThemeName) => {
|
||||
const otherThemeNameList = themeList.map((item) => item.name).filter((name) => name !== value)
|
||||
function removeHtmlClass(value: ThemeName) {
|
||||
const otherThemeNameList = themeList.map(item => item.name).filter(name => name !== value)
|
||||
document.documentElement.classList.remove(...otherThemeNameList)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
const initTheme = () => {
|
||||
function initTheme() {
|
||||
// watchEffect 来收集副作用
|
||||
watchEffect(() => {
|
||||
const value = activeThemeName.value
|
||||
|
@ -7,7 +7,7 @@ const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
|
||||
const dynamicTitle = ref<string>("")
|
||||
|
||||
/** 设置标题 */
|
||||
const setTitle = (title?: string) => {
|
||||
function setTitle(title?: string) {
|
||||
dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { type Ref, onBeforeUnmount, ref } from "vue"
|
||||
import { debounce } from "lodash-es"
|
||||
import { onBeforeUnmount, type Ref, ref } from "vue"
|
||||
|
||||
type Observer = {
|
||||
interface Observer {
|
||||
watermarkElMutationObserver?: MutationObserver
|
||||
parentElMutationObserver?: MutationObserver
|
||||
parentElResizeObserver?: ResizeObserver
|
||||
@ -124,10 +124,12 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
// 移除水印元素
|
||||
try {
|
||||
parentEl.value.removeChild(watermarkEl)
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
// 比如在无防御情况下,用户打开控制台删除了这个元素
|
||||
console.warn("水印元素已不存在,请重新创建")
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
watermarkEl = null
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue" // Svg Component
|
||||
import "virtual:svg-icons-register"
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { computed } from "vue"
|
||||
import { AppMain, NavigationBar, Sidebar, TagsView } from "./components"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
|
||||
const { isMobile } = useDevice()
|
||||
const appStore = useAppStore()
|
||||
@ -22,7 +22,7 @@ const layoutClasses = computed(() => {
|
||||
})
|
||||
|
||||
/** 用于处理点击 mobile 端侧边栏遮罩层的事件 */
|
||||
const handleClickOutside = () => {
|
||||
function handleClickOutside() {
|
||||
appStore.closeSidebar(false)
|
||||
}
|
||||
</script>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
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 settingsStore = useSettingsStore()
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia"
|
||||
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 { showTagsView, showLogo } = storeToRefs(settingsStore)
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTagsViewStore } from "@/store/modules/tags-view"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useTagsViewStore } from "@/store/modules/tags-view"
|
||||
import Footer from "./Footer/index.vue"
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { type RouteLocationMatched, useRoute, useRouter } from "vue-router"
|
||||
import type { RouteLocationMatched } from "vue-router"
|
||||
import { useRouteListener } from "@/hooks/useRouteListener"
|
||||
import { compile } from "path-to-regexp"
|
||||
import { ref } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@ -12,18 +13,18 @@ const { listenerRouteChange } = useRouteListener()
|
||||
const breadcrumbs = ref<RouteLocationMatched[]>([])
|
||||
|
||||
/** 获取面包屑导航信息 */
|
||||
const getBreadcrumb = () => {
|
||||
breadcrumbs.value = route.matched.filter((item) => item.meta?.title && item.meta?.breadcrumb !== false)
|
||||
function getBreadcrumb() {
|
||||
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)
|
||||
return toPath(route.params)
|
||||
}
|
||||
|
||||
/** 处理面包屑导航点击事件 */
|
||||
const handleLink = (item: RouteLocationMatched) => {
|
||||
function handleLink(item: RouteLocationMatched) {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
router.push(redirect as string)
|
||||
|
@ -3,7 +3,9 @@ const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="layout-footer">MIT © 2021-PRESENT {{ VITE_APP_TITLE }}</footer>
|
||||
<footer class="layout-footer">
|
||||
MIT © 2021-PRESENT {{ VITE_APP_TITLE }}
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -14,7 +14,7 @@ const emit = defineEmits<{
|
||||
toggleClick: []
|
||||
}>()
|
||||
|
||||
const toggleClick = () => {
|
||||
function toggleClick() {
|
||||
emit("toggleClick")
|
||||
}
|
||||
</script>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
import logo from "@/assets/layouts/logo.png?url"
|
||||
import logoText1 from "@/assets/layouts/logo-text-1.png?url"
|
||||
import logoText2 from "@/assets/layouts/logo-text-2.png?url"
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
|
||||
interface Props {
|
||||
collapse?: boolean
|
||||
@ -16,13 +16,13 @@ const { isLeft, isTop } = useLayoutMode()
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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 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>
|
||||
</transition>
|
||||
</div>
|
||||
|
@ -1,19 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router"
|
||||
import { storeToRefs } from "pinia"
|
||||
import Notify from "@/components/Notify/index.vue"
|
||||
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 { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
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 Hamburger from "../Hamburger/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 { isTop } = useLayoutMode()
|
||||
@ -24,12 +24,12 @@ const settingsStore = useSettingsStore()
|
||||
const { showNotify, showThemeSwitch, showScreenfull, showSearchMenu } = storeToRefs(settingsStore)
|
||||
|
||||
/** 切换侧边栏 */
|
||||
const toggleSidebar = () => {
|
||||
function toggleSidebar() {
|
||||
appStore.toggleSidebar(false)
|
||||
}
|
||||
|
||||
/** 登出 */
|
||||
const logout = () => {
|
||||
function logout() {
|
||||
userStore.logout()
|
||||
router.push("/login")
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { Setting } from "@element-plus/icons-vue"
|
||||
import { ref } from "vue"
|
||||
|
||||
interface Props {
|
||||
buttonTop?: number
|
||||
@ -10,7 +10,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
buttonTop: 350
|
||||
})
|
||||
|
||||
const buttonTopCss = props.buttonTop + "px"
|
||||
const buttonTopCss = `${props.buttonTop}px`
|
||||
const show = ref(false)
|
||||
</script>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
import { LayoutModeEnum } from "@/constants/app-key"
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
|
||||
const { isLeft, isTop, isLeftTop, setLayoutMode } = useLayoutMode()
|
||||
</script>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { watchEffect } from "vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { removeConfigLayout } from "@/utils/cache/local-storage"
|
||||
import SelectLayoutMode from "./SelectLayoutMode.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 settingsStore = useSettingsStore()
|
||||
@ -28,18 +28,18 @@ const {
|
||||
|
||||
/** 定义 switch 设置项 */
|
||||
const switchSettings = {
|
||||
显示标签栏: showTagsView,
|
||||
"显示标签栏": showTagsView,
|
||||
"显示 Logo": showLogo,
|
||||
"固定 Header": fixedHeader,
|
||||
"显示页脚 Footer": showFooter,
|
||||
显示消息通知: showNotify,
|
||||
显示切换主题按钮: showThemeSwitch,
|
||||
显示全屏按钮: showScreenfull,
|
||||
显示搜索按钮: showSearchMenu,
|
||||
是否缓存标签栏: cacheTagsView,
|
||||
开启系统水印: showWatermark,
|
||||
显示灰色模式: showGreyMode,
|
||||
显示色弱模式: showColorWeakness
|
||||
"显示消息通知": showNotify,
|
||||
"显示切换主题按钮": showThemeSwitch,
|
||||
"显示全屏按钮": showScreenfull,
|
||||
"显示搜索按钮": showSearchMenu,
|
||||
"是否缓存标签栏": cacheTagsView,
|
||||
"开启系统水印": showWatermark,
|
||||
"显示灰色模式": showGreyMode,
|
||||
"显示色弱模式": showColorWeakness
|
||||
}
|
||||
|
||||
/** 非左侧模式时,Header 都是 fixed 布局 */
|
||||
@ -48,7 +48,7 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
/** 重置项目配置 */
|
||||
const resetConfigLayout = () => {
|
||||
function resetConfigLayout() {
|
||||
removeConfigLayout()
|
||||
location.reload()
|
||||
}
|
||||
@ -60,11 +60,13 @@ const resetConfigLayout = () => {
|
||||
<SelectLayoutMode />
|
||||
<el-divider />
|
||||
<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>
|
||||
<el-switch v-model="settingValue.value" :disabled="!isLeft && settingName === '固定 Header'" />
|
||||
</div>
|
||||
<el-button type="danger" :icon="Refresh" @click="resetConfigLayout">重 置</el-button>
|
||||
<el-button type="danger" :icon="Refresh" @click="resetConfigLayout">
|
||||
重 置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { type RouteRecordRaw } from "vue-router"
|
||||
import SidebarItemLink from "./SidebarItemLink.vue"
|
||||
import type { RouteRecordRaw } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import path from "path-browserify"
|
||||
import { computed } from "vue"
|
||||
import SidebarItemLink from "./SidebarItemLink.vue"
|
||||
|
||||
interface Props {
|
||||
item: RouteRecordRaw
|
||||
@ -19,7 +19,7 @@ const alwaysShowRootMenu = computed(() => props.item.meta?.alwaysShow)
|
||||
|
||||
/** 显示的子菜单 */
|
||||
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) {
|
||||
case isExternal(routePath):
|
||||
return routePath
|
||||
@ -58,7 +58,7 @@ const resolvePath = (routePath: string) => {
|
||||
<SidebarItemLink v-if="theOnlyOneChild.meta" :to="resolvePath(theOnlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(theOnlyOneChild.path)">
|
||||
<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>
|
||||
{{ theOnlyOneChild.meta.title }}
|
||||
</template>
|
||||
@ -68,7 +68,7 @@ const resolvePath = (routePath: string) => {
|
||||
<el-sub-menu v-else :index="resolvePath(props.item.path)" teleported>
|
||||
<template #title>
|
||||
<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>
|
||||
</template>
|
||||
<template v-if="props.item.children">
|
||||
|
@ -1,14 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
import { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
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 { 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 v3SidebarMenuTextColor = getCssVar("--v3-sidebar-menu-text-color")
|
||||
@ -26,9 +26,9 @@ const activeMenu = computed(() => {
|
||||
meta: { activeMenu },
|
||||
path
|
||||
} = 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 isLogo = computed(() => isLeft.value && settingsStore.showLogo)
|
||||
const backgroundColor = computed(() => (isLeft.value ? v3SidebarMenuBgColor : undefined))
|
||||
@ -63,7 +63,12 @@ const hiddenScrollbarVerticalBar = computed(() => {
|
||||
:collapse-transition="false"
|
||||
: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-scrollbar>
|
||||
</div>
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from "vue"
|
||||
import { RouterLink, useRoute } from "vue-router"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useRouteListener } from "@/hooks/useRouteListener"
|
||||
import type { RouterLink } from "vue-router"
|
||||
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 { ElScrollbar } from "element-plus"
|
||||
import { nextTick, ref } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
interface Props {
|
||||
tagRefs: InstanceType<typeof RouterLink>[]
|
||||
@ -28,13 +29,13 @@ let currentScrollLeft = 0
|
||||
const translateDistance = 200
|
||||
|
||||
/** 滚动时触发 */
|
||||
const scroll = ({ scrollLeft }: { scrollLeft: number }) => {
|
||||
function scroll({ scrollLeft }: { scrollLeft: number }) {
|
||||
currentScrollLeft = scrollLeft
|
||||
}
|
||||
|
||||
/** 鼠标滚轮滚动时触发 */
|
||||
const wheelScroll = ({ deltaY }: WheelEvent) => {
|
||||
if (/^-/.test(deltaY.toString())) {
|
||||
function wheelScroll({ deltaY }: WheelEvent) {
|
||||
if (deltaY.toString().startsWith("-")) {
|
||||
scrollTo("left")
|
||||
} else {
|
||||
scrollTo("right")
|
||||
@ -42,7 +43,7 @@ const wheelScroll = ({ deltaY }: WheelEvent) => {
|
||||
}
|
||||
|
||||
/** 获取可能需要的宽度 */
|
||||
const getWidth = () => {
|
||||
function getWidth() {
|
||||
/** 可滚动内容的长度 */
|
||||
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
|
||||
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
|
||||
for (let i = 0; i < tagRefs.length; i++) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error ignore
|
||||
if (route.path === tagRefs[i].$props.to.path) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error ignore
|
||||
const el: HTMLElement = tagRefs[i].$el
|
||||
const offsetWidth = el.offsetWidth
|
||||
const offsetLeft = el.offsetLeft
|
||||
@ -106,11 +107,11 @@ listenerRouteChange(() => {
|
||||
<el-icon class="arrow left" @click="scrollTo('left')">
|
||||
<ArrowLeft />
|
||||
</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">
|
||||
<slot />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</ElScrollbar>
|
||||
<el-icon class="arrow right" @click="scrollTo('right')">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
|
@ -1,12 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue"
|
||||
import { type RouteLocationNormalizedLoaded, type RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router"
|
||||
import { type TagView, useTagsViewStore } from "@/store/modules/tags-view"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import type { TagView } from "@/store/modules/tags-view"
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from "vue-router"
|
||||
import { useRouteListener } from "@/hooks/useRouteListener"
|
||||
import path from "path-browserify"
|
||||
import ScrollPane from "./ScrollPane.vue"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { useTagsViewStore } from "@/store/modules/tags-view"
|
||||
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 route = useRoute()
|
||||
@ -29,17 +31,17 @@ const selectedTag = ref<TagView>({})
|
||||
let affixTags: TagView[] = []
|
||||
|
||||
/** 判断标签页是否激活 */
|
||||
const isActive = (tag: TagView) => {
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path
|
||||
}
|
||||
|
||||
/** 判断标签页是否固定 */
|
||||
const isAffix = (tag: TagView) => {
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta?.affix
|
||||
}
|
||||
|
||||
/** 筛选出固定标签页 */
|
||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||
function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") {
|
||||
const tags: TagView[] = []
|
||||
routes.forEach((route) => {
|
||||
if (isAffix(route)) {
|
||||
@ -60,7 +62,7 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||
}
|
||||
|
||||
/** 初始化标签页 */
|
||||
const initTags = () => {
|
||||
function initTags() {
|
||||
affixTags = filterAffixTags(permissionStore.routes)
|
||||
for (const tag of affixTags) {
|
||||
// 必须含有 name 属性
|
||||
@ -69,7 +71,7 @@ const initTags = () => {
|
||||
}
|
||||
|
||||
/** 添加标签页 */
|
||||
const addTags = (route: RouteLocationNormalizedLoaded) => {
|
||||
function addTags(route: RouteLocationNormalizedLoaded) {
|
||||
if (route.name) {
|
||||
tagsViewStore.addVisitedView(route)
|
||||
tagsViewStore.addCachedView(route)
|
||||
@ -77,20 +79,20 @@ const addTags = (route: RouteLocationNormalizedLoaded) => {
|
||||
}
|
||||
|
||||
/** 刷新当前正在右键操作的标签页 */
|
||||
const refreshSelectedTag = (view: TagView) => {
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
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.delCachedView(view)
|
||||
isActive(view) && toLastView(tagsViewStore.visitedViews, view)
|
||||
}
|
||||
|
||||
/** 关闭其他标签页 */
|
||||
const closeOthersTags = () => {
|
||||
function closeOthersTags() {
|
||||
const fullPath = selectedTag.value.fullPath
|
||||
if (fullPath !== route.path && fullPath !== undefined) {
|
||||
router.push(fullPath)
|
||||
@ -100,15 +102,15 @@ const closeOthersTags = () => {
|
||||
}
|
||||
|
||||
/** 关闭所有标签页 */
|
||||
const closeAllTags = (view: TagView) => {
|
||||
function closeAllTags(view: TagView) {
|
||||
tagsViewStore.delAllVisitedViews()
|
||||
tagsViewStore.delAllCachedViews()
|
||||
if (affixTags.some((tag) => tag.path === route.path)) return
|
||||
if (affixTags.some(tag => tag.path === route.path)) return
|
||||
toLastView(tagsViewStore.visitedViews, view)
|
||||
}
|
||||
|
||||
/** 跳转到最后一个标签页 */
|
||||
const toLastView = (visitedViews: TagView[], view: TagView) => {
|
||||
function toLastView(visitedViews: TagView[], view: TagView) {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
const fullPath = latestView?.fullPath
|
||||
if (fullPath !== undefined) {
|
||||
@ -117,7 +119,7 @@ const toLastView = (visitedViews: TagView[], view: TagView) => {
|
||||
// 如果 TagsView 全部被关闭了,则默认重定向到主页
|
||||
if (view.name === "Dashboard") {
|
||||
// 重新加载主页
|
||||
router.push({ path: "/redirect" + view.path, query: view.query })
|
||||
router.push({ path: `/redirect${view.path}`, query: view.query })
|
||||
} else {
|
||||
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 offsetWidth = document.body.offsetWidth
|
||||
@ -142,7 +144,7 @@ const openMenu = (tag: TagView, e: MouseEvent) => {
|
||||
}
|
||||
|
||||
/** 关闭右键菜单面板 */
|
||||
const closeMenu = () => {
|
||||
function closeMenu() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
@ -161,9 +163,9 @@ listenerRouteChange((route) => {
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<ScrollPane class="tags-view-wrapper" :tag-refs="tagRefs">
|
||||
<router-link
|
||||
ref="tagRefs"
|
||||
<RouterLink
|
||||
v-for="tag in tagsViewStore.visitedViews"
|
||||
ref="tagRefs"
|
||||
:key="tag.path"
|
||||
:class="{ active: isActive(tag) }"
|
||||
class="tags-view-item"
|
||||
@ -175,13 +177,21 @@ listenerRouteChange((route) => {
|
||||
<el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
</ScrollPane>
|
||||
<ul v-show="visible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }">
|
||||
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
||||
<li @click="closeOthersTags">关闭其它</li>
|
||||
<li @click="closeAllTags(selectedTag)">关闭所有</li>
|
||||
<ul v-show="visible" class="contextmenu" :style="{ left: `${left}px`, top: `${top}px` }">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
刷新
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
关闭
|
||||
</li>
|
||||
<li @click="closeOthersTags">
|
||||
关闭其它
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
关闭所有
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 RightPanel } from "./RightPanel/index.vue"
|
||||
export { default as Settings } from "./Settings/index.vue"
|
||||
export { default as Sidebar } from "./Sidebar/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"
|
||||
|
@ -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 { useRouteListener } from "@/hooks/useRouteListener"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { onBeforeMount, onBeforeUnmount, onMounted } from "vue"
|
||||
|
||||
/** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */
|
||||
const MAX_MOBILE_WIDTH = 992
|
||||
|
@ -1,16 +1,16 @@
|
||||
<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 { useLayoutMode } from "@/hooks/useLayoutMode"
|
||||
import LeftMode from "./LeftMode.vue"
|
||||
import TopMode from "./TopMode.vue"
|
||||
import LeftTopMode from "./LeftTopMode.vue"
|
||||
import { Settings, RightPanel } from "./components"
|
||||
import { useWatermark } from "@/hooks/useWatermark"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
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 布局响应式 */
|
||||
useResize()
|
||||
@ -21,13 +21,13 @@ const { isLeft, isTop, isLeftTop } = useLayoutMode()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { showSettings, showTagsView, showWatermark } = storeToRefs(settingsStore)
|
||||
|
||||
//#region 隐藏标签栏时删除其高度,是为了让 Logo 组件高度和 Header 区域高度始终一致
|
||||
// #region 隐藏标签栏时删除其高度,是为了让 Logo 组件高度和 Header 区域高度始终一致
|
||||
const cssVarName = "--v3-tagsview-height"
|
||||
const v3TagsviewHeight = getCssVar(cssVarName)
|
||||
watchEffect(() => {
|
||||
showTagsView.value ? setCssVar(cssVarName, v3TagsviewHeight) : setCssVar(cssVarName, "0px")
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
/** 开启或关闭系统水印 */
|
||||
watchEffect(() => {
|
||||
|
@ -1,13 +1,13 @@
|
||||
// core
|
||||
import App from "@/App.vue"
|
||||
import { createApp } from "vue"
|
||||
import { pinia } from "@/store"
|
||||
import { loadDirectives } from "@/directives"
|
||||
import { router } from "@/router"
|
||||
import { pinia } from "@/store"
|
||||
import { createApp } from "vue"
|
||||
import "@/router/permission"
|
||||
// load
|
||||
import { loadSvg } from "@/icons"
|
||||
import { loadPlugins } from "@/plugins"
|
||||
import { loadDirectives } from "@/directives"
|
||||
// css
|
||||
import "uno.css"
|
||||
import "normalize.css"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
import * as ElementPlusIconsVue from "@element-plus/icons-vue"
|
||||
|
||||
export function loadElementPlusIcon(app: App) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
import ElementPlus from "element-plus"
|
||||
|
||||
export function loadElementPlus(app: App) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
import { loadElementPlus } from "./element-plus"
|
||||
import { loadElementPlusIcon } from "./element-plus-icon"
|
||||
import { loadVxeTable } from "./vxe-table"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type App } from "vue"
|
||||
import type { App } from "vue"
|
||||
// https://vxetable.cn/#/table/start/install
|
||||
import VXETable from "vxe-table"
|
||||
// https://github.com/x-extends/vxe-table-plugin-element
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { cloneDeep, omit } from "lodash-es"
|
||||
import {
|
||||
type Router,
|
||||
type RouteRecordNormalized,
|
||||
type RouteRecordRaw,
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory
|
||||
createWebHistory,
|
||||
type Router,
|
||||
type RouteRecordNormalized,
|
||||
type RouteRecordRaw
|
||||
} from "vue-router"
|
||||
import { cloneDeep, omit } from "lodash-es"
|
||||
|
||||
/** 路由模式 */
|
||||
export const history =
|
||||
import.meta.env.VITE_ROUTER_HISTORY === "hash"
|
||||
export const history
|
||||
= import.meta.env.VITE_ROUTER_HISTORY === "hash"
|
||||
? createWebHashHistory(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)
|
||||
routesMirror.forEach((route) => {
|
||||
// 如果路由是三级及其以上路由,对其进行降级处理
|
||||
@ -25,17 +25,17 @@ export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
|
||||
}
|
||||
|
||||
/** 判断路由层级是否大于 2 */
|
||||
const isMultipleRoute = (route: RouteRecordRaw) => {
|
||||
function isMultipleRoute(route: RouteRecordRaw) {
|
||||
const children = route.children
|
||||
if (children?.length) {
|
||||
// 只要有一个子路由的 children 长度大于 0,就说明是三级及其以上路由
|
||||
return children.some((child) => child.children?.length)
|
||||
return children.some(child => child.children?.length)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 生成二级路由 */
|
||||
const promoteRouteLevel = (route: RouteRecordRaw) => {
|
||||
function promoteRouteLevel(route: RouteRecordRaw) {
|
||||
// 创建 router 实例是为了获取到当前传入的 route 的所有路由信息
|
||||
let router: Router | null = createRouter({
|
||||
history,
|
||||
@ -46,13 +46,13 @@ const promoteRouteLevel = (route: RouteRecordRaw) => {
|
||||
addToChildren(routes, route.children || [], route)
|
||||
router = null
|
||||
// 转为二级路由后,去除所有子路由中的 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) => {
|
||||
const route = routes.find((item) => item.name === child.name)
|
||||
const route = routes.find(item => item.name === child.name)
|
||||
if (route) {
|
||||
// 初始化 routeModule 的 children
|
||||
routeModule.children = routeModule.children || []
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { type RouteRecordRaw, createRouter } from "vue-router"
|
||||
import { history, flatMultiLevelRoutes } from "./helper"
|
||||
import routeSettings from "@/config/route"
|
||||
import { createRouter, type RouteRecordRaw } from "vue-router"
|
||||
import { flatMultiLevelRoutes, history } from "./helper"
|
||||
|
||||
const Layouts = () => import("@/layouts/index.vue")
|
||||
|
||||
@ -304,7 +304,8 @@ export function resetRouter() {
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
// 强制刷新浏览器也行,只是交互体验不是很好
|
||||
window.location.reload()
|
||||
}
|
||||
|
@ -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 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/nprogress.css"
|
||||
|
||||
@ -41,10 +41,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||
// 生成可访问的 Routes
|
||||
routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
|
||||
// 将 "有访问权限的动态路由" 添加到 Router 中
|
||||
permissionStore.addRoutes.forEach((route) => router.addRoute(route))
|
||||
permissionStore.addRoutes.forEach(route => router.addRoute(route))
|
||||
// 设置 replace: true, 因此导航将不会留下历史记录
|
||||
next({ ...to, replace: true })
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
// 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
|
||||
userStore.resetToken()
|
||||
ElMessage.error((error as Error).message || "路由守卫过程发生错误")
|
||||
|
@ -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 { defineStore } from "pinia"
|
||||
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 {
|
||||
opened: boolean
|
||||
@ -26,7 +26,7 @@ export const useAppStore = defineStore("app", () => {
|
||||
/** 监听侧边栏 opened 状态 */
|
||||
watch(
|
||||
() => sidebar.opened,
|
||||
(opened) => handleSidebarStatus(opened)
|
||||
opened => handleSidebarStatus(opened)
|
||||
)
|
||||
|
||||
/** 切换侧边栏 */
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { ref } from "vue"
|
||||
import { pinia } from "@/store"
|
||||
import { defineStore } from "pinia"
|
||||
import { type RouteRecordRaw } from "vue-router"
|
||||
import type { RouteRecordRaw } from "vue-router"
|
||||
import routeSettings from "@/config/route"
|
||||
import { constantRoutes, dynamicRoutes } from "@/router"
|
||||
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
|
||||
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[] = []
|
||||
routes.forEach((route) => {
|
||||
const tempRoute = { ...route }
|
||||
|
@ -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 { defineStore } from "pinia"
|
||||
import { type LayoutSettings, layoutSettings } from "@/config/layouts"
|
||||
import { setConfigLayout } from "@/utils/cache/local-storage"
|
||||
import { defineStore } from "pinia"
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
type SettingsStore = {
|
||||
// 使用映射类型来遍历 layoutSettings 对象的键
|
||||
@ -18,7 +20,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
for (const [key, value] of Object.entries(layoutSettings)) {
|
||||
// 使用类型断言来指定 key 的类型,将 value 包装在 ref 函数中,创建一个响应式变量
|
||||
const refValue = ref(value)
|
||||
// @ts-ignore
|
||||
// @ts-expect-error ignore
|
||||
state[key as SettingsStoreKey] = refValue
|
||||
// 监听每个响应式变量
|
||||
watch(refValue, () => {
|
||||
@ -31,7 +33,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
const _getCacheData = () => {
|
||||
const settings = {} as LayoutSettings
|
||||
for (const [key, value] of Object.entries(state)) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error ignore
|
||||
settings[key as SettingsStoreKey] = value.value
|
||||
}
|
||||
return settings
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ref, watchEffect } from "vue"
|
||||
import type { RouteLocationNormalized } from "vue-router"
|
||||
import { pinia } from "@/store"
|
||||
import { getCachedViews, getVisitedViews, setCachedViews, setVisitedViews } from "@/utils/cache/local-storage"
|
||||
import { defineStore } from "pinia"
|
||||
import { ref, watchEffect } from "vue"
|
||||
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>
|
||||
|
||||
@ -18,10 +18,10 @@ export const useTagsViewStore = defineStore("tags-view", () => {
|
||||
setCachedViews(cachedViews.value)
|
||||
})
|
||||
|
||||
//#region add
|
||||
// #region add
|
||||
const addVisitedView = (view: TagView) => {
|
||||
// 检查是否已经存在相同的 visitedView
|
||||
const index = visitedViews.value.findIndex((v) => v.path === view.path)
|
||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||
if (index !== -1) {
|
||||
// 防止 query 参数丢失
|
||||
visitedViews.value[index].fullPath !== view.fullPath && (visitedViews.value[index] = { ...view })
|
||||
@ -34,24 +34,27 @@ export const useTagsViewStore = defineStore("tags-view", () => {
|
||||
const addCachedView = (view: TagView) => {
|
||||
if (typeof view.name !== "string") 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 index = visitedViews.value.findIndex((v) => v.path === view.path)
|
||||
if (index !== -1) visitedViews.value.splice(index, 1)
|
||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||
if (index !== -1)
|
||||
visitedViews.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const delCachedView = (view: TagView) => {
|
||||
if (typeof view.name !== "string") return
|
||||
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) => {
|
||||
visitedViews.value = visitedViews.value.filter((v) => {
|
||||
return v.meta?.affix || v.path === view.path
|
||||
@ -68,18 +71,18 @@ export const useTagsViewStore = defineStore("tags-view", () => {
|
||||
cachedViews.value = []
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region delAll
|
||||
// #region delAll
|
||||
const delAllVisitedViews = () => {
|
||||
// 保留固定的 tags
|
||||
visitedViews.value = visitedViews.value.filter((tag) => tag.meta?.affix)
|
||||
visitedViews.value = visitedViews.value.filter(tag => tag.meta?.affix)
|
||||
}
|
||||
|
||||
const delAllCachedViews = () => {
|
||||
cachedViews.value = []
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { ref } from "vue"
|
||||
import { pinia } from "@/store"
|
||||
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 type { LoginRequestData } from "@/api/login/types/login"
|
||||
import { getUserInfoApi, loginApi } from "@/api/login"
|
||||
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", () => {
|
||||
const token = ref<string>(getToken() || "")
|
||||
@ -32,7 +32,7 @@ export const useUserStore = defineStore("user", () => {
|
||||
}
|
||||
/** 模拟角色变化 */
|
||||
const changeRoles = async (role: string) => {
|
||||
const newToken = "token-" + role
|
||||
const newToken = `token-${role}`
|
||||
token.value = newToken
|
||||
setToken(newToken)
|
||||
// 用刷新页面代替重新登录
|
||||
|
6
src/utils/cache/cookies.ts
vendored
6
src/utils/cache/cookies.ts
vendored
@ -3,12 +3,12 @@
|
||||
import CacheKey from "@/constants/cache-key"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
export const getToken = () => {
|
||||
export function getToken() {
|
||||
return Cookies.get(CacheKey.TOKEN)
|
||||
}
|
||||
export const setToken = (token: string) => {
|
||||
export function setToken(token: string) {
|
||||
Cookies.set(CacheKey.TOKEN, token)
|
||||
}
|
||||
export const removeToken = () => {
|
||||
export function removeToken() {
|
||||
Cookies.remove(CacheKey.TOKEN)
|
||||
}
|
||||
|
46
src/utils/cache/local-storage.ts
vendored
46
src/utils/cache/local-storage.ts
vendored
@ -1,48 +1,48 @@
|
||||
/** 统一处理 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 { 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 系统布局配置
|
||||
export const getConfigLayout = () => {
|
||||
// #region 系统布局配置
|
||||
export function getConfigLayout() {
|
||||
const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT)
|
||||
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))
|
||||
}
|
||||
export const removeConfigLayout = () => {
|
||||
export function removeConfigLayout() {
|
||||
localStorage.removeItem(CacheKey.CONFIG_LAYOUT)
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 侧边栏状态
|
||||
export const getSidebarStatus = () => {
|
||||
// #region 侧边栏状态
|
||||
export function getSidebarStatus() {
|
||||
return localStorage.getItem(CacheKey.SIDEBAR_STATUS)
|
||||
}
|
||||
export const setSidebarStatus = (sidebarStatus: SidebarOpened | SidebarClosed) => {
|
||||
export function setSidebarStatus(sidebarStatus: SidebarOpened | SidebarClosed) {
|
||||
localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus)
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 正在应用的主题名称
|
||||
export const getActiveThemeName = () => {
|
||||
// #region 正在应用的主题名称
|
||||
export function getActiveThemeName() {
|
||||
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)
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 标签栏
|
||||
export const getVisitedViews = () => {
|
||||
// #region 标签栏
|
||||
export function getVisitedViews() {
|
||||
const json = localStorage.getItem(CacheKey.VISITED_VIEWS)
|
||||
return JSON.parse(json ?? "[]") as TagView[]
|
||||
}
|
||||
export const setVisitedViews = (views: TagView[]) => {
|
||||
export function setVisitedViews(views: TagView[]) {
|
||||
views.forEach((view) => {
|
||||
// 删除不必要的属性,防止 JSON.stringify 处理到循环引用
|
||||
delete view.matched
|
||||
@ -50,11 +50,11 @@ export const setVisitedViews = (views: TagView[]) => {
|
||||
})
|
||||
localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views))
|
||||
}
|
||||
export const getCachedViews = () => {
|
||||
export function getCachedViews() {
|
||||
const json = localStorage.getItem(CacheKey.CACHED_VIEWS)
|
||||
return JSON.parse(json ?? "[]") as string[]
|
||||
}
|
||||
export const setCachedViews = (views: string[]) => {
|
||||
export function setCachedViews(views: string[]) {
|
||||
localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views))
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
@ -1,5 +1,5 @@
|
||||
/** 获取指定元素(默认全局)上的 CSS 变量的值 */
|
||||
export const getCssVar = (varName: string, element: HTMLElement = document.documentElement) => {
|
||||
export function getCssVar(varName: string, element: HTMLElement = document.documentElement) {
|
||||
if (!varName?.startsWith("--")) {
|
||||
console.warn("CSS 变量名应以 '--' 开头")
|
||||
return ""
|
||||
@ -9,7 +9,7 @@ export const getCssVar = (varName: string, element: HTMLElement = document.docum
|
||||
}
|
||||
|
||||
/** 设置指定元素(默认全局)上的 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("--")) {
|
||||
console.warn("CSS 变量名应以 '--' 开头")
|
||||
return
|
||||
|
@ -3,7 +3,7 @@ import dayjs from "dayjs"
|
||||
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)
|
||||
return day.isValid() ? day.format(template) : INVALID_DATE
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
|
||||
/** 全局权限判断函数,和权限指令 v-permission 功能类似 */
|
||||
export const checkPermission = (permissionRoles: string[]): boolean => {
|
||||
export function checkPermission(permissionRoles: string[]): boolean {
|
||||
if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
|
||||
const { roles } = useUserStore()
|
||||
return roles.some((role) => permissionRoles.includes(role))
|
||||
return roles.some(role => permissionRoles.includes(role))
|
||||
} else {
|
||||
console.error("need roles! Like checkPermission(['admin','editor'])")
|
||||
return false
|
||||
|
@ -1,5 +1,5 @@
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { get, merge } from "lodash-es"
|
||||
import { getToken } from "./cache/cookies"
|
||||
@ -16,9 +16,9 @@ function createService() {
|
||||
const service = axios.create()
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config) => config,
|
||||
config => config,
|
||||
// 发送失败
|
||||
(error) => Promise.reject(error)
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
// 响应拦截(可根据具体业务作出相应的调整)
|
||||
service.interceptors.response.use(
|
||||
@ -27,7 +27,8 @@ function createService() {
|
||||
const apiData = response.data
|
||||
// 二进制数据则直接返回
|
||||
const responseType = response.request?.responseType
|
||||
if (responseType === "blob" || responseType === "arraybuffer") return apiData
|
||||
if (responseType === "blob" || responseType === "arraybuffer")
|
||||
return apiData
|
||||
// 这个 code 是和后端约定的业务 code
|
||||
const code = apiData.code
|
||||
// 如果没有 code, 代表这不是项目后端开发的 api
|
||||
@ -103,7 +104,7 @@ function createRequest(service: AxiosInstance) {
|
||||
const defaultConfig = {
|
||||
headers: {
|
||||
// 携带 Token
|
||||
Authorization: token ? `Bearer ${token}` : undefined,
|
||||
"Authorization": token ? `Bearer ${token}` : undefined,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
timeout: 5000,
|
||||
|
@ -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]"
|
||||
}
|
||||
|
||||
/** 判断是否为字符串 */
|
||||
export const isString = <T>(str: T) => {
|
||||
export function isString<T>(str: T) {
|
||||
return typeof str === "string" || str instanceof String
|
||||
}
|
||||
|
||||
/** 判断是否为外链 */
|
||||
export const isExternal = (path: string) => {
|
||||
export function isExternal(path: string) {
|
||||
const reg = /^(https?:|mailto:|tel:)/
|
||||
return reg.test(path)
|
||||
}
|
||||
|
||||
/** 判断是否为网址(带协议) */
|
||||
export const isUrl = (url: string) => {
|
||||
const reg = /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/
|
||||
export function isUrl(url: string) {
|
||||
const reg = /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{1,64})?\.)+[a-z]{2,6}\/?/
|
||||
return reg.test(url)
|
||||
}
|
||||
|
||||
/** 判断是否为网址或 IP(带端口) */
|
||||
export const isUrlPort = (url: string) => {
|
||||
export function isUrlPort(url: string) {
|
||||
const reg = /^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/
|
||||
return reg.test(url)
|
||||
}
|
||||
|
||||
/** 判断是否为域名(不带协议) */
|
||||
export const isDomain = (domain: string) => {
|
||||
const reg = /^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/
|
||||
export function isDomain(domain: string) {
|
||||
const reg = /^([0-9a-z-]+\.)+([a-z]{2,})$/i
|
||||
return reg.test(domain)
|
||||
}
|
||||
|
||||
/** 判断版本号格式是否为 X.Y.Z */
|
||||
export const isVersion = (version: string) => {
|
||||
export function isVersion(version: string) {
|
||||
const reg = /^\d+(?:\.\d+){2}$/
|
||||
return reg.test(version)
|
||||
}
|
||||
|
||||
/** 判断时间格式是否为 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$/
|
||||
return reg.test(time)
|
||||
}
|
||||
|
||||
/** 判断是否为手机号(1 开头) */
|
||||
export const isPhoneNumber = (str: string) => {
|
||||
export function isPhoneNumber(str: string) {
|
||||
const reg = /^(?:(?:\+|00)86)?1\d{10}$/
|
||||
return reg.test(str)
|
||||
}
|
||||
|
||||
/** 判断是否为第二代身份证(18 位) */
|
||||
export const 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]$/
|
||||
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]|[12]\d|30|31)\d{3}[\dX]$/i
|
||||
return reg.test(str)
|
||||
}
|
||||
|
||||
/** 判断是否为 Email(支持中文邮箱) */
|
||||
export const isEmail = (email: string) => {
|
||||
const reg = /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
|
||||
export function isEmail(email: string) {
|
||||
const reg = /^[A-Z0-9\u4E00-\u9FA5]+@[\w-]+(\.[\w-]+)+$/i
|
||||
return reg.test(email)
|
||||
}
|
||||
|
||||
/** 判断是否为 MAC 地址 */
|
||||
export const isMAC = (mac: string) => {
|
||||
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
|
||||
export function isMAC(mac: string) {
|
||||
const reg
|
||||
= /^(([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)
|
||||
}
|
||||
|
||||
/** 判断是否为 IPv4 地址 */
|
||||
export const isIPv4 = (ip: string) => {
|
||||
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]))?$/
|
||||
export function isIPv4(ip: string) {
|
||||
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])(?::(?:\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)
|
||||
}
|
||||
|
||||
/** 判断是否为车牌(兼容新能源车牌) */
|
||||
export const isLicensePlate = (str: string) => {
|
||||
const reg =
|
||||
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/
|
||||
export function isLicensePlate(str: string) {
|
||||
const reg
|
||||
= /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/
|
||||
return reg.test(str)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
|
||||
import Svg403 from "@/assets/error-page/403.svg?component" // vite-svg-loader 插件的功能
|
||||
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
|
||||
import Svg404 from "@/assets/error-page/404.svg?component" // vite-svg-loader 插件的功能
|
||||
import ErrorPageLayout from "./components/ErrorPageLayout.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -4,7 +4,9 @@
|
||||
<slot />
|
||||
</div>
|
||||
<router-link to="/">
|
||||
<el-button type="primary">回到首页</el-button>
|
||||
<el-button type="primary">
|
||||
回到首页
|
||||
</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useFetchSelect } from "@/hooks/useFetchSelect"
|
||||
import { getSelectDataApi } from "@/api/hook-demo/use-fetch-select"
|
||||
import { useFetchSelect } from "@/hooks/useFetchSelect"
|
||||
|
||||
const { loading, options, value } = useFetchSelect({
|
||||
api: getSelectDataApi
|
||||
@ -11,10 +11,10 @@ const { loading, options, value } = useFetchSelect({
|
||||
<div class="app-container">
|
||||
<h4>该示例是演示:通过 hook 自动调用 api 后拿到 Select 组件需要的数据并传递给 Select 组件</h4>
|
||||
<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-select>
|
||||
<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>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { getErrorApi, getSuccessApi } from "@/api/hook-demo/use-fullscreen-loading"
|
||||
import { useFullscreenLoading } from "@/hooks/useFullscreenLoading"
|
||||
import { getSuccessApi, getErrorApi } from "@/api/hook-demo/use-fullscreen-loading"
|
||||
import { ElMessage } from "element-plus"
|
||||
|
||||
const svg = `
|
||||
@ -21,7 +21,7 @@ const options = {
|
||||
svgViewBox: "-10, -10, 50, 50"
|
||||
}
|
||||
|
||||
const querySuccess = async () => {
|
||||
async function querySuccess() {
|
||||
// 注意:
|
||||
// 1. getSuccessApi 是一个函数而非函数调用
|
||||
// 2. 如需给 getSuccessApi 函数传递参数,请在后面的括号中进行(真正的 getSuccessApi 调用)
|
||||
@ -29,10 +29,11 @@ const querySuccess = async () => {
|
||||
ElMessage.success(`${res.message},传参为 ${res.data.list.toString()}`)
|
||||
}
|
||||
|
||||
const queryError = async () => {
|
||||
async function queryError() {
|
||||
try {
|
||||
await useFullscreenLoading(getErrorApi, options)()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error((error as Error).message)
|
||||
}
|
||||
}
|
||||
@ -41,7 +42,11 @@ const queryError = async () => {
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<h4>该示例是演示:通过将要执行的函数传递给 hook,让 hook 自动开启全屏 loading,函数执行结束后自动关闭 loading</h4>
|
||||
<el-button type="primary" @click="querySuccess">查询成功</el-button>
|
||||
<el-button type="danger" @click="queryError">查询失败</el-button>
|
||||
<el-button type="primary" @click="querySuccess">
|
||||
查询成功
|
||||
</el-button>
|
||||
<el-button type="danger" @click="queryError">
|
||||
查询失败
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { useWatermark } from "@/hooks/useWatermark"
|
||||
import { ref } from "vue"
|
||||
|
||||
const localRef = ref<HTMLElement | null>(null)
|
||||
const { setWatermark, clearWatermark } = useWatermark(localRef)
|
||||
@ -15,21 +15,29 @@ const { setWatermark: setGlobalWatermark, clearWatermark: clearGlobalWatermark }
|
||||
</h4>
|
||||
<div ref="localRef" class="local" />
|
||||
<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>
|
||||
<el-button type="danger" @click="clearWatermark">清除局部水印</el-button>
|
||||
<el-button type="danger" @click="clearWatermark">
|
||||
清除局部水印
|
||||
</el-button>
|
||||
</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
|
||||
type="warning"
|
||||
@click="setGlobalWatermark('没有防御功能的全局水印', { color: '#e6a23c', defense: false })"
|
||||
>
|
||||
关闭防御功能
|
||||
</el-button>
|
||||
<el-button type="danger" @click="clearGlobalWatermark">清除全局水印</el-button>
|
||||
<el-button type="danger" @click="clearGlobalWatermark">
|
||||
清除全局水印
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<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 { 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 { useFocus } from "./hooks/useFocus"
|
||||
|
||||
@ -36,7 +36,7 @@ const loginFormRules: FormRules = {
|
||||
code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
|
||||
}
|
||||
/** 登录逻辑 */
|
||||
const handleLogin = () => {
|
||||
function handleLogin() {
|
||||
loginFormRef.value?.validate((valid: boolean, fields) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
@ -58,7 +58,7 @@ const handleLogin = () => {
|
||||
})
|
||||
}
|
||||
/** 创建验证码 */
|
||||
const createCode = () => {
|
||||
function createCode() {
|
||||
// 先清空验证码的输入
|
||||
loginFormData.code = ""
|
||||
// 获取验证码
|
||||
@ -78,7 +78,7 @@ createCode()
|
||||
<Owl :close-eyes="isFocus" />
|
||||
<div class="login-card">
|
||||
<div class="title">
|
||||
<img src="@/assets/layouts/logo-text-2.png" />
|
||||
<img src="@/assets/layouts/logo-text-2.png">
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @keyup.enter="handleLogin">
|
||||
@ -116,7 +116,7 @@ createCode()
|
||||
size="large"
|
||||
>
|
||||
<template #append>
|
||||
<el-image :src="codeUrl" @click="createCode" draggable="false">
|
||||
<el-image :src="codeUrl" draggable="false" @click="createCode">
|
||||
<template #placeholder>
|
||||
<el-icon>
|
||||
<Picture />
|
||||
@ -131,7 +131,9 @@ createCode()
|
||||
</template>
|
||||
</el-input>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
defineOptions({
|
||||
name: "Menu1-1"
|
||||
name: "Menu11"
|
||||
})
|
||||
|
||||
const text = ref("")
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
defineOptions({
|
||||
name: "Menu1-2-1"
|
||||
name: "Menu121"
|
||||
})
|
||||
|
||||
const text = ref("")
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
defineOptions({
|
||||
name: "Menu1-2-2"
|
||||
name: "Menu122"
|
||||
})
|
||||
|
||||
const text = ref("")
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
defineOptions({
|
||||
name: "Menu1-3"
|
||||
name: "Menu13"
|
||||
})
|
||||
|
||||
const text = ref("")
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
const userStore = useUserStore()
|
||||
const switchRoles = ref(userStore.roles[0])
|
||||
|
@ -5,7 +5,9 @@ import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<SwitchRoles />
|
||||
<el-tag type="warning" size="large">当前页面只有 admin 角色可见,切换角色后将不能进入该页面</el-tag>
|
||||
<el-tag type="warning" size="large">
|
||||
当前页面只有 admin 角色可见,切换角色后将不能进入该页面
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { useRoute, useRouter } from "vue-router"
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
router.replace({ path: "/" + route.params.path, query: route.query })
|
||||
router.replace({ path: `/${route.params.path}`, query: route.query })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { createTableDataApi, deleteTableDataApi, updateTableDataApi, getTableDataApi } 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 type { CreateOrUpdateTableRequestData, TableData } from "@/api/table/types/table"
|
||||
import { createTableDataApi, deleteTableDataApi, getTableDataApi, updateTableDataApi } from "@/api/table"
|
||||
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 { reactive, ref, watch } from "vue"
|
||||
|
||||
defineOptions({
|
||||
// 命名当前组件
|
||||
@ -15,7 +15,7 @@ defineOptions({
|
||||
const loading = ref<boolean>(false)
|
||||
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
//#region 增
|
||||
// #region 增
|
||||
const DEFAULT_FORM_DATA: CreateOrUpdateTableRequestData = {
|
||||
id: undefined,
|
||||
username: "",
|
||||
@ -28,7 +28,7 @@ const formRules: FormRules<CreateOrUpdateTableRequestData> = {
|
||||
username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
|
||||
password: [{ required: true, trigger: "blur", message: "请输入密码" }]
|
||||
}
|
||||
const handleCreateOrUpdate = () => {
|
||||
function handleCreateOrUpdate() {
|
||||
formRef.value?.validate((valid: boolean, fields) => {
|
||||
if (!valid) return console.error("表单校验不通过", fields)
|
||||
loading.value = true
|
||||
@ -44,14 +44,14 @@ const handleCreateOrUpdate = () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
const resetForm = () => {
|
||||
function resetForm() {
|
||||
formRef.value?.clearValidate()
|
||||
formData.value = cloneDeep(DEFAULT_FORM_DATA)
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 删
|
||||
const handleDelete = (row: TableData) => {
|
||||
// #region 删
|
||||
function handleDelete(row: TableData) {
|
||||
ElMessageBox.confirm(`正在删除用户:${row.username},确认删除?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -63,23 +63,23 @@ const handleDelete = (row: TableData) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 改
|
||||
const handleUpdate = (row: TableData) => {
|
||||
// #region 改
|
||||
function handleUpdate(row: TableData) {
|
||||
dialogVisible.value = true
|
||||
formData.value = cloneDeep(row)
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 查
|
||||
// #region 查
|
||||
const tableData = ref<TableData[]>([])
|
||||
const searchFormRef = ref<FormInstance | null>(null)
|
||||
const searchData = reactive({
|
||||
username: "",
|
||||
phone: ""
|
||||
})
|
||||
const getTableData = () => {
|
||||
function getTableData() {
|
||||
loading.value = true
|
||||
getTableDataApi({
|
||||
currentPage: paginationData.currentPage,
|
||||
@ -98,14 +98,14 @@ const getTableData = () => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
const handleSearch = () => {
|
||||
function handleSearch() {
|
||||
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
|
||||
}
|
||||
const resetSearch = () => {
|
||||
function resetSearch() {
|
||||
searchFormRef.value?.resetFields()
|
||||
handleSearch()
|
||||
}
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
/** 监听分页参数的变化 */
|
||||
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-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card v-loading="loading" shadow="never">
|
||||
<div class="toolbar-wrapper">
|
||||
<div>
|
||||
<el-button type="primary" :icon="CirclePlus" @click="dialogVisible = true">新增用户</el-button>
|
||||
<el-button type="danger" :icon="Delete">批量删除</el-button>
|
||||
<el-button type="primary" :icon="CirclePlus" @click="dialogVisible = true">
|
||||
新增用户
|
||||
</el-button>
|
||||
<el-button type="danger" :icon="Delete">
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
admin
|
||||
</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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" />
|
||||
<el-table-column prop="status" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status" type="success" effect="plain" disable-transitions>启用</el-tag>
|
||||
<el-tag v-else type="danger" effect="plain" disable-transitions>禁用</el-tag>
|
||||
<el-tag v-if="scope.row.status" type="success" effect="plain" disable-transitions>
|
||||
启用
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger" effect="plain" disable-transitions>
|
||||
禁用
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" align="center" />
|
||||
<el-table-column fixed="right" label="操作" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -178,7 +196,7 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
|
||||
:page-sizes="paginationData.pageSizes"
|
||||
:total="paginationData.total"
|
||||
:page-size="paginationData.pageSize"
|
||||
:currentPage="paginationData.currentPage"
|
||||
:current-page="paginationData.currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
@ -188,20 +206,24 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="formData.id === undefined ? '新增用户' : '修改用户'"
|
||||
@closed="resetForm"
|
||||
width="30%"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" label-position="left">
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-input v-model="formData.username" placeholder="请输入" />
|
||||
</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-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleCreateOrUpdate" :loading="loading">确认</el-button>
|
||||
<el-button @click="dialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleCreateOrUpdate">
|
||||
确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
@ -1,25 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from "vue"
|
||||
import { type ElMessageBoxOptions, ElMessageBox, ElMessage } from "element-plus"
|
||||
import type { TableResponseData } from "@/api/table/types/table"
|
||||
import type { ElMessageBoxOptions } from "element-plus"
|
||||
import type { VxeFormInstance, VxeFormProps, VxeGridInstance, VxeGridProps, VxeModalInstance, VxeModalProps } from "vxe-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 StatusColumnSolts from "./tsx/StatusColumnSolts"
|
||||
import {
|
||||
type VxeGridInstance,
|
||||
type VxeGridProps,
|
||||
type VxeModalInstance,
|
||||
type VxeModalProps,
|
||||
type VxeFormInstance,
|
||||
type VxeFormProps
|
||||
} from "vxe-table"
|
||||
|
||||
defineOptions({
|
||||
// 命名当前组件
|
||||
name: "VxeTable"
|
||||
})
|
||||
|
||||
//#region vxe-grid
|
||||
// #region vxe-grid
|
||||
interface RowMeta {
|
||||
id: string
|
||||
username: string
|
||||
@ -168,9 +162,9 @@ const xGridOpt: VxeGridProps = reactive({
|
||||
}
|
||||
}
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region vxe-modal
|
||||
// #region vxe-modal
|
||||
const xModalDom = ref<VxeModalInstance>()
|
||||
const xModalOpt: VxeModalProps = reactive({
|
||||
title: "",
|
||||
@ -182,9 +176,9 @@ const xModalOpt: VxeModalProps = reactive({
|
||||
return Promise.resolve()
|
||||
}
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region vxe-form
|
||||
// #region vxe-form
|
||||
const xFormDom = ref<VxeFormInstance>()
|
||||
const xFormOpt: VxeFormProps = reactive({
|
||||
span: 24,
|
||||
@ -253,9 +247,9 @@ const xFormOpt: VxeFormProps = reactive({
|
||||
]
|
||||
}
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region 增删改查
|
||||
// #region 增删改查
|
||||
const crudStore = reactive({
|
||||
/** 表单类型,true 表示修改,false 表示新增 */
|
||||
isUpdate: true,
|
||||
@ -346,7 +340,7 @@ const crudStore = reactive({
|
||||
/** 更多自定义方法 */
|
||||
moreFn: () => {}
|
||||
})
|
||||
//#endregion
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -355,13 +349,21 @@ const crudStore = reactive({
|
||||
<vxe-grid ref="xGridDom" v-bind="xGridOpt">
|
||||
<!-- 左侧按钮列表 -->
|
||||
<template #toolbar-btns>
|
||||
<vxe-button status="primary" icon="vxe-icon-add" @click="crudStore.onShowModal()">新增用户</vxe-button>
|
||||
<vxe-button status="danger" icon="vxe-icon-delete">批量删除</vxe-button>
|
||||
<vxe-button status="primary" icon="vxe-icon-add" @click="crudStore.onShowModal()">
|
||||
新增用户
|
||||
</vxe-button>
|
||||
<vxe-button status="danger" icon="vxe-icon-delete">
|
||||
批量删除
|
||||
</vxe-button>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #row-operate="{ row }">
|
||||
<el-button link type="primary" @click="crudStore.onShowModal(row)">修改</el-button>
|
||||
<el-button link type="danger" @click="crudStore.onDelete(row)">删除</el-button>
|
||||
<el-button link type="primary" @click="crudStore.onShowModal(row)">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="crudStore.onDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
<!-- 弹窗 -->
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
import type { VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
|
||||
const solts: VxeColumnPropTypes.Slots = {
|
||||
default: ({ row, column }) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
import type { VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
|
||||
const solts: VxeColumnPropTypes.Slots = {
|
||||
default: ({ row, column }) => {
|
||||
|
@ -2,10 +2,16 @@
|
||||
<div h-full uno-padding-20>
|
||||
<div h-full text-center flex select-none all:transition-400>
|
||||
<div ma>
|
||||
<div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-1s>UnoCSS</div>
|
||||
<div op30 dark:op60 text-lg fw300 m1>该页面是一个 UnoCSS 的使用案例,其他页面依旧采用 Scss</div>
|
||||
<div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-1s>
|
||||
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">
|
||||
<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>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { shallowMount } from "@vue/test-utils"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import Notify from "@/components/Notify/index.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("正常渲染", () => {
|
||||
const wrapper = shallowMount(Notify)
|
||||
expect(wrapper.classes("notify")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("NotifyList", () => {
|
||||
it("List 长度为 0", () => {
|
||||
describe("notifyList", () => {
|
||||
it("list 长度为 0", () => {
|
||||
const wrapper = shallowMount(NotifyList, {
|
||||
props: {
|
||||
list: []
|
||||
@ -19,7 +19,7 @@ describe("NotifyList", () => {
|
||||
})
|
||||
expect(wrapper.find("el-empty").exists()).toBe(true)
|
||||
})
|
||||
it("List 长度不为 0", () => {
|
||||
it("list 长度不为 0", () => {
|
||||
const wrapper = shallowMount(NotifyList, {
|
||||
props: {
|
||||
list: [
|
||||
|
@ -1,32 +1,32 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { isArray } from "@/utils/validate"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
describe("isArray", () => {
|
||||
it("String", () => {
|
||||
it("string", () => {
|
||||
expect(isArray("")).toBe(false)
|
||||
})
|
||||
it("Number", () => {
|
||||
it("number", () => {
|
||||
expect(isArray(1)).toBe(false)
|
||||
})
|
||||
it("Boolean", () => {
|
||||
it("boolean", () => {
|
||||
expect(isArray(true)).toBe(false)
|
||||
})
|
||||
it("Null", () => {
|
||||
it("null", () => {
|
||||
expect(isArray(null)).toBe(false)
|
||||
})
|
||||
it("Undefined", () => {
|
||||
it("undefined", () => {
|
||||
expect(isArray(undefined)).toBe(false)
|
||||
})
|
||||
it("Symbol", () => {
|
||||
it("symbol", () => {
|
||||
expect(isArray(Symbol())).toBe(false)
|
||||
})
|
||||
it("BigInt", () => {
|
||||
it("bigInt", () => {
|
||||
expect(isArray(BigInt(1))).toBe(false)
|
||||
})
|
||||
it("Object", () => {
|
||||
it("object", () => {
|
||||
expect(isArray({})).toBe(false)
|
||||
})
|
||||
it("Array Object", () => {
|
||||
it("array object", () => {
|
||||
expect(isArray([])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
@ -1,24 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
/** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
/** TS 严格模式 */
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"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"],
|
||||
"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": [
|
||||
"node",
|
||||
"vite/client",
|
||||
@ -26,12 +23,15 @@
|
||||
"element-plus/global",
|
||||
"vitest"
|
||||
],
|
||||
/** baseUrl 用来告诉编译器到哪里去查找模块,使用非相对模块时必须配置此项 */
|
||||
"baseUrl": ".",
|
||||
/** 非相对模块导入的路径映射配置,根据 baseUrl 配置进行路径计算 */
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
/** TS 严格模式 */
|
||||
"strict": true,
|
||||
"importHelpers": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
/** https://cn.vitejs.dev/guide/features.html#typescript-compiler-options */
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
2
types/global-components.d.ts
vendored
2
types/global-components.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue"
|
||||
import type SvgIcon from "@/components/SvgIcon/index.vue"
|
||||
|
||||
/** 由 app.component 全局注册的组件需要在这里声明 TS 类型才能获得 Volar 插件提供的类型提示) */
|
||||
declare module "vue" {
|
||||
|
@ -1,12 +1,13 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import { type ConfigEnv, type UserConfigExport, loadEnv } from "vite"
|
||||
import path, { resolve } from "path"
|
||||
import type { ConfigEnv, UserConfigExport } from "vite"
|
||||
import path, { resolve } from "node:path"
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx"
|
||||
import UnoCSS from "unocss/vite"
|
||||
import { loadEnv } from "vite"
|
||||
import { createSvgIconsPlugin } from "vite-plugin-svg-icons"
|
||||
import svgLoader from "vite-svg-loader"
|
||||
import UnoCSS from "unocss/vite"
|
||||
|
||||
/** 配置项文档:https://cn.vitejs.dev/config */
|
||||
export default ({ mode }: ConfigEnv): UserConfigExport => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user