Compare commits

...
17 Commits
Author SHA1 Message Date
Hooray ce810aa633 chore: release v5.4.0 2025-05-30 14:52:21 +08:00
Hooray eabb43ecab chore: 更新依赖 2025-05-30 14:49:31 +08:00
Hooray 8bfa0f0c27 fix: 修复激活标签页定位可能不居中的问题 2025-05-27 18:19:59 +08:00
Hooray 05261b37bb fix: 修复次导航收起时页面展示错误 2025-05-26 14:35:24 +08:00
Hooray b974139eeb feat: 将 FaDropdown 组件插槽名从 label 修改为 header,并新增 footer 插槽 2025-05-21 12:01:08 +08:00
Hooray 867852e501 refactor: 优化布局相关 CSS 变量处理逻辑 2025-05-15 16:04:03 +08:00
Hooray 2cadef4ad1 chore: 更新依赖 2025-05-15 10:50:47 +08:00
Hooray 3b889f611f feat: 增加请求重试机制 2025-05-14 17:30:42 +08:00
Hooray 9f37438669 fix: 优化菜单点击处理逻辑,增加对链接的判断 2025-05-14 14:14:00 +08:00
Hooray b7c627c556 feat: 新增 FaProgress 组件 2025-05-14 10:43:27 +08:00
Hooray 7d2279f38d chore: typo 2025-05-06 09:43:44 +08:00
Hooray eb306b18bc feat: 新增 FaHoverCard 组件 2025-05-06 03:01:16 +08:00
Hooray 8d4e226c17 feat: 对未设置 redirect 的父路由,访问时将自动重定向到允许访问的子路由 2025-05-05 00:59:11 +08:00
Hooray 1edf486160 refactor: 优化 FaScrollArea 滚动体验 2025-05-02 23:19:32 +08:00
Hooray e2d058abdd refactor: 调整标签页关闭逻辑,优先跳转至前一个标签页 2025-05-01 20:09:23 +08:00
Hooray 9b5003fad8 feat: FaScrollArea 组件新增 onScroll 事件 2025-05-01 17:07:21 +08:00
Hooray cbef1bb52e refacttor: toast 代替 ElMessage 2025-04-30 15:09:54 +08:00
105 changed files with 2275 additions and 1554 deletions
+38 -38
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "5.3.0",
"version": "5.4.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -25,87 +25,87 @@
},
"dependencies": {
"@vee-validate/zod": "^4.15.0",
"@vueuse/components": "^13.1.0",
"@vueuse/core": "^13.1.0",
"@vueuse/integrations": "^13.1.0",
"@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0",
"@vueuse/integrations": "^13.3.0",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"defu": "^6.1.4",
"disable-devtool": "^0.3.8",
"element-plus": "^2.9.9",
"element-plus": "^2.9.11",
"eruda": "^3.4.1",
"es-toolkit": "^1.36.0",
"hotkeys-js": "^3.13.9",
"lucide-vue-next": "^0.503.0",
"es-toolkit": "^1.38.0",
"hotkeys-js": "^3.13.10",
"lucide-vue-next": "^0.511.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "^8.2.0",
"pinia": "^3.0.2",
"qs": "^6.14.0",
"reka-ui": "^2.2.0",
"reka-ui": "^2.3.0",
"scule": "^1.3.0",
"tailwind-merge": "^3.2.0",
"tailwind-merge": "^3.3.0",
"ua-parser-js": "^2.0.3",
"vconsole": "^3.15.1",
"vee-validate": "^4.15.0",
"vue": "^3.5.13",
"vue": "^3.5.16",
"vue-router": "^4.5.1",
"vue-sonner": "^1.3.2",
"zod": "^3.24.3"
"vue-sonner": "^2.0.0",
"zod": "^3.25.41"
},
"devDependencies": {
"@antfu/eslint-config": "4.3.0",
"@faker-js/faker": "^9.7.0",
"@iconify/json": "^2.2.333",
"@iconify/vue": "^4.3.0",
"@antfu/eslint-config": "^4.13.2",
"@faker-js/faker": "^9.8.0",
"@iconify/json": "^2.2.342",
"@iconify/vue": "^5.0.0",
"@stylistic/stylelint-config": "^2.0.0",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@unocss/eslint-plugin": "^66.0.0",
"@unocss/preset-legacy-compat": "^66.0.0",
"@types/qs": "^6.14.0",
"@unocss/eslint-plugin": "^66.1.2",
"@unocss/preset-legacy-compat": "^66.1.2",
"@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vitejs/plugin-vue": "^5.2.4",
"@vitejs/plugin-vue-jsx": "^4.2.0",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.21",
"boxen": "^8.0.1",
"bumpp": "^10.1.0",
"bumpp": "^10.1.1",
"cz-git": "^1.11.1",
"eslint": "^9.25.1",
"eslint": "^9.27.0",
"esno": "^4.8.0",
"fs-extra": "^11.3.0",
"http-server": "^14.1.1",
"inquirer": "^12.6.0",
"lint-staged": "^15.5.1",
"npm-run-all2": "^7.0.2",
"inquirer": "^12.6.3",
"lint-staged": "^16.1.0",
"npm-run-all2": "^8.0.4",
"picocolors": "^1.1.1",
"plop": "^4.0.1",
"postcss": "^8.5.3",
"postcss": "^8.5.4",
"postcss-nested": "^7.0.2",
"sass-embedded": "^1.87.0",
"sass-embedded": "^1.89.0",
"simple-git-hooks": "^2.13.0",
"stylelint": "^16.19.1",
"stylelint": "^16.20.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-scss": "^6.11.1",
"stylelint-scss": "^6.12.0",
"svgo": "^3.3.2",
"taze": "^19.0.4",
"taze": "^19.1.0",
"typescript": "^5.8.3",
"unocss": "^66.0.0",
"unocss": "^66.1.2",
"unocss-preset-animations": "^1.2.1",
"unplugin-auto-import": "^19.1.2",
"unplugin-auto-import": "^19.3.0",
"unplugin-turbo-console": "^2.1.3",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.2.7",
"unplugin-vue-components": "^28.7.0",
"vite": "^6.3.5",
"vite-plugin-app-loading": "^0.3.1",
"vite-plugin-archiver": "^0.1.2",
"vite-plugin-banner": "^0.8.1",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-compression2": "^1.4.0",
"vite-plugin-env-parse": "^1.0.15",
"vite-plugin-fake-server": "^2.2.0",
"vite-plugin-pages": "^0.33.0",
+1810 -1325
View File
File diff suppressed because it is too large Load Diff
+58 -18
View File
@@ -1,7 +1,19 @@
import useUserStore from '@/store/modules/user'
import axios from 'axios'
// import qs from 'qs'
import { toast } from 'vue-sonner'
import useUserStore from '@/store/modules/user'
// 请求重试配置
const MAX_RETRY_COUNT = 3 // 最大重试次数
const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)
// 扩展 AxiosRequestConfig 类型
declare module 'axios' {
export interface AxiosRequestConfig {
retry?: boolean
retryCount?: number
}
}
const api = axios.create({
baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY) ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
@@ -29,6 +41,28 @@ api.interceptors.request.use(
},
)
// 处理错误信息的函数
function handleError(error: any) {
if (error.status === 401) {
useUserStore().requestLogout()
throw error
}
let message = error.message
if (message === 'Network Error') {
message = '后端网络故障'
}
else if (message.includes('timeout')) {
message = '接口请求超时'
}
else if (message.includes('Request failed with status code')) {
message = `接口${message.substr(message.length - 3)}异常`
}
toast.error('Error', {
description: message,
})
return Promise.reject(error)
}
api.interceptors.response.use(
(response) => {
/**
@@ -50,25 +84,31 @@ api.interceptors.response.use(
}
return Promise.resolve(response.data)
},
(error) => {
if (error.status === 401) {
useUserStore().requestLogout()
throw error
async (error) => {
// 获取请求配置
const config = error.config
// 如果配置不存在或未启用重试,则直接处理错误
if (!config || !config.retry) {
return handleError(error)
}
let message = error.message
if (message === 'Network Error') {
message = '后端网络故障'
// 设置重试次数
config.retryCount = config.retryCount || 0
// 判断是否超过重试次数
if (config.retryCount >= MAX_RETRY_COUNT) {
return handleError(error)
}
else if (message.includes('timeout')) {
message = '接口请求超时'
}
else if (message.includes('Request failed with status code')) {
message = `接口${message.substr(message.length - 3)}异常`
}
toast.error('Error', {
description: message,
})
return Promise.reject(error)
// 重试次数自增
config.retryCount += 1
// 延迟重试
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
// 重新发起请求
return api(config)
},
)
+1 -1
View File
@@ -41,7 +41,7 @@ const isProfileShow = ref(false)
],
]" class="flex-center"
>
<template #label>
<template #header>
<div class="space-y-2">
<div class="text-xs text-secondary-foreground/50 font-light">
当前登录账号
@@ -1,10 +1,10 @@
<script setup lang="ts">
import useUserStore from '@/store/modules/user'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { toast } from 'vue-sonner'
import * as z from 'zod'
import useUserStore from '@/store/modules/user'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
defineOptions({
name: 'EditPasswordForm',
+2 -2
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import useUserStore from '@/store/modules/user'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import useUserStore from '@/store/modules/user'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
defineOptions({
name: 'LoginForm',
+1 -1
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
defineOptions({
name: 'RegisterForm',
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
defineOptions({
name: 'ResetPasswordForm',
+4 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { UploadProps, UploadUserFile } from 'element-plus'
import { ElMessage } from 'element-plus'
import { toast } from 'vue-sonner'
defineOptions({
name: 'FileUpload',
@@ -47,16 +47,16 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isTypeOk = props.ext.includes(fileExt)
const isSizeOk = file.size / 1024 / 1024 < props.size
if (!isTypeOk) {
ElMessage.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
toast.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
}
if (!isSizeOk) {
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
toast.error(`上传文件大小不能超过 ${props.size}MB!`)
}
return isTypeOk && isSizeOk
}
const onExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('文件上传超过限制')
toast.warning('文件上传超过限制')
}
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
+3 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
import { toast } from 'vue-sonner'
defineOptions({
name: 'ImageUpload',
@@ -67,10 +67,10 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isTypeOk = props.ext.includes(fileExt)
const isSizeOk = file.size / 1024 / 1024 < props.size
if (!isTypeOk) {
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
}
if (!isSizeOk) {
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
toast.error(`上传图片大小不能超过 ${props.size}MB!`)
}
if (isTypeOk && isSizeOk) {
uploadData.value.progress.preview = URL.createObjectURL(file)
+3 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
import { toast } from 'vue-sonner'
defineOptions({
name: 'ImagesUpload',
@@ -81,10 +81,10 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isTypeOk = props.ext.includes(fileExt)
const isSizeOk = file.size / 1024 / 1024 < props.size
if (!isTypeOk) {
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
}
if (!isSizeOk) {
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
toast.error(`上传图片大小不能超过 ${props.size}MB!`)
}
if (isTypeOk && isSizeOk) {
uploadData.value.progress.preview = URL.createObjectURL(file)
+2 -2
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { toast } from 'vue-sonner'
import settingsDefault from '@/settings.default'
import useMenuStore from '@/store/modules/menu'
import useSettingsStore from '@/store/modules/settings'
import eventBus from '@/utils/eventBus'
import { diffTwoObj } from '@/utils/object'
import { useClipboard } from '@vueuse/core'
import { toast } from 'vue-sonner'
defineOptions({
name: 'AppSetting',
+1 -1
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import hotkeys from 'hotkeys-js'
import { useSlots } from '@/slots'
import useMenuStore from '@/store/modules/menu'
import useSettingsStore from '@/store/modules/settings'
import hotkeys from 'hotkeys-js'
import Logo from '../Logo/index.vue'
defineOptions({
+4 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SubMenuProps } from './types'
import { cn } from '@/utils'
import { useTimeoutFn } from '@vueuse/core'
import { cn } from '@/utils'
import Item from './item.vue'
import { rootMenuInjectionKey } from './types'
@@ -112,6 +112,9 @@ function handleClick() {
if (rootMenu.isMenuPopup && hasChildren.value) {
return
}
if (props.menu.meta?.link) {
return
}
if (hasChildren.value) {
rootMenu.handleSubMenuClick(index, props.uniqueKey)
}
+16 -6
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
import type { Tabbar } from '#/global'
import { useSlots } from '@/slots'
import useSettingsStore from '@/store/modules/settings'
import useTabbarStore from '@/store/modules/tabbar'
import { useMagicKeys } from '@vueuse/core'
import hotkeys from 'hotkeys-js'
import { toast } from 'vue-sonner'
import { useSlots } from '@/slots'
import useSettingsStore from '@/store/modules/settings'
import useTabbarStore from '@/store/modules/tabbar'
defineOptions({
name: 'Tabbar',
@@ -30,10 +30,20 @@ const tabRef = useTemplateRef<HTMLElement[]>('tabRef')
watch(() => route, (val) => {
if (settingsStore.settings.tabbar.enable) {
tabbarStore.add(val).then(() => {
tabbarStore.add(val)
nextTick(() => {
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
if (index !== -1) {
tabRef.value && tabsRef.value?.scrollTo(tabRef.value[index].offsetLeft - tabsRef.value.ref?.$el.clientWidth * 0.4)
const tabEl = tabRef.value?.find(item => Number.parseInt(item.dataset.index!) === index)
const containerEl = tabsRef.value?.ref?.$el
if (tabEl && containerEl) {
const tabLeft = tabEl.offsetLeft
const tabWidth = tabEl.offsetWidth
const containerWidth = containerEl.clientWidth
// 计算滚动位置,使标签页居中
const scrollLeft = tabLeft - (containerWidth - tabWidth) / 2
tabsRef.value?.scrollTo(scrollLeft)
}
tabbarScrollTip()
}
})
@@ -147,7 +157,7 @@ onUnmounted(() => {
<div class="tabbar">
<component :is="useSlots('tabbar-start')" />
<div class="tabbar-container">
<FaScrollArea ref="tabsRef" horizontal :scrollbar="false" mask gradient-color="var(--g-tabbar-bg)" class="tabs">
<FaScrollArea ref="tabsRef" :scrollbar="false" mask horizontal gradient-color="var(--g-tabbar-bg)" class="tabs">
<TransitionGroup ref="tabContainerRef" name="tabbar" tag="div" class="tab-container">
<div
v-for="(element, index) in tabbarStore.list" :key="element.tabId"
@@ -1,6 +1,6 @@
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
import { compile } from 'path-to-regexp'
import useSettingsStore from '@/store/modules/settings'
import Breadcrumb from '../../../Breadcrumb/index.vue'
import BreadcrumbItem from '../../../Breadcrumb/item.vue'
@@ -1,6 +1,6 @@
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
import { useFullscreen } from '@vueuse/core'
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'Fullscreen',
@@ -1,16 +1,16 @@
<script setup lang="ts">
import type { Menu } from '@/types/global'
import { useFocus } from '@vueuse/core'
import hotkeys from 'hotkeys-js'
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
import useMenuStore from '@/store/modules/menu'
import useRouteStore from '@/store/modules/route'
import useSettingsStore from '@/store/modules/settings'
import { resolveRoutePath } from '@/utils'
import { useFocus } from '@vueuse/core'
import hotkeys from 'hotkeys-js'
defineOptions({
name: 'Search',
name: 'NavSearchModal',
})
const isShow = defineModel<boolean>({
@@ -1,6 +1,6 @@
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
import hotkeys from 'hotkeys-js'
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'PageReload',
+6 -6
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useElementSize } from '@vueuse/core'
import useSettingsStore from '@/store/modules/settings'
import Tabbar from './Tabbar/index.vue'
import Toolbar from './Toolbar/index.vue'
@@ -20,11 +21,10 @@ const enableToolbar = computed(() => {
const scrollTop = ref(0)
const scrollOnHide = ref(false)
const topbarHeight = computed(() => {
const tabbarHeight = settingsStore.settings.tabbar.enable ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-tabbar-height')) : 0
const toolbarHeight = enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
return tabbarHeight + toolbarHeight
})
const topbarRef = useTemplateRef('topbarRef')
const { height: topbarHeight } = useElementSize(topbarRef)
onMounted(() => {
window.addEventListener('scroll', onScroll)
})
@@ -42,7 +42,7 @@ watch(scrollTop, (val, oldVal) => {
<template>
<FaSmartFixedBlock position="top" :class="{ 'absolute!': settingsStore.settings.topbar.mode === 'static' }">
<div
class="topbar-container transition-[transform,box-shadow]-300" :class="{
ref="topbarRef" class="topbar-container transition-[transform,box-shadow]-300" :class="{
[`topbar-${settingsStore.settings.topbar.mode}`]: true,
mask: scrollTop,
hide: scrollOnHide,
+30 -44
View File
@@ -22,51 +22,36 @@ const settingsStore = useSettingsStore()
const keepAliveStore = useKeepAliveStore()
const menuStore = useMenuStore()
// 头部当前实际高度
const headerActualHeight = computed(() => {
let actualHeight = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-header-height'))
if (['single', 'side'].includes(settingsStore.settings.menu.mode) || settingsStore.mode === 'mobile') {
actualHeight = 0
}
return actualHeight
// 头部是否隐藏
const isHeaderHide = computed(() => {
return ['single', 'side'].includes(settingsStore.settings.menu.mode) || settingsStore.mode === 'mobile'
})
// 侧边栏主导航当前实际宽度
const mainSidebarActualWidth = computed(() => {
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
if (settingsStore.settings.menu.mode === 'single' || (settingsStore.settings.menu.mode === 'head' && settingsStore.mode !== 'mobile')) {
actualWidth = 0
}
return actualWidth
// 侧边栏主导航是否隐藏
const isMainSidebarHide = computed(() => {
return settingsStore.settings.menu.mode === 'single'
|| (settingsStore.settings.menu.mode === 'head' && settingsStore.mode !== 'mobile')
})
// 侧边栏次导航当前实际宽度
const subSidebarActualWidth = computed(() => {
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
if (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile') {
actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-collapse-width'))
}
if (menuStore.sidebarMenus.every(item => item.meta?.menu === false)) {
actualWidth = 0
}
return actualWidth
// 侧边栏次导航是否隐藏
const isSubSidebarHide = computed(() => {
return menuStore.sidebarMenus.every(item => item.meta?.menu === false)
})
// 顶栏当前实际高度
const topbarActualHeight = computed(() => {
let actualHeight = 0
if (settingsStore.settings.tabbar.enable) {
actualHeight += Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-tabbar-height'))
}
if (settingsStore.settings.toolbar.enable && Object.keys(settingsStore.settings.toolbar).some((key) => {
if (settingsStore.settings.app.routeBaseOn === 'filesystem' && key === 'breadcrumb') {
return false
}
return settingsStore.settings.toolbar[key as keyof typeof settingsStore.settings.toolbar]
})) {
actualHeight += Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-toolbar-height'))
}
return actualHeight
// 标签栏是否隐藏
const isTabbarHide = computed(() => {
return !settingsStore.settings.tabbar.enable
})
// 工具栏是否隐藏
const isToolbarHide = computed(() => {
return !settingsStore.settings.toolbar.enable
|| !Object.keys(settingsStore.settings.toolbar).some((key) => {
if (settingsStore.settings.app.routeBaseOn === 'filesystem' && key === 'breadcrumb') {
return false
}
return settingsStore.settings.toolbar[key as keyof typeof settingsStore.settings.toolbar]
})
})
const isLink = computed(() => !!routeInfo.meta.link)
@@ -96,10 +81,11 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
<template>
<div
class="layout" :style="{
'--g-header-actual-height': `${headerActualHeight}px`,
'--g-main-sidebar-actual-width': `${mainSidebarActualWidth}px`,
'--g-sub-sidebar-actual-width': `${subSidebarActualWidth}px`,
'--g-topbar-actual-height': `${topbarActualHeight}px`,
'--g-header-actual-height': isHeaderHide ? '0px' : 'var(--g-header-height)',
'--g-main-sidebar-actual-width': isMainSidebarHide ? '0px' : 'var(--g-main-sidebar-width)',
'--g-sub-sidebar-actual-width': isSubSidebarHide ? '0px' : (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile' ? 'var(--g-sub-sidebar-collapse-width)' : 'var(--g-sub-sidebar-width)'),
'--g-tabbar-actual-height': isTabbarHide ? '0px' : 'var(--g-tabbar-height)',
'--g-toolbar-actual-height': isToolbarHide ? '0px' : 'var(--g-toolbar-height)',
}"
>
<div id="app-main">
@@ -214,7 +200,7 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
position: relative;
flex: auto;
height: 100%;
margin: var(--g-topbar-actual-height) 0 0;
margin: calc(var(--g-tabbar-actual-height) + var(--g-toolbar-actual-height)) 0 0;
overflow: hidden;
}
}
-2
View File
@@ -1,12 +1,10 @@
// 加载 iconify 图标
import { downloadAndInstall } from '@/iconify'
import icons from '@/iconify/index.json'
// 自定义指令
import directive from '@/utils/directive'
import App from './App.vue'
import router from './router'
import pinia from './store'
import uiProvider from './ui/provider'
-3
View File
@@ -18,7 +18,6 @@ export default defineFakeRoute([
{
path: '/multilevel_menu_example',
component: 'Layout',
redirect: '/multilevel_menu_example/page',
name: 'multilevelMenuExample',
meta: {
title: '多级导航',
@@ -36,7 +35,6 @@ export default defineFakeRoute([
{
path: 'level2',
name: 'multilevelMenuExample2',
redirect: '/multilevel_menu_example/level2/page',
meta: {
title: '导航2',
},
@@ -52,7 +50,6 @@ export default defineFakeRoute([
{
path: 'level3',
name: 'multilevelMenuExample2-2',
redirect: '/multilevel_menu_example/level2/level3/page1',
meta: {
title: '导航2-2',
},
+22 -1
View File
@@ -1,10 +1,10 @@
import type { Router, RouteRecordRaw } from 'vue-router'
import { useNProgress } from '@vueuse/integrations/useNProgress'
import useKeepAliveStore from '@/store/modules/keepAlive'
import useMenuStore from '@/store/modules/menu'
import useRouteStore from '@/store/modules/route'
import useSettingsStore from '@/store/modules/settings'
import useUserStore from '@/store/modules/user'
import { useNProgress } from '@vueuse/integrations/useNProgress'
import { asyncRoutes, asyncRoutesByFilesystem } from './routes'
import '@/assets/styles/nprogress.css'
@@ -112,6 +112,26 @@ function setupRoutes(router: Router) {
})
}
// 当父级路由未配置重定向时,自动重定向到有访问权限的子路由
function setupRedirectAuthChildrenRoute(router: Router) {
router.beforeEach((to, _from, next) => {
const { auth } = useAuth()
const currentRoute = router.getRoutes().find(route => route.path === (to.matched.at(-1)?.path ?? ''))
if (!currentRoute?.redirect) {
const findAuthRoute = currentRoute?.children?.find(route => route.meta?.menu !== false && auth(route.meta?.auth ?? ''))
if (findAuthRoute) {
next(findAuthRoute)
}
else {
next()
}
}
else {
next()
}
})
}
// 进度条
function setupProgress(router: Router) {
const { isLoading } = useNProgress()
@@ -208,6 +228,7 @@ function setupOther(router: Router) {
export default function setupGuards(router: Router) {
setupRoutes(router)
setupRedirectAuthChildrenRoute(router)
setupProgress(router)
setupTitle(router)
setupKeepAlive(router)
+2 -2
View File
@@ -1,7 +1,7 @@
import pinia from '@/store'
import useSettingsStore from '@/store/modules/settings'
import { loadingFadeOut } from 'virtual:app-loading'
import { createRouter, createWebHashHistory } from 'vue-router'
import pinia from '@/store'
import useSettingsStore from '@/store/modules/settings'
import setupExtensions from './extensions'
import setupGuards from './guards'
// 路由相关数据
@@ -7,7 +7,6 @@ function Layout() {
const routes: RouteRecordRaw = {
path: '/multilevel_menu_example',
component: Layout,
redirect: '/multilevel_menu_example/page',
name: 'multilevelMenuExample',
meta: {
title: '多级导航',
@@ -25,7 +24,6 @@ const routes: RouteRecordRaw = {
{
path: 'level2',
name: 'multilevelMenuExample2',
redirect: '/multilevel_menu_example/level2/page',
meta: {
title: '导航2',
},
@@ -41,7 +39,6 @@ const routes: RouteRecordRaw = {
{
path: 'level3',
name: 'multilevelMenuExample2-2',
redirect: '/multilevel_menu_example/level2/level3/page1',
meta: {
title: '导航2-2',
},
+1 -1
View File
@@ -1,8 +1,8 @@
import type { Route } from '#/global'
import type { RouteRecordRaw } from 'vue-router'
import useSettingsStore from '@/store/modules/settings'
import generatedRoutes from 'virtual:generated-pages'
import { setupLayouts } from 'virtual:meta-layouts'
import useSettingsStore from '@/store/modules/settings'
import MultilevelMenuExample from './modules/multilevel.menu.example'
// 固定路由(默认路由)
+1 -1
View File
@@ -1,7 +1,7 @@
import type { RecursiveRequired, Settings } from '#/global'
import { cloneDeep } from 'es-toolkit'
import settingsDefault from '@/settings.default'
import { merge } from '@/utils/object'
import { cloneDeep } from 'es-toolkit'
const globalSettings: Settings.all = {
// 请在此处编写或粘贴配置代码
+1 -1
View File
@@ -1,9 +1,9 @@
import type { Menu, Route } from '#/global'
import type { RouteRecordRaw } from 'vue-router'
import { cloneDeep } from 'es-toolkit'
import apiApp from '@/api/modules/app'
import menu from '@/menu'
import { resolveRoutePath } from '@/utils'
import { cloneDeep } from 'es-toolkit'
import useRouteStore from './route'
import useSettingsStore from './settings'
import useUserStore from './user'
+2 -2
View File
@@ -1,9 +1,9 @@
import type { Route } from '#/global'
import type { RouteRecordRaw, RouterMatcher } from 'vue-router'
import apiApp from '@/api/modules/app'
import { systemRoutes as systemRoutesRaw } from '@/router/routes'
import { cloneDeep } from 'es-toolkit'
import { createRouterMatcher } from 'vue-router'
import apiApp from '@/api/modules/app'
import { systemRoutes as systemRoutesRaw } from '@/router/routes'
import useSettingsStore from './settings'
const useRouteStore = defineStore(
+1 -1
View File
@@ -1,8 +1,8 @@
import type { Settings } from '#/global'
import type { RouteMeta } from 'vue-router'
import { cloneDeep } from 'es-toolkit'
import settingsDefault from '@/settings'
import { merge } from '@/utils/object'
import { cloneDeep } from 'es-toolkit'
const useSettingsStore = defineStore(
// 唯一ID
+1 -1
View File
@@ -12,7 +12,7 @@ const useTabbarStore = defineStore(
const leaveIndex = ref(-1)
// 添加标签页
async function add(route: RouteLocationNormalized) {
function add(route: RouteLocationNormalized) {
const names: string[] = []
route.matched.forEach((v, i) => {
if (i > 0) {
+1 -1
View File
@@ -89,6 +89,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+2
View File
@@ -21,6 +21,7 @@ declare module 'vue' {
FaDrawer: typeof import('./../ui/components/FaDrawer/index.vue')['default']
FaDropdown: typeof import('./../ui/components/FaDropdown/index.vue')['default']
FaFixedActionBar: typeof import('./../ui/components/FaFixedActionBar/index.vue')['default']
FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
FaInput: typeof import('./../ui/components/FaInput/index.vue')['default']
FaKbd: typeof import('./../ui/components/FaKbd/index.vue')['default']
@@ -32,6 +33,7 @@ declare module 'vue' {
FaPasswordStrength: typeof import('./../ui/components/FaPasswordStrength/index.vue')['default']
FaPinInput: typeof import('./../ui/components/FaPinInput/index.vue')['default']
FaPopover: typeof import('./../ui/components/FaPopover/index.vue')['default']
FaProgress: typeof import('./../ui/components/FaProgress/index.vue')['default']
FaScrollArea: typeof import('./../ui/components/FaScrollArea/index.vue')['default']
FaSearchBar: typeof import('./../ui/components/FaSearchBar/index.vue')['default']
FaSelect: typeof import('./../ui/components/FaSelect/index.vue')['default']
+1 -1
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AvatarVariants } from '.'
import { cn } from '@/utils'
import { AvatarRoot } from 'reka-ui'
import { cn } from '@/utils'
import { avatarVariant } from '.'
const props = withDefaults(defineProps<{
+1 -1
View File
@@ -2,8 +2,8 @@
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { cn } from '@/utils'
import { Primitive } from 'reka-ui'
import { cn } from '@/utils'
import { buttonVariants } from '.'
interface Props extends PrimitiveProps {
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Check } from 'lucide-vue-next'
import {
ContextMenuCheckboxItem,
@@ -9,6 +8,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
ContextMenuContent,
ContextMenuPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuContentEmits>()
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
ContextMenuItem,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const emits = defineEmits<ContextMenuItemEmits>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ContextMenuLabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ContextMenuLabel } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Circle } from 'lucide-vue-next'
import {
ContextMenuItemIndicator,
@@ -9,6 +8,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuRadioItemEmits>()
@@ -1,11 +1,11 @@
<script setup lang="ts">
import type { ContextMenuSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
ContextMenuSeparator,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }>()
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
ContextMenuSubContent,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { ContextMenuSubTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ChevronRight } from 'lucide-vue-next'
import {
ContextMenuSubTrigger,
useForwardProps,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
@@ -2,7 +2,6 @@
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { SheetVariants } from '.'
import { cn } from '@/utils'
import { useScrollLock } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import {
@@ -12,6 +11,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
import { sheetVariants } from '.'
interface SheetContentProps extends DialogContentProps {
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DialogDescription } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DialogTitle } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Check } from 'lucide-vue-next'
import {
DropdownMenuCheckboxItem,
@@ -9,6 +8,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DropdownMenuLabel, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Circle } from 'lucide-vue-next'
import {
DropdownMenuItemIndicator,
@@ -9,6 +8,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
@@ -1,11 +1,11 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
DropdownMenuSeparator,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class']
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ChevronRight } from 'lucide-vue-next'
import {
DropdownMenuSubTrigger,
useForwardProps,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>()
+10 -3
View File
@@ -31,7 +31,8 @@ const props = defineProps<{
const slots = defineSlots<{
default?: () => VNode
label?: () => VNode
header?: () => VNode
footer?: () => VNode
}>()
const myItems = computed(() => {
@@ -55,9 +56,9 @@ function handleItemClick(item: { handle?: () => void }) {
<slot />
</DropdownMenuTrigger>
<DropdownMenuContent :align :align-offset :side :side-offset :collision-padding>
<template v-if="!!slots.label">
<template v-if="!!slots.header">
<DropdownMenuLabel>
<slot name="label" />
<slot name="header" />
</DropdownMenuLabel>
<DropdownMenuSeparator />
</template>
@@ -72,6 +73,12 @@ function handleItemClick(item: { handle?: () => void }) {
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="index !== myItems.length - 1" />
</template>
<template v-if="!!slots.footer">
<DropdownMenuSeparator />
<DropdownMenuLabel>
<slot name="footer" />
</DropdownMenuLabel>
</template>
</DropdownMenuContent>
</DropdownMenu>
</template>
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HoverCardRootEmits, HoverCardRootProps } from 'reka-ui'
import { HoverCardRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<HoverCardRootProps>()
const emits = defineEmits<HoverCardRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<HoverCardRoot v-bind="forwarded">
<slot />
</HoverCardRoot>
</template>
@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { HoverCardContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import {
HoverCardContent,
HoverCardPortal,
useForwardProps,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<HoverCardPortal>
<HoverCardContent
v-bind="forwardedProps"
:class="
cn(
'z-2000 min-w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</HoverCardContent>
</HoverCardPortal>
</template>
@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HoverCardTriggerProps } from 'reka-ui'
import { HoverCardTrigger } from 'reka-ui'
const props = defineProps<HoverCardTriggerProps>()
</script>
<template>
<HoverCardTrigger v-bind="props">
<slot />
</HoverCardTrigger>
</template>
@@ -0,0 +1,3 @@
export { default as HoverCard } from './HoverCard.vue'
export { default as HoverCardContent } from './HoverCardContent.vue'
export { default as HoverCardTrigger } from './HoverCardTrigger.vue'
+33
View File
@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HoverCardContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from './hover-card'
defineOptions({
name: 'FaHoverCard',
})
const props = defineProps<{
align?: HoverCardContentProps['align']
alignOffset?: HoverCardContentProps['alignOffset']
side?: HoverCardContentProps['side']
sideOffset?: HoverCardContentProps['sideOffset']
collisionPadding?: HoverCardContentProps['collisionPadding']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<HoverCard>
<HoverCardTrigger as-child>
<slot />
</HoverCardTrigger>
<HoverCardContent :align :align-offset :side :side-offset :collision-padding :class="props.class">
<slot name="card" />
</HoverCardContent>
</HoverCard>
</template>
+1 -1
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Icon } from '@iconify/vue'
import { UseImage } from '@vueuse/components'
import { cn } from '@/utils'
defineOptions({
name: 'FaIcon',
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { useVModel } from '@vueuse/core'
import { cn } from '@/utils'
const props = defineProps<{
defaultValue?: string | number
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { useScrollLock } from '@vueuse/core'
import { Maximize, Minimize, X } from 'lucide-vue-next'
import {
@@ -11,6 +10,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogContentProps & {
class?: HTMLAttributes['class']
@@ -91,7 +91,7 @@ const id = inject('ModalId')
>
<slot />
<div class="absolute inset-e-4 top-4 flex-center gap-2">
<button v-if="props.maximizable" class="rounded-sm bg-transparent opacity-70 ring-offset-background transition-opacity hidden disabled:pointer-events-none sm:inline-block data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ring" @click="handleMaximize">
<button v-if="props.maximizable" class="hidden rounded-sm bg-transparent opacity-70 ring-offset-background transition-opacity disabled:pointer-events-none sm:inline-block data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ring" @click="handleMaximize">
<Maximize v-if="!props.maximize" class="h-4 w-4" />
<Minimize v-else class="h-4 w-4" />
</button>
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DialogDescription, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { X } from 'lucide-vue-next'
import {
DialogClose,
@@ -11,6 +10,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { DialogTitle, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ModalEmits, ModalProps } from '.'
import { cn } from '@/utils'
import { VisuallyHidden } from 'reka-ui'
import { cn } from '@/utils'
import {
Dialog,
DialogContent,
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ToastRootEmits } from 'reka-ui'
import type { ToastProps } from '.'
import { cn } from '@/utils'
import { ToastRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
import { toastVariants } from '.'
const props = defineProps<ToastProps>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ToastActionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ToastAction } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ToastActionProps & { class?: HTMLAttributes['class'] }>()
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { ToastCloseProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { X } from 'lucide-vue-next'
import { ToastClose } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ToastCloseProps & {
class?: HTMLAttributes['class']
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ToastDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ToastDescription } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ToastDescriptionProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ToastTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ToastTitle } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ToastTitleProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ToastViewportProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ToastViewport } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<ToastViewportProps & { class?: HTMLAttributes['class'] }>()
+1 -1
View File
@@ -53,7 +53,7 @@ function handleCollaspe() {
}"
>
<slot />
<FaButton v-if="collaspe" variant="link" size="icon" class="absolute inset-b-0 inset-s-1/2 opacity-0 transition-all -translate-x-1/2 group-hover/pagemain-opacity-100" :class="{ 'rotate-x-180': !isCollaspe }" @click="handleCollaspe">
<FaButton v-if="collaspe" variant="link" size="icon" class="group-hover/pagemain-opacity-100 absolute inset-b-0 inset-s-1/2 opacity-0 transition-all -translate-x-1/2" :class="{ 'rotate-x-180': !isCollaspe }" @click="handleCollaspe">
<FaIcon name="i-ep:arrow-down" class="text-xl" />
</FaButton>
</div>
+1 -1
View File
@@ -39,7 +39,7 @@ const id = useId()
</script>
<template>
<PinInput :id :model-value="value" placeholder="○" otp type="number" @complete="handleComplete" @update:model-value="arrStr => value = arrStr.filter(Boolean)">
<PinInput :id :model-value="value" placeholder="○" otp @complete="handleComplete" @update:model-value="arrStr => value = arrStr.filter(Boolean)">
<PinInputGroup>
<PinInputInput v-for="(item, index) in length" :key="item" :index="index" />
</PinInputGroup>
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { PinInputRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = withDefaults(defineProps<PinInputRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: () => [],
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Primitive, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { PinInputInputProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { PinInputInput, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<PinInputInputProps & { class?: HTMLAttributes['class'] }>()
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
PopoverContent,
PopoverPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
defineOptions({
inheritAttrs: false,
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Progress } from './progress'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const modelValue = defineModel<number>({
default: 0,
})
</script>
<template>
<Progress v-bind="props" :model-value="modelValue" />
</template>
@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { ProgressRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import {
ProgressIndicator,
ProgressRoot,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(),
{
modelValue: 0,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ProgressRoot
v-bind="delegatedProps"
:class="
cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
props.class,
)
"
>
<ProgressIndicator
class="h-full w-full flex-1 bg-primary transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>
@@ -0,0 +1 @@
export { default as Progress } from './Progress.vue'
+10 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { useElementSize, useScroll } from '@vueuse/core'
import { cn } from '@/utils'
import { ScrollArea, ScrollBar } from './scroll-area'
defineOptions({
@@ -25,6 +25,10 @@ const props = withDefaults(
},
)
const emits = defineEmits<{
onScroll: [Event]
}>()
const scrollAreaRef = useTemplateRef('scrollAreaRef')
const arrivedState = ref<{
@@ -46,6 +50,10 @@ const showMaskEnd = computed(() => {
return !arrivedState.value?.bottom
})
function onScroll(event: Event) {
emits('onScroll', event)
}
function onWheel(event: WheelEvent) {
if (props.horizontal) {
scrollAreaRef.value?.el?.viewportElement?.scrollBy({
@@ -108,7 +116,7 @@ defineExpose({
'--mask-scroll-container-gradient-color': props.gradientColor,
} : {}"
>
<ScrollArea ref="scrollAreaRef" :class="cn('relative z-0 flex-1', props.contentClass)" :scrollbar="props.scrollbar" :on-wheel="onWheel">
<ScrollArea ref="scrollAreaRef" :class="cn('relative z-0 flex-1', props.contentClass)" :scrollbar="props.scrollbar" :on-scroll="onScroll" :on-wheel="onWheel">
<slot />
<ScrollBar v-if="props.horizontal" orientation="horizontal" :class="{ 'opacity-0 pointer-events-none': !props.scrollbar }" />
</ScrollArea>
@@ -1,18 +1,19 @@
<script setup lang="ts">
import type { ScrollAreaRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
import ScrollBar from './ScrollBar.vue'
const props = defineProps<ScrollAreaRootProps & {
class?: HTMLAttributes['class']
scrollbar?: boolean
onScroll?: (event: Event) => void
onWheel?: (event: WheelEvent) => void
}>()
@@ -31,7 +32,7 @@ defineExpose({
<template>
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
<ScrollAreaViewport ref="viewportRef" class="scroll-area-viewport h-full w-full rounded-[inherit]" @wheel.prevent="onWheel">
<ScrollAreaViewport ref="viewportRef" class="scroll-area-viewport h-full w-full overscroll-contain rounded-[inherit]" @scroll="onScroll" @wheel="onWheel">
<slot />
</ScrollAreaViewport>
<ScrollBar :class="{ 'opacity-0 pointer-events-none': !props.scrollbar }" />
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'vertical',
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
SelectContent,
SelectPortal,
@@ -9,6 +8,7 @@ import {
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
defineOptions({
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectGroupProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { SelectGroup } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { SelectItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { Check } from 'lucide-vue-next'
import {
SelectItem,
@@ -10,6 +9,7 @@ import {
useForwardProps,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectLabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { SelectLabel } from 'reka-ui'
import { cn } from '@/utils'
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
</script>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ChevronDown } from 'lucide-vue-next'
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ChevronUp } from 'lucide-vue-next'
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { SelectSeparator } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { SelectTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { ChevronDown } from 'lucide-vue-next'
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
+1 -1
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SliderRootEmits, SliderRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SliderRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SliderRootEmits>()
+1 -1
View File
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { SwitchRootEmits, SwitchRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import {
SwitchRoot,
SwitchThumb,
useForwardPropsEmits,
} from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { TabsContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { TabsContent } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
+1 -1
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { TabsListProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { TabsList } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { TabsTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { TabsTrigger, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '@/utils'
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()

Some files were not shown because too many files have changed in this diff Show More