Template
1
0
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:
pany 2024-11-18 19:40:44 +08:00
parent c8571a5f55
commit 7f02e18d37
93 changed files with 704 additions and 508 deletions

View File

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

View File

@ -67,6 +67,7 @@
"@vitejs/plugin-vue-jsx": "4.1.0",
"@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
View File

@ -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: {}

View File

@ -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>"
}

View File

@ -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

View File

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

View File

@ -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[] }>

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

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

View File

@ -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)
}

View File

@ -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 {

View File

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

View File

@ -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

View File

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

View File

@ -1,4 +1,4 @@
import { type Directive } from "vue"
import type { Directive } from "vue"
import { useUserStore } from "@/store/modules/user"
/** 权限指令,和权限判断函数 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 {

View File

@ -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)

View File

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

View File

@ -1,4 +1,4 @@
import { type LoadingOptions, ElLoading } from "element-plus"
import { ElLoading, type LoadingOptions } from "element-plus"
const defaultOptions = {
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()
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
// 缓存最新的路由信息

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

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

View File

@ -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>

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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")
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

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

View File

@ -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(() => {

View File

@ -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"

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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 || []

View File

@ -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()
}

View File

@ -1,12 +1,12 @@
import { router } from "@/router"
import { useUserStoreHook } from "@/store/modules/user"
import { usePermissionStoreHook } from "@/store/modules/permission"
import { ElMessage } from "element-plus"
import { setRouteChange } from "@/hooks/useRouteListener"
import { useTitle } from "@/hooks/useTitle"
import { getToken } from "@/utils/cache/cookies"
import routeSettings from "@/config/route"
import 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 || "路由守卫过程发生错误")

View File

@ -1,8 +1,8 @@
import { reactive, ref, watch } from "vue"
import { DeviceEnum, SIDEBAR_CLOSED, SIDEBAR_OPENED } from "@/constants/app-key"
import { pinia } from "@/store"
import { 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)
)
/** 切换侧边栏 */

View File

@ -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 }

View File

@ -1,8 +1,10 @@
import { type Ref, ref, watch } from "vue"
import type { LayoutSettings } from "@/config/layouts"
import type { Ref } from "vue"
import { layoutSettings } from "@/config/layouts"
import { pinia } from "@/store"
import { 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

View File

@ -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,

View File

@ -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)
// 用刷新页面代替重新登录

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -1,84 +1,84 @@
/** 判断是否为数组 */
export const isArray = <T>(arg: T) => {
export function isArray<T>(arg: T) {
return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]"
}
/** 判断是否为字符串 */
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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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])

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
<!-- 弹窗 -->

View File

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

View File

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

View File

@ -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>

View File

@ -1,17 +1,17 @@
import { shallowMount } from "@vue/test-utils"
import { describe, expect, it } from "vitest"
import Notify from "@/components/Notify/index.vue"
import 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: [

View File

@ -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)
})
})

View File

@ -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",

View File

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

View File

@ -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 => {