mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 09:13:30 +08:00
Compare commits
4
Commits
v5.0.0-beta.1
...
v5.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75411af22f | ||
|
|
2909caaf0e | ||
|
|
83b87a5f3f | ||
|
|
cfd23d54da |
+1
-1
@@ -1,7 +1,7 @@
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = false
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 页面标题
|
||||
VITE_APP_TITLE = Fantastic-admin 基础版
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = false
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 页面标题(test)
|
||||
VITE_APP_TITLE = Fantastic-admin 基础版
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
engine-strict=true
|
||||
verify-deps-before-run=prompt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.png" width="200" height="200" />
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Fantastic-admin</h1>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="stylesheet" href="/browser_upgrade/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Expires" content="0">
|
||||
|
||||
+32
-35
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -17,37 +17,33 @@
|
||||
"lint:tsc": "vue-tsc -b",
|
||||
"lint:eslint": "eslint . --cache --fix",
|
||||
"lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix",
|
||||
"postinstall": "simple-git-hooks",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"postinstall": "simple-git-hooks",
|
||||
"commit": "git cz",
|
||||
"release": "bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@imengyu/vue3-context-menu": "^1.4.4",
|
||||
"@vee-validate/zod": "^4.15.0",
|
||||
"@vueuse/components": "^12.3.0",
|
||||
"@vueuse/core": "^12.3.0",
|
||||
"@vueuse/integrations": "^12.3.0",
|
||||
"@vueuse/components": "^12.5.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"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.2",
|
||||
"element-plus": "^2.9.3",
|
||||
"eruda": "^3.4.1",
|
||||
"es-toolkit": "^1.31.0",
|
||||
"hotkeys-js": "^3.13.9",
|
||||
"lucide-vue-next": "^0.469.0",
|
||||
"lucide-vue-next": "^0.474.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"overlayscrollbars": "^2.10.1",
|
||||
"overlayscrollbars-vue": "^0.5.9",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"pinia": "^2.3.0",
|
||||
"qs": "^6.13.1",
|
||||
"pinia": "^2.3.1",
|
||||
"qs": "^6.14.0",
|
||||
"radix-vue": "^1.9.12",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -60,58 +56,59 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "3.12.1",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@iconify/json": "^2.2.291",
|
||||
"@antfu/eslint-config": "3.16.0",
|
||||
"@faker-js/faker": "^9.4.0",
|
||||
"@iconify/json": "^2.2.299",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@stylistic/stylelint-config": "^2.0.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qs": "^6.9.17",
|
||||
"@unocss/eslint-plugin": "^0.65.3",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@unocss/eslint-plugin": "^65.4.3",
|
||||
"@unocss/preset-legacy-compat": "^65.4.3",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"boxen": "^8.0.1",
|
||||
"bumpp": "^9.9.2",
|
||||
"bumpp": "^9.10.2",
|
||||
"cz-git": "^1.11.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint": "^9.18.0",
|
||||
"esno": "^4.8.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"inquirer": "^12.3.0",
|
||||
"lint-staged": "^15.3.0",
|
||||
"inquirer": "^12.3.2",
|
||||
"lint-staged": "^15.4.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"sass-embedded": "^1.83.1",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-recess-order": "^5.1.1",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-scss": "^6.10.0",
|
||||
"stylelint-scss": "^6.10.1",
|
||||
"svgo": "^3.3.2",
|
||||
"typescript": "^5.7.2",
|
||||
"unocss": "^0.65.3",
|
||||
"unocss-preset-animations": "^1.1.0",
|
||||
"typescript": "^5.7.3",
|
||||
"unocss": "^65.4.3",
|
||||
"unocss-preset-animations": "^1.1.1",
|
||||
"unocss-preset-scrollbar": "^0.3.1",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-auto-import": "^19.0.0",
|
||||
"unplugin-turbo-console": "^1.11.1",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"vite": "^6.0.7",
|
||||
"unplugin-vue-components": "^28.0.0",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-app-loading": "^0.3.0",
|
||||
"vite-plugin-archiver": "^0.1.1",
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite-plugin-env-parse": "^1.0.15",
|
||||
"vite-plugin-fake-server": "^2.1.5",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-pages": "^0.32.4",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
"vite-plugin-vue-devtools": "^7.7.1",
|
||||
"vite-plugin-vue-meta-layouts": "^0.5.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
|
||||
Generated
+1321
-1138
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -21 206 206" xml:space="preserve">
|
||||
<path d="M0,82.57C0,58.4,0,34.23,0,10.06C0,1.35,1.35,0,10.03,0C45.36,0,80.7-0.01,116.03,0.03c1.77,0,3.59,0.26,5.29,0.74
|
||||
c3.07,0.87,3.57,3.21,2.45,5.8c-4.37,10.1-8.83,20.15-13.31,30.2c-1.44,3.23-4.06,4.26-7.54,4.25c-18.5-0.09-37,0.1-55.5-0.14
|
||||
c-4.95-0.06-6.49,2.12-6.46,6.47c0.02,3,0.11,6.01-0.02,9c-0.3,6.8,0.34,9.29,9.51,9.62c5.36,0.19,10.73,0.04,16.09,0.04
|
||||
c7.57,0,15.15-0.04,22.72,0.01c5.13,0.03,6.6,2.12,4.59,6.89C90.43,81.09,86.27,89,83.4,97.35c-2.59,7.55-8.57,7.27-14.28,7.56
|
||||
c-7.48,0.38-15,0.17-22.5,0.06c-3.69-0.05-4.96,1.04-4.86,5.18c0.36,15.83,0.26,31.67,0.12,47.5c-0.04,4.2-2.26,6.53-6.94,6.42
|
||||
c-9-0.21-18-0.06-27-0.06c-6.05,0-7.91-1.8-7.92-7.94C-0.02,131.57,0,107.07,0,82.57z" fill="#42b883" />
|
||||
<path d="M132.98,94.13c-8.21,18.88-16.33,37.1-24.06,55.48c-6.02,14.31-5.81,14.4-21.31,14.4c-6.67,0-13.33,0.05-20-0.01
|
||||
c-5.91-0.05-7.71-2.44-5.46-7.61c6.86-15.8,13.86-31.55,20.82-47.31c9.41-21.32,18.81-42.63,28.24-63.94
|
||||
c5.59-12.62,11.14-25.25,16.94-37.77c0.82-1.78,2.95-4.09,4.47-4.09c1.63,0,4,2.11,4.78,3.86c13.4,29.78,26.56,59.66,39.87,89.49
|
||||
c6.48,14.53,13.16,28.97,19.72,43.46c2.57,5.68,5.11,11.38,7.61,17.1c1.61,3.68-0.13,6.75-4.12,6.77
|
||||
c-23.83,0.08-47.66,0.09-71.49-0.01c-4.03-0.02-5.46-2.47-3.98-6.17c2.26-5.63,4.77-11.16,6.95-16.82c1.74-4.54,4.92-6.06,9.6-5.98
|
||||
c9.65,0.16,9.89-0.55,5.89-9.13c-4.57-9.81-8.76-19.8-13.14-29.7C134.08,95.62,133.68,95.17,132.98,94.13z" fill="#42b883" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { ua } from '@/utils/ua'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Provider from './ui/provider/index.vue'
|
||||
import eventBus from './utils/eventBus'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -40,9 +38,6 @@ onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
})
|
||||
hotkeys('alt+i', () => {
|
||||
eventBus.emit('global-system-info-toggle')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
// 修改密码
|
||||
passwordEdit: (data: {
|
||||
password: string
|
||||
newpassword: string
|
||||
newPassword: string
|
||||
}) => api.post('user/password/edit', data, {
|
||||
baseURL: '/mock/',
|
||||
}),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-25 -46 256 256" xml:space="preserve">
|
||||
<path d="M0,82.57C0,58.4,0,34.23,0,10.06C0,1.35,1.35,0,10.03,0C45.36,0,80.7-0.01,116.03,0.03c1.77,0,3.59,0.26,5.29,0.74
|
||||
c3.07,0.87,3.57,3.21,2.45,5.8c-4.37,10.1-8.83,20.15-13.31,30.2c-1.44,3.23-4.06,4.26-7.54,4.25c-18.5-0.09-37,0.1-55.5-0.14
|
||||
c-4.95-0.06-6.49,2.12-6.46,6.47c0.02,3,0.11,6.01-0.02,9c-0.3,6.8,0.34,9.29,9.51,9.62c5.36,0.19,10.73,0.04,16.09,0.04
|
||||
c7.57,0,15.15-0.04,22.72,0.01c5.13,0.03,6.6,2.12,4.59,6.89C90.43,81.09,86.27,89,83.4,97.35c-2.59,7.55-8.57,7.27-14.28,7.56
|
||||
c-7.48,0.38-15,0.17-22.5,0.06c-3.69-0.05-4.96,1.04-4.86,5.18c0.36,15.83,0.26,31.67,0.12,47.5c-0.04,4.2-2.26,6.53-6.94,6.42
|
||||
c-9-0.21-18-0.06-27-0.06c-6.05,0-7.91-1.8-7.92-7.94C-0.02,131.57,0,107.07,0,82.57z" fill="#35495e" />
|
||||
<path d="M132.98,94.13c-8.21,18.88-16.33,37.1-24.06,55.48c-6.02,14.31-5.81,14.4-21.31,14.4c-6.67,0-13.33,0.05-20-0.01
|
||||
c-5.91-0.05-7.71-2.44-5.46-7.61c6.86-15.8,13.86-31.55,20.82-47.31c9.41-21.32,18.81-42.63,28.24-63.94
|
||||
c5.59-12.62,11.14-25.25,16.94-37.77c0.82-1.78,2.95-4.09,4.47-4.09c1.63,0,4,2.11,4.78,3.86c13.4,29.78,26.56,59.66,39.87,89.49
|
||||
c6.48,14.53,13.16,28.97,19.72,43.46c2.57,5.68,5.11,11.38,7.61,17.1c1.61,3.68-0.13,6.75-4.12,6.77
|
||||
c-23.83,0.08-47.66,0.09-71.49-0.01c-4.03-0.02-5.46-2.47-3.98-6.17c2.26-5.63,4.77-11.16,6.95-16.82c1.74-4.54,4.92-6.06,9.6-5.98
|
||||
c9.65,0.16,9.89-0.55,5.89-9.13c-4.57-9.81-8.76-19.8-13.14-29.7C134.08,95.62,133.68,95.17,132.98,94.13z" fill="#42b883" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -21,7 +21,7 @@
|
||||
/* 明暗模式 CSS 变量 */
|
||||
/* stylelint-disable-next-line no-duplicate-selectors */
|
||||
:root {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
color-scheme: light;
|
||||
|
||||
&::view-transition-old(root),
|
||||
&::view-transition-new(root) {
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 72%);
|
||||
color-scheme: dark;
|
||||
|
||||
&::view-transition-old(root) {
|
||||
z-index: 9999;
|
||||
@@ -50,29 +50,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
body:not([data-os="macOS"]) {
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--primary) / 30%);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--primary) / 50%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
scrollbar-width: thin;
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
@@ -88,10 +68,6 @@ body {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--background));
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -102,14 +78,3 @@ body {
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Overrides OverlayScrollbars */
|
||||
[data-overlayscrollbars-contents] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.os-scrollbar {
|
||||
--os-handle-bg: hsl(var(--primary) / 30%);
|
||||
--os-handle-bg-hover: hsl(var(--primary) / 40%);
|
||||
--os-handle-bg-active: hsl(var(--primary) / 50%);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
z-index: 3000;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: hsl(var(--primary));
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<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'
|
||||
|
||||
defineOptions({
|
||||
name: 'EditPasswordForm',
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(
|
||||
z.object({
|
||||
password: z.string().min(1, '请输入原密码'),
|
||||
newPassword: z.string().min(1, '请输入新密码').min(6, '密码长度为6到18位').max(18, '密码长度为6到18位'),
|
||||
checkPassword: z.string().min(1, '请确认新密码'),
|
||||
}).refine(data => data.newPassword === data.checkPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
path: ['checkPassword'],
|
||||
}),
|
||||
),
|
||||
initialValues: {
|
||||
password: '',
|
||||
newPassword: '',
|
||||
checkPassword: '',
|
||||
},
|
||||
})
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
loading.value = true
|
||||
userStore.editPassword(values).then(async () => {
|
||||
toast.success('模拟修改成功,请重新登录')
|
||||
userStore.logout()
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex-col-stretch-center">
|
||||
<div class="mb-6 space-y-2">
|
||||
<h3 class="text-4xl color-[var(--el-text-color-primary)] font-bold">
|
||||
修改密码
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground lg:text-base">
|
||||
请输入原密码、新密码和确认密码
|
||||
</p>
|
||||
</div>
|
||||
<form @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入原密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField, errors }" name="newPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField, errors }" name="checkPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请确认新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FaButton :loading="loading" size="lg" class="mt-8 w-full" type="submit">
|
||||
保存
|
||||
</FaButton>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,7 +49,7 @@ function handleCopy() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块">
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块" :footer="isSupported">
|
||||
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
|
||||
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
|
||||
</div>
|
||||
@@ -350,7 +350,7 @@ function handleCopy() {
|
||||
<FaSwitch v-model="settingsStore.settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<template #footer>
|
||||
<FaButton class="w-full" @click="handleCopy">
|
||||
<FaIcon name="i-ep:document-copy" />
|
||||
复制配置
|
||||
|
||||
@@ -62,7 +62,7 @@ function handleBackToTop() {
|
||||
<Teleport to="body">
|
||||
<Transition v-bind="transitionClass">
|
||||
<FaButton v-if="scrollTop >= 200" variant="outline" size="icon" class="fixed inset-b-4 z-1000 h-12 w-12 rounded-full transition-all -inset-e-9" :class="{ 'inset-e-3!': show }" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" @click="handleBackToTop">
|
||||
<FaIcon name="i-icon-park-outline:to-top-one" :size="24" />
|
||||
<FaIcon name="i-icon-park-outline:to-top-one" class="size-6" />
|
||||
</FaButton>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
@@ -11,7 +11,7 @@ const settingsStore = useSettingsStore()
|
||||
<template>
|
||||
<footer v-if="settingsStore.settings.copyright.enable" class="copyright">
|
||||
<span>Copyright</span>
|
||||
<FaIcon name="i-ri:copyright-line" :size="18" />
|
||||
<FaIcon name="i-ri:copyright-line" class="size-5" />
|
||||
<span v-if="settingsStore.settings.copyright.dates">{{ settingsStore.settings.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.settings.copyright.company">
|
||||
<a v-if="settingsStore.settings.copyright.website" :href="settingsStore.settings.copyright.website" target="_blank" rel="noopener">{{ settingsStore.settings.copyright.company }}</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
@@ -17,9 +18,11 @@ const { switchTo } = useMenu()
|
||||
<template>
|
||||
<Transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.mode === 'head'">
|
||||
<component :is="useSlots('header-start')" />
|
||||
<div class="header-container">
|
||||
<Logo class="title" />
|
||||
<FaMaskScrollContainer scroll="x" gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1">
|
||||
<component :is="useSlots('header-after-logo')" />
|
||||
<FaScrollArea horizontal :scrollbar="false" mask gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1">
|
||||
<!-- 顶部模式 -->
|
||||
<div class="menu h-full flex of-hidden transition-all">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
@@ -43,9 +46,10 @@ const { switchTo } = useMenu()
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FaMaskScrollContainer>
|
||||
</FaScrollArea>
|
||||
<ToolbarRightSide />
|
||||
</div>
|
||||
<component :is="useSlots('header-end')" />
|
||||
</header>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -54,14 +58,13 @@ const { switchTo } = useMenu()
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
right: var(--scrollbar-width, 0);
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
width: calc(100% - var(--scrollbar-width, 0px));
|
||||
height: var(--g-header-height);
|
||||
padding: 0 20px;
|
||||
margin: 0 auto;
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
@@ -75,6 +78,7 @@ header {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px 0 20px;
|
||||
margin: 0 auto;
|
||||
|
||||
:deep(a.title) {
|
||||
@@ -82,7 +86,7 @@ header {
|
||||
flex: 0;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
padding: inherit;
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
|
||||
.logo {
|
||||
|
||||
@@ -28,15 +28,15 @@ onMounted(() => {
|
||||
<ul class="list-none ps-2 pt-2 text-sm">
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌘' : 'Ctrl' }}</FaKbd>
|
||||
<FaKbd>I</FaKbd>
|
||||
</div>
|
||||
查看系统信息
|
||||
</li>
|
||||
<li v-if="settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys" class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>S</FaKbd>
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌘' : 'Ctrl' }}</FaKbd>
|
||||
<FaKbd>K</FaKbd>
|
||||
</div>
|
||||
唤起导航搜索
|
||||
</li>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import imgLogo from '@/assets/images/logo.png'
|
||||
import imgLogo from '@/assets/images/logo.svg'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
@@ -16,8 +17,10 @@ const { switchTo } = useMenu()
|
||||
<template>
|
||||
<Transition name="main-sidebar">
|
||||
<div v-if="settingsStore.settings.menu.mode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.settings.menu.mode !== 'single')" class="main-sidebar-container">
|
||||
<component :is="useSlots('main-sidebar-top')" />
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<FaMaskScrollContainer gradient-color="var(--g-main-sidebar-bg)" class="menu">
|
||||
<component :is="useSlots('main-sidebar-after-logo')" />
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-main-sidebar-bg)" class="menu">
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="w-full flex flex-col of-hidden py-1 transition-all -mt-2">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
@@ -41,7 +44,8 @@ const { switchTo } = useMenu()
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FaMaskScrollContainer>
|
||||
</FaScrollArea>
|
||||
<component :is="useSlots('main-sidebar-bottom')" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
@@ -74,7 +74,7 @@ defineExpose({
|
||||
'w-full': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'vertical',
|
||||
}" :style="indentStyle"
|
||||
>
|
||||
<FaIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" :size="18" class="menu-item-container-icon transition-transform group-hover-scale-120" />
|
||||
<FaIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" class="menu-item-container-icon size-5 transition-transform group-hover-scale-120" />
|
||||
<span
|
||||
v-if="!(rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName)" class="w-0 flex-1 truncate text-sm transition-height transition-opacity transition-width"
|
||||
:class="{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-vue'
|
||||
import type { SubMenuProps } from './types'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue'
|
||||
import Item from './item.vue'
|
||||
import { rootMenuInjectionKey } from './types'
|
||||
|
||||
@@ -19,7 +17,7 @@ const props = withDefaults(
|
||||
|
||||
const index = props.menu.path ?? JSON.stringify(props.menu)
|
||||
const itemRef = useTemplateRef('itemRef')
|
||||
const subMenuRef = useTemplateRef<OverlayScrollbarsComponentRef>('subMenuRef')
|
||||
const subMenuRef = useTemplateRef('subMenuRef')
|
||||
const rootMenu = inject(rootMenuInjectionKey)!
|
||||
|
||||
const opened = computed(() => {
|
||||
@@ -141,7 +139,8 @@ function handleMouseenter() {
|
||||
rootMenu.openMenu(index, props.uniqueKey)
|
||||
nextTick(() => {
|
||||
const el = itemRef.value?.ref
|
||||
if (!el) {
|
||||
const subMenuEl = subMenuRef.value?.$el
|
||||
if (!el || !subMenuEl) {
|
||||
return
|
||||
}
|
||||
let top = 0
|
||||
@@ -149,22 +148,22 @@ function handleMouseenter() {
|
||||
if (rootMenu.props.mode === 'vertical' || props.level !== 0) {
|
||||
top = el.getBoundingClientRect().top + el.scrollTop
|
||||
left = el.getBoundingClientRect().left + el.getBoundingClientRect().width
|
||||
if (top + subMenuRef.value!.getElement()!.offsetHeight > window.innerHeight) {
|
||||
top = window.innerHeight - subMenuRef.value!.getElement()!.offsetHeight
|
||||
if (top + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
top = window.innerHeight - subMenuEl.offsetHeight
|
||||
}
|
||||
}
|
||||
else {
|
||||
top = el.getBoundingClientRect().top + el.getBoundingClientRect().height
|
||||
left = el.getBoundingClientRect().left
|
||||
if (top + subMenuRef.value!.getElement()!.offsetHeight > window.innerHeight) {
|
||||
subMenuRef.value!.getElement()!.style.height = `${window.innerHeight - top}px`
|
||||
if (top + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
subMenuEl.style.height = `${window.innerHeight - top}px`
|
||||
}
|
||||
}
|
||||
if (left + subMenuRef.value!.getElement()!.offsetWidth > document.documentElement.clientWidth) {
|
||||
if (left + subMenuEl.offsetWidth > document.documentElement.clientWidth) {
|
||||
left = el.getBoundingClientRect().left - el.getBoundingClientRect().width
|
||||
}
|
||||
subMenuRef.value!.getElement()!.style.top = `${top}px`
|
||||
subMenuRef.value!.getElement()!.style.left = `${left}px`
|
||||
subMenuEl.style.top = `${top}px`
|
||||
subMenuEl.style.insetInlineStart = `${left}px`
|
||||
})
|
||||
}
|
||||
else {
|
||||
@@ -197,8 +196,8 @@ function handleMouseleave() {
|
||||
<Item ref="itemRef" :unique-key="uniqueKey" :item="menu" :level="level" :sub-menu="hasChildren" :expand="opened" @click="handleClick" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" />
|
||||
<Teleport v-if="hasChildren" to="body" :disabled="!rootMenu.isMenuPopup">
|
||||
<Transition v-bind="transitionClass" v-on="transitionEvent">
|
||||
<OverlayScrollbarsComponent
|
||||
v-if="opened" ref="subMenuRef" :options="{ scrollbars: { visibility: 'hidden' } }" defer class="sub-menu static rounded-lg" :class="{
|
||||
<FaScrollArea
|
||||
v-if="opened" ref="subMenuRef" :scrollbar="false" :mask="rootMenu.isMenuPopup" class="sub-menu static rounded-lg" :class="{
|
||||
'bg-[var(--g-sub-sidebar-bg)]': rootMenu.isMenuPopup,
|
||||
'border shadow-xl fixed! z-3000 w-[200px]': rootMenu.isMenuPopup,
|
||||
'mx-1': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
|
||||
@@ -208,7 +207,7 @@ function handleMouseleave() {
|
||||
<template v-for="item in menu.children" :key="item.path ?? JSON.stringify(item)">
|
||||
<SubMenu v-if="item.meta?.menu !== false" :unique-key="[...uniqueKey, item.path ?? JSON.stringify(item)]" :menu="item" :level="level + 1" />
|
||||
</template>
|
||||
</OverlayScrollbarsComponent>
|
||||
</FaScrollArea>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
@@ -47,12 +48,14 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
'is-collapse': settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse,
|
||||
}"
|
||||
>
|
||||
<component :is="useSlots('sub-sidebar-top')" />
|
||||
<Logo
|
||||
v-if="['side', 'single'].includes(settingsStore.settings.menu.mode)" :show-logo="settingsStore.settings.menu.mode === 'single'" class="sidebar-logo" :class="{
|
||||
'sidebar-logo-bg': settingsStore.settings.menu.mode === 'single',
|
||||
single: settingsStore.settings.menu.mode === 'single',
|
||||
}"
|
||||
/>
|
||||
<FaMaskScrollContainer gradient-color="var(--g-sub-sidebar-bg)" class="flex-1">
|
||||
<component :is="useSlots('sub-sidebar-after-logo')" />
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-sub-sidebar-bg)" class="flex-1">
|
||||
<TransitionGroup :name="transitionName">
|
||||
<template v-for="(mainItem, mainIndex) in menuStore.allMenus" :key="mainIndex">
|
||||
<div v-show="mainIndex === menuStore.actived">
|
||||
@@ -64,12 +67,13 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
</div>
|
||||
</template>
|
||||
</TransitionGroup>
|
||||
</FaMaskScrollContainer>
|
||||
</FaScrollArea>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="relative flex items-center px-4 py-3" :class="[settingsStore.settings.menu.subMenuCollapse ? 'justify-center' : 'justify-end']">
|
||||
<FaButton v-show="settingsStore.settings.menu.enableSubMenuCollapseButton" variant="secondary" size="icon" class="h-8 w-8 transition" :class="{ '-rotate-z-180': settingsStore.settings.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<FaIcon name="toolbar-collapse" :size="16" />
|
||||
<FaIcon name="toolbar-collapse" class="size-4" />
|
||||
</FaButton>
|
||||
</div>
|
||||
<component :is="useSlots('sub-sidebar-bottom')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,7 +94,7 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
width: var(--g-sub-sidebar-collapse-width);
|
||||
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
&:not(.single) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -100,19 +104,6 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.sidebar-logo-bg {
|
||||
background-color: var(--g-sub-sidebar-logo-bg);
|
||||
|
||||
:deep(span) {
|
||||
color: var(--g-sub-sidebar-logo-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,12 +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 ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { toast } from 'vue-sonner'
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
|
||||
defineOptions({
|
||||
name: 'Tabbar',
|
||||
@@ -25,7 +24,7 @@ const keys = useMagicKeys({ reactive: true })
|
||||
|
||||
const activedTabId = computed(() => tabbar.getId())
|
||||
|
||||
const tabsRef = ref()
|
||||
const tabsRef = useTemplateRef('tabsRef')
|
||||
const tabContainerRef = useTemplateRef('tabContainerRef')
|
||||
const tabRef = useTemplateRef<HTMLElement[]>('tabRef')
|
||||
|
||||
@@ -34,7 +33,7 @@ watch(() => route, (val) => {
|
||||
tabbarStore.add(val).then(() => {
|
||||
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.clientWidth * 0.4)
|
||||
tabRef.value && tabsRef.value?.scrollTo(tabRef.value[index].offsetLeft - tabsRef.value.ref?.$el.clientWidth * 0.4)
|
||||
tabbarScrollTip()
|
||||
}
|
||||
})
|
||||
@@ -44,7 +43,7 @@ watch(() => route, (val) => {
|
||||
deep: true,
|
||||
})
|
||||
function tabbarScrollTip() {
|
||||
if (tabContainerRef.value?.$el.clientWidth > (tabsRef.value.ref.clientWidth ?? 0) && localStorage.getItem('tabbarScrollTip') === undefined) {
|
||||
if (tabContainerRef.value?.$el.clientWidth > (tabsRef.value?.ref?.$el.clientWidth ?? 0) && localStorage.getItem('tabbarScrollTip') === undefined) {
|
||||
localStorage.setItem('tabbarScrollTip', '')
|
||||
const tips = toast.info('温馨提示', {
|
||||
description: '标签栏数量超过展示区域范围,可以将鼠标移到标签栏上,通过鼠标滚轮滑动浏览',
|
||||
@@ -57,53 +56,40 @@ function tabbarScrollTip() {
|
||||
})
|
||||
}
|
||||
}
|
||||
function onTabbarContextmenu(event: MouseEvent, routeItem: Tabbar.recordRaw) {
|
||||
event.preventDefault()
|
||||
ContextMenu.showContextMenu({
|
||||
x: event.x,
|
||||
y: event.y,
|
||||
zIndex: 1050,
|
||||
iconFontClass: '',
|
||||
customClass: 'tabbar-contextmenu',
|
||||
items: [
|
||||
function contextMenuItems(routeItem: Tabbar.recordRaw) {
|
||||
return [
|
||||
[
|
||||
{
|
||||
label: '重新加载',
|
||||
icon: 'i-ri:refresh-line',
|
||||
disabled: routeItem.tabId !== activedTabId.value,
|
||||
onClick: () => mainPage.reload(),
|
||||
handle: () => mainPage.reload(),
|
||||
},
|
||||
{
|
||||
label: '关闭标签页',
|
||||
icon: 'i-ri:close-line',
|
||||
disabled: tabbarStore.list.length <= 1,
|
||||
divided: true,
|
||||
onClick: () => {
|
||||
tabbar.closeById(routeItem.tabId)
|
||||
},
|
||||
handle: () => tabbar.closeById(routeItem.tabId),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: '关闭其他标签页',
|
||||
disabled: !tabbar.checkCloseOtherSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeOtherSide(routeItem.tabId)
|
||||
},
|
||||
handle: () => tabbar.closeOtherSide(routeItem.tabId),
|
||||
},
|
||||
{
|
||||
label: '关闭左侧标签页',
|
||||
disabled: !tabbar.checkCloseLeftSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeLeftSide(routeItem.tabId)
|
||||
},
|
||||
handle: () => tabbar.closeLeftSide(routeItem.tabId),
|
||||
},
|
||||
{
|
||||
label: '关闭右侧标签页',
|
||||
disabled: !tabbar.checkCloseRightSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeRightSide(routeItem.tabId)
|
||||
},
|
||||
handle: () => tabbar.closeRightSide(routeItem.tabId),
|
||||
},
|
||||
],
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -158,85 +144,58 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tabbar-container">
|
||||
<FaMaskScrollContainer ref="tabsRef" scroll="x" 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"
|
||||
ref="tabRef" :data-index="index" class="tab" :class="{
|
||||
actived: element.tabId === activedTabId,
|
||||
}" @click="router.push(element.fullPath)" @contextmenu="onTabbarContextmenu($event, element)"
|
||||
>
|
||||
<div class="tab-dividers" />
|
||||
<div class="tab-background" />
|
||||
<FaTooltip :delay="1000" side="bottom">
|
||||
<div class="tab-content">
|
||||
<div :key="element.tabId" class="title">
|
||||
<FaIcon v-if="settingsStore.settings.tabbar.enableIcon && element.icon" :name="element.icon" class="icon" />
|
||||
{{ typeof element?.title === 'function' ? element.title() : element.title }}
|
||||
<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">
|
||||
<TransitionGroup ref="tabContainerRef" name="tabbar" tag="div" class="tab-container">
|
||||
<div
|
||||
v-for="(element, index) in tabbarStore.list" :key="element.tabId"
|
||||
ref="tabRef" :data-index="index" class="tab" :class="{
|
||||
actived: element.tabId === activedTabId,
|
||||
}" @click="router.push(element.fullPath)"
|
||||
>
|
||||
<FaContextMenu :items="contextMenuItems(element)">
|
||||
<div class="size-full">
|
||||
<div class="tab-dividers" />
|
||||
<div class="tab-background" />
|
||||
<FaTooltip :delay="1000" side="bottom">
|
||||
<div class="tab-content">
|
||||
<div :key="element.tabId" class="title">
|
||||
<FaIcon v-if="settingsStore.settings.tabbar.enableIcon && element.icon" :name="element.icon" class="icon" />
|
||||
{{ typeof element?.title === 'function' ? element.title() : element.title }}
|
||||
</div>
|
||||
<div v-if="tabbarStore.list.length > 1" class="action-icon" @click.stop="tabbar.closeById(element.tabId)">
|
||||
<FaIcon name="i-ri:close-fill" />
|
||||
</div>
|
||||
<div v-show="keys.alt && index < 9" class="hotkey-number">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="text-sm">
|
||||
{{ typeof element?.title === 'function' ? element.title() : element.title }}
|
||||
</div>
|
||||
<div class="text-accent-foreground/50">
|
||||
{{ element.fullPath }}
|
||||
</div>
|
||||
</template>
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<div v-if="tabbarStore.list.length > 1" class="action-icon" @click.stop="tabbar.closeById(element.tabId)">
|
||||
<FaIcon name="i-ri:close-fill" />
|
||||
</div>
|
||||
<div v-show="keys.alt && index < 9" class="hotkey-number">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="text-sm">
|
||||
{{ typeof element?.title === 'function' ? element.title() : element.title }}
|
||||
</div>
|
||||
<div class="text-accent-foreground/50">
|
||||
{{ element.fullPath }}
|
||||
</div>
|
||||
</template>
|
||||
</FaTooltip>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</FaMaskScrollContainer>
|
||||
</FaContextMenu>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</FaScrollArea>
|
||||
</div>
|
||||
<component :is="useSlots('tabbar-end')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tabbar-contextmenu {
|
||||
--uno: fixed ring-1 ring-border shadow-2xl;
|
||||
|
||||
background-color: hsl(var(--popover));
|
||||
|
||||
.mx-context-menu-items .mx-context-menu-item {
|
||||
--uno: transition-background-color;
|
||||
|
||||
&:not(.disabled):hover {
|
||||
--uno: cursor-pointer bg-accent;
|
||||
}
|
||||
|
||||
span {
|
||||
color: initial;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: initial;
|
||||
}
|
||||
|
||||
&.disabled span,
|
||||
&.disabled .icon {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-context-menu-item-sperator {
|
||||
background-color: hsl(var(--popover));
|
||||
|
||||
&::after {
|
||||
--uno: bg-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.tabbar-container {
|
||||
.tabbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--g-tabbar-height);
|
||||
background-color: var(--g-tabbar-bg);
|
||||
transition: background-color 0.3s, box-shadow 0.3s;
|
||||
@@ -245,158 +204,163 @@ onUnmounted(() => {
|
||||
box-shadow: 0 1px 0 0 hsl(var(--border)), 0 -1px 0 0 hsl(var(--border));
|
||||
}
|
||||
|
||||
.tabs {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
margin-inline-end: 50px;
|
||||
white-space: nowrap;
|
||||
.tabbar-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.tab-container {
|
||||
display: inline-block;
|
||||
.tabs {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
.tab-container {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: var(--g-tabbar-height);
|
||||
font-size: 14px;
|
||||
line-height: calc(var(--g-tabbar-height) - 2px);
|
||||
vertical-align: bottom;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.actived):hover {
|
||||
z-index: 3;
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: var(--g-tabbar-height);
|
||||
font-size: 14px;
|
||||
line-height: calc(var(--g-tabbar-height) - 2px);
|
||||
vertical-align: bottom;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
&:not(.actived):hover {
|
||||
z-index: 3;
|
||||
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-hover-color);
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-tabbar-tab-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-tabbar-tab-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& + .tab:hover,
|
||||
& + .tab.actived {
|
||||
.tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.actived {
|
||||
z-index: 5;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-active-color);
|
||||
& + .tab:hover,
|
||||
& + .tab.actived {
|
||||
.tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-tabbar-tab-active-bg);
|
||||
&.actived {
|
||||
z-index: 5;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-tabbar-tab-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-dividers {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -1px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
height: 14px;
|
||||
transform: translateY(-50%);
|
||||
.tab-dividers {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -1px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
height: 14px;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&::before {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1px;
|
||||
display: block;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background-color: var(--g-tabbar-dividers-bg);
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1px;
|
||||
display: block;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background-color: var(--g-tabbar-dividers-bg);
|
||||
opacity: 1;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s, background-color 0.3s;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
|
||||
.title {
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
color: var(--g-tabbar-tab-color);
|
||||
white-space: nowrap;
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
|
||||
transition: margin-right 0.3s;
|
||||
pointer-events: all;
|
||||
|
||||
&:has(+ .action-icon) {
|
||||
margin-right: 28px;
|
||||
.title {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
color: var(--g-tabbar-tab-color);
|
||||
white-space: nowrap;
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
|
||||
transition: margin-right 0.3s;
|
||||
|
||||
&:has(+ .action-icon) {
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
.action-icon {
|
||||
--uno: transition absolute inset-e-2 top-1/2 -translate-y-1/2 rounded-full z-10 w-5 h-5 flex-center text-xs "text-[var(--g-tabbar-tab-color)]" hover:(border bg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
--uno: transition absolute inset-e-2 top-1/2 -translate-y-1/2 rounded-full z-10 w-5 h-5 flex-center text-xs "text-[var(--g-tabbar-tab-color)]" hover:(border bg-secondary);
|
||||
}
|
||||
|
||||
.hotkey-number {
|
||||
--uno: border bg-secondary absolute inset-e-2 top-1/2 -translate-y-1/2 rounded-full z-10 w-5 h-5 flex-center text-xs "text-[var(--g-tabbar-tab-color)]";
|
||||
.hotkey-number {
|
||||
--uno: border bg-secondary absolute inset-e-2 top-1/2 -translate-y-1/2 rounded-full z-10 w-5 h-5 flex-center text-xs "text-[var(--g-tabbar-tab-color)]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function toggleColorScheme(event: MouseEvent) {
|
||||
:name="{
|
||||
light: 'i-ri:sun-line',
|
||||
dark: 'i-ri:moon-line',
|
||||
}[settingsStore.currentColorScheme!]" :size="16"
|
||||
}[settingsStore.currentColorScheme!]" class="size-4"
|
||||
/>
|
||||
</FaButton>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,6 @@ const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
<template>
|
||||
<FaButton v-if="settingsStore.mode === 'pc'" variant="ghost" size="icon" @click="toggle">
|
||||
<FaIcon :name="isFullscreen ? 'i-ri:fullscreen-exit-line' : 'i-ri:fullscreen-line'" :size="16" />
|
||||
<FaIcon :name="isFullscreen ? 'i-ri:fullscreen-exit-line' : 'i-ri:fullscreen-line'" class="size-4" />
|
||||
</FaButton>
|
||||
</template>
|
||||
|
||||
@@ -12,12 +12,12 @@ const isShow = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaButton :variant="settingsStore.mode === 'pc' ? 'outline' : 'ghost'" :size="settingsStore.mode === 'pc' ? undefined : 'icon'" :class="{ 'mx-2 pe-1.5 ps-2': settingsStore.mode === 'pc' }" @click="isShow = true">
|
||||
<FaIcon name="i-ri:search-line" :size="16" />
|
||||
<FaButton :variant="settingsStore.mode === 'pc' ? 'outline' : 'ghost'" :size="settingsStore.mode === 'pc' ? undefined : 'icon'" :class="{ 'mx-2 px-3': settingsStore.mode === 'pc' }" @click="isShow = true">
|
||||
<FaIcon name="i-ri:search-line" class="size-4" />
|
||||
<template v-if="settingsStore.mode === 'pc'">
|
||||
<span class="text-sm text-muted-foreground/60 transition group-hover-text-muted-foreground">搜索</span>
|
||||
<FaKbd v-if="settingsStore.settings.navSearch.enableHotkeys">
|
||||
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} S
|
||||
<FaKbd v-if="settingsStore.settings.navSearch.enableHotkeys" class="-me-1">
|
||||
{{ settingsStore.os === 'mac' ? '⌘' : 'Ctrl' }} K
|
||||
</FaKbd>
|
||||
</template>
|
||||
</FaButton>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Menu } from '@/types/global'
|
||||
import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-vue'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
import { Dialog, DialogDescription, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Search',
|
||||
@@ -19,26 +16,6 @@ const isShow = defineModel<boolean>({
|
||||
default: false,
|
||||
})
|
||||
|
||||
const overlayTransitionClass = ref({
|
||||
enter: 'ease-in-out duration-500',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in-out duration-500',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0',
|
||||
})
|
||||
|
||||
const transitionClass = computed(() => {
|
||||
return {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
|
||||
enterTo: 'opacity-100 translate-y-0 lg-scale-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100 translate-y-0 lg-scale-100',
|
||||
leaveTo: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
@@ -57,43 +34,44 @@ const searchInput = ref('')
|
||||
const sourceList = ref<listTypes[]>([])
|
||||
const actived = ref(-1)
|
||||
|
||||
const searchInputRef = useTemplateRef('searchInputRef')
|
||||
const searchResultRef = useTemplateRef<OverlayScrollbarsComponentRef>('searchResultRef')
|
||||
const searchResultRef = useTemplateRef('searchResultRef')
|
||||
const searchResultItemRef = useTemplateRef<HTMLElement[]>('searchResultItemRef')
|
||||
|
||||
const resultList = computed(() => {
|
||||
let result = []
|
||||
result = sourceList.value.filter((item) => {
|
||||
let flag = false
|
||||
if (item.title) {
|
||||
if (typeof item.title === 'function') {
|
||||
if (item.title().includes(searchInput.value)) {
|
||||
flag = true
|
||||
if (searchInput.value !== '') {
|
||||
if (item.title) {
|
||||
if (typeof item.title === 'function') {
|
||||
if (item.title().includes(searchInput.value)) {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (item.title.includes(searchInput.value)) {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (item.title.includes(searchInput.value)) {
|
||||
flag = true
|
||||
}
|
||||
if (item.path.includes(searchInput.value)) {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
if (item.path.includes(searchInput.value)) {
|
||||
flag = true
|
||||
}
|
||||
if (item.breadcrumb.some((b) => {
|
||||
if (typeof b.title === 'function') {
|
||||
if (b.title().includes(searchInput.value)) {
|
||||
return true
|
||||
if (item.breadcrumb.some((b) => {
|
||||
if (typeof b.title === 'function') {
|
||||
if (b.title().includes(searchInput.value)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (b.title?.includes(searchInput.value)) {
|
||||
return true
|
||||
else {
|
||||
if (b.title?.includes(searchInput.value)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})) {
|
||||
flag = true
|
||||
}
|
||||
return false
|
||||
})) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
})
|
||||
@@ -129,7 +107,7 @@ watch(() => resultList.value, () => {
|
||||
|
||||
onMounted(() => {
|
||||
initSourceList()
|
||||
hotkeys('alt+s', (e) => {
|
||||
hotkeys('command+k, ctrl+k', (e) => {
|
||||
if (settingsStore.settings.navSearch.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
isShow.value = true
|
||||
@@ -138,7 +116,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('alt+s')
|
||||
hotkeys.unbind('command+k, ctrl+k')
|
||||
})
|
||||
|
||||
function initSourceList() {
|
||||
@@ -205,8 +183,8 @@ function keyEnter() {
|
||||
}
|
||||
}
|
||||
function handleScroll() {
|
||||
if (searchResultRef.value) {
|
||||
const contentDom = searchResultRef.value.osInstance()!.elements().content
|
||||
if (searchResultRef.value?.areaRef?.ref?.el?.viewportElement) {
|
||||
const contentDom = searchResultRef.value.areaRef.ref.el.viewportElement
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
scrollTo = contentDom.scrollTop
|
||||
@@ -239,92 +217,77 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot as="template" :show="isShow">
|
||||
<Dialog :initial-focus="searchInputRef" class="fixed inset-0 z-2000 flex" @close="isShow = false">
|
||||
<TransitionChild v-if="settingsStore.mode === 'pc'" as="template" v-bind="overlayTransitionClass">
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" />
|
||||
</TransitionChild>
|
||||
<div class="fixed inset-0">
|
||||
<div class="h-full flex items-end justify-center text-center lg-items-center lg-p-4">
|
||||
<TransitionChild as="template" v-bind="transitionClass">
|
||||
<div class="h-full max-h-full w-full lg-max-h-4/5 lg-max-w-2xl">
|
||||
<DialogPanel class="relative h-full flex flex-col text-left lg-max-h-full">
|
||||
<div class="h-full flex flex-col overflow-y-auto border bg-background shadow-xl lg-h-auto lg-rounded-md">
|
||||
<div class="h-12 flex flex-shrink-0 items-center border-b">
|
||||
<div class="h-full w-14 flex-center">
|
||||
<FaIcon name="i-ri:search-line" :size="18" class="text-foreground/30" />
|
||||
</div>
|
||||
<input ref="searchInputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" class="h-full w-full border-0 rounded-md bg-transparent text-base text-foreground focus-outline-none placeholder-foreground/30" @keydown.esc="isShow = false" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<div v-if="settingsStore.mode === 'mobile'" class="h-full w-14 flex-center border-s">
|
||||
<FaIcon name="i-carbon:close" :size="18" @click="isShow = false" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription class="relative m-0 h-full of-y-hidden">
|
||||
<OverlayScrollbarsComponent ref="searchResultRef" :options="{ scrollbars: { autoHide: 'leave', autoHideDelay: 300 } }" defer class="h-full">
|
||||
<template v-if="resultList.length > 0">
|
||||
<div v-for="(item, index) in resultList" ref="searchResultItemRef" :key="item.path" class="p-4" :data-index="index" @click="pageJump(item.path, item.link)" @mouseover="actived = index">
|
||||
<a class="flex cursor-pointer items-center border rounded-lg" :class="{ '-mt-4': index !== 0, 'bg-accent': index === actived }">
|
||||
<FaIcon v-if="item.icon" :name="item.icon" :size="20" class="basis-16 transition" :class="{ 'scale-120 text-primary': index === actived }" />
|
||||
<div class="flex flex-1 flex-col gap-1 truncate border-s px-4 py-3">
|
||||
<div class="truncate text-start text-base font-bold">{{ (typeof item.title === 'function' ? item.title() : item.title) ?? '[ 无标题 ]' }}</div>
|
||||
<Breadcrumb v-if="item.breadcrumb.length" class="truncate">
|
||||
<BreadcrumbItem v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex" class="text-xs">
|
||||
{{ (typeof bc.title === 'function' ? bc.title() : bc.title) ?? '[ 无标题 ]' }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="searchInput === ''">
|
||||
<div class="h-full flex-col-center py-6 text-secondary-foreground/50">
|
||||
<FaIcon name="i-tabler:mood-smile" :size="40" />
|
||||
<p class="m-2 text-base">
|
||||
输入你要搜索的导航
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-full flex-col-center py-6 text-secondary-foreground/50">
|
||||
<FaIcon name="i-tabler:mood-empty" :size="40" />
|
||||
<p class="m-2 text-base">
|
||||
没有找到你想要的
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</OverlayScrollbarsComponent>
|
||||
</DialogDescription>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="flex justify-between border-t px-4 py-3">
|
||||
<div class="flex gap-8">
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ion:md-return-left" :size="14" />
|
||||
</FaKbd>
|
||||
<span>访问</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ant-design:caret-up-filled" :size="14" />
|
||||
</FaKbd>
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ant-design:caret-down-filled" :size="14" />
|
||||
</FaKbd>
|
||||
<span>切换</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.navSearch.enableHotkeys" class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
ESC
|
||||
</FaKbd>
|
||||
<span>退出</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
<FaModal ref="searchResultRef" v-model="isShow" border :footer="settingsStore.mode === 'pc'" :closable="false" class="w-full lg-max-w-2xl" content-class="flex flex-col p-0 min-h-auto" header-class="p-0" footer-class="p-0">
|
||||
<template #header>
|
||||
<div class="h-12 flex flex-shrink-0 items-center">
|
||||
<div class="h-full w-14 flex-center">
|
||||
<FaIcon name="i-ri:search-line" class="size-4 text-foreground/30" />
|
||||
</div>
|
||||
<input v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" class="h-full w-full border-0 rounded-md bg-transparent text-base text-foreground focus-outline-none placeholder-foreground/30" @keydown.esc="isShow = false" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<div v-if="settingsStore.mode === 'mobile'" class="h-full w-14 flex-center border-s">
|
||||
<FaIcon name="i-carbon:close" class="size-4" @click="isShow = false" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="w-full flex justify-between px-4 py-3">
|
||||
<div class="flex gap-8">
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ion:md-return-left" class="size-4" />
|
||||
</FaKbd>
|
||||
<span>访问</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ant-design:caret-up-filled" class="size-4" />
|
||||
</FaKbd>
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ant-design:caret-down-filled" class="size-4" />
|
||||
</FaKbd>
|
||||
<span>切换</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.navSearch.enableHotkeys" class="inline-flex items-center gap-1 text-xs">
|
||||
<FaKbd>
|
||||
ESC
|
||||
</FaKbd>
|
||||
<span>退出</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<template v-if="resultList.length > 0">
|
||||
<div v-for="(item, index) in resultList" ref="searchResultItemRef" :key="item.path" class="p-4" :data-index="index" @click="pageJump(item.path, item.link)" @mouseover="actived = index">
|
||||
<a class="flex cursor-pointer items-center border rounded-lg" :class="{ '-mt-4': index !== 0, 'bg-accent': index === actived }">
|
||||
<FaIcon v-if="item.icon" :name="item.icon" class="size-5 basis-16 transition" :class="{ 'scale-120 text-primary': index === actived }" />
|
||||
<div class="flex flex-1 flex-col gap-1 truncate border-s px-4 py-3">
|
||||
<div class="truncate text-start text-base font-bold">{{ (typeof item.title === 'function' ? item.title() : item.title) ?? '[ 无标题 ]' }}</div>
|
||||
<Breadcrumb v-if="item.breadcrumb.length" class="truncate">
|
||||
<BreadcrumbItem v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex" class="text-xs">
|
||||
{{ (typeof bc.title === 'function' ? bc.title() : bc.title) ?? '[ 无标题 ]' }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="searchInput === ''">
|
||||
<div class="h-full flex-col-center py-6 text-secondary-foreground/50">
|
||||
<FaIcon name="i-tabler:mood-smile" class="size-10" />
|
||||
<p class="m-2 text-base">
|
||||
输入你要搜索的导航
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-full flex-col-center py-6 text-secondary-foreground/50">
|
||||
<FaIcon name="i-tabler:mood-empty" class="size-10" />
|
||||
<p class="m-2 text-base">
|
||||
没有找到你想要的
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FaModal>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@ function handleClick() {
|
||||
|
||||
<template>
|
||||
<FaButton variant="ghost" size="icon" @click="handleClick" @animationend="isAnimating = false">
|
||||
<FaIcon name="i-iconoir:refresh-double" :size="16" :class="{ animation: isAnimating }" />
|
||||
<FaIcon name="i-iconoir:refresh-double" class="size-4" :class="{ animation: isAnimating }" />
|
||||
</FaButton>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ToolbarStart from '@/slots/ToolbarStart/index.vue'
|
||||
import { useSlots } from '@/slots'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Breadcrumb from './Breadcrumb/index.vue'
|
||||
|
||||
@@ -13,9 +13,9 @@ const settingsStore = useSettingsStore()
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<FaButton v-if="settingsStore.mode === 'mobile'" variant="ghost" size="icon" class="h-9 w-9 -rotate-z-180" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<FaIcon name="toolbar-collapse" :size="16" />
|
||||
<FaIcon name="toolbar-collapse" class="size-4" />
|
||||
</FaButton>
|
||||
<ToolbarStart />
|
||||
<component :is="useSlots('toolbar-start')" />
|
||||
<Breadcrumb v-if="settingsStore.settings.toolbar.breadcrumb" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ToolbarEnd from '@/slots/ToolbarEnd/index.vue'
|
||||
import { useSlots } from '@/slots'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import ColorScheme from './ColorScheme/index.vue'
|
||||
import Fullscreen from './Fullscreen/index.vue'
|
||||
@@ -7,7 +7,7 @@ import NavSearch from './NavSearch/index.vue'
|
||||
import PageReload from './PageReload/index.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Tools',
|
||||
name: 'ToolbarRightSide',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -19,6 +19,6 @@ const settingsStore = useSettingsStore()
|
||||
<Fullscreen v-if="settingsStore.settings.toolbar.fullscreen" />
|
||||
<PageReload v-if="settingsStore.settings.toolbar.pageReload" />
|
||||
<ColorScheme v-if="settingsStore.settings.toolbar.colorScheme" />
|
||||
<ToolbarEnd />
|
||||
<component :is="useSlots('toolbar-end')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -57,7 +57,7 @@ watch(scrollTop, (val, oldVal) => {
|
||||
.topbar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: calc(100% - var(--scrollbar-width, 0px));
|
||||
box-shadow: 0 1px 0 0 hsl(var(--border));
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
|
||||
@@ -73,8 +73,8 @@ watch(scrollTop, (val, oldVal) => {
|
||||
background-image: linear-gradient(0deg, transparent, var(--g-main-area-bg));
|
||||
box-shadow: 0 -1px 0 0 hsl(var(--border));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
transform: translateY(100%);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
&.topbar-fixed,
|
||||
|
||||
@@ -23,7 +23,7 @@ function open() {
|
||||
<Transition name="slide-right" mode="out-in" appear>
|
||||
<FaPageMain :key="route.meta.link" class="flex flex-1 flex-col justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<FaIcon name="i-icon-park-twotone:planet" :size="120" class="text-primary/80" />
|
||||
<FaIcon name="i-icon-park-twotone:planet" class="size-30 text-primary/80" />
|
||||
<div class="my-2 text-xl text-dark dark-text-white">
|
||||
是否访问此链接
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import FreePosition from '@/slots/FreePosition/index.vue'
|
||||
import { useSlots } from '@/slots'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
@@ -154,7 +154,7 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
<AppSetting />
|
||||
</template>
|
||||
<BackTop />
|
||||
<FreePosition />
|
||||
<component :is="useSlots('free-position')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import uiProvider from './ui/provider'
|
||||
|
||||
import '@/utils/systemCopyright'
|
||||
|
||||
import 'overlayscrollbars/overlayscrollbars.css'
|
||||
// 加载 svg 图标
|
||||
import 'virtual:svg-icons-register'
|
||||
import 'virtual:uno.css'
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export default defineFakeRoute([
|
||||
data: {
|
||||
account: body.account,
|
||||
token: `${body.account}:${faker.internet.jwt()}`,
|
||||
avatar: 'https://fantastic-admin.hurui.me/logo.png',
|
||||
avatar: 'https://fantastic-admin.hurui.me/logo.svg',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import type { RecursiveRequired, Settings } from '#/global'
|
||||
import settingsDefault from '@/settings.default'
|
||||
import { merge } from '@/utils/object'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
|
||||
const globalSettings: Settings.all = {
|
||||
// 请在此处编写或粘贴配置代码
|
||||
}
|
||||
|
||||
export default merge(globalSettings, settingsDefault) as RecursiveRequired<Settings.all>
|
||||
export default merge(globalSettings, cloneDeep(settingsDefault)) as RecursiveRequired<Settings.all>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -2,19 +2,22 @@
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import Profile from './profile.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isProfileShow = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDropdown
|
||||
align="end"
|
||||
:items="[
|
||||
align="end" :items="[
|
||||
[
|
||||
{ label: settingsStore.settings.home.title, icon: 'i-mdi:home', handle: () => router.push({ path: settingsStore.settings.home.fullPath }), hide: !settingsStore.settings.home.enable },
|
||||
{ label: '个人设置', icon: 'i-mdi:account', handle: () => isProfileShow = true },
|
||||
],
|
||||
[
|
||||
{ label: '快捷键介绍', icon: 'i-mdi:keyboard', handle: () => eventBus.emit('global-hotkeys-intro-toggle'), hide: settingsStore.mode !== 'pc' },
|
||||
@@ -44,7 +47,7 @@ const userStore = useUserStore()
|
||||
</template>
|
||||
<FaButton variant="ghost" class="h-9 flex-center gap-1 px-2">
|
||||
<FaAvatar :src="userStore.avatar" class="size-6">
|
||||
<FaIcon name="i-carbon:user-avatar-filled-alt" :size="24" class="text-secondary-foreground/50" />
|
||||
<FaIcon name="i-carbon:user-avatar-filled-alt" class="size-6 text-secondary-foreground/50" />
|
||||
</FaAvatar>
|
||||
<template v-if="settingsStore.mode === 'pc'">
|
||||
{{ userStore.account }}
|
||||
@@ -52,4 +55,7 @@ const userStore = useUserStore()
|
||||
</template>
|
||||
</FaButton>
|
||||
</FaDropdown>
|
||||
<FaModal v-model="isProfileShow" align-center :header="false" :footer="false" :close-on-click-overlay="false" :close-on-press-escape="false" class="h-500px min-w-600px overflow-hidden" content-class="min-h-full p-0 flex">
|
||||
<Profile />
|
||||
</FaModal>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import EditPasswordForm from '@/components/AccountForm/EditPasswordForm.vue'
|
||||
|
||||
const active = ref(0)
|
||||
const tabs = ref([
|
||||
{
|
||||
title: '基本设置',
|
||||
description: '账号的基本信息,头像、昵称等',
|
||||
},
|
||||
{
|
||||
title: '安全设置',
|
||||
description: '定期修改密码可以提高帐号安全性',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full w-full">
|
||||
<div class="fixed inset-s-0 bottom-0 top-0 h-full w-40 border-e">
|
||||
<div v-for="(tab, index) in tabs" :key="index" class="cursor-pointer px-4 py-3 transition-background-color space-y-2 hover-bg-accent/50" :class="{ 'bg-accent hover-bg-accent!': active === index }" @click="active = index">
|
||||
<div class="text-base text-accent-foreground leading-tight">
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
<div class="text-xs text-accent-foreground/50">
|
||||
{{ tab.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-40 min-h-full flex-col-center p-10">
|
||||
<div v-if="active === 0">
|
||||
请开发者自行扩展
|
||||
</div>
|
||||
<EditPasswordForm v-if="active === 1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { pascalCase } from 'scule'
|
||||
|
||||
type Slots =
|
||||
'header-start' | 'header-end' | 'header-after-logo' |
|
||||
'main-sidebar-top' | 'main-sidebar-bottom' | 'main-sidebar-after-logo' |
|
||||
'sub-sidebar-top' | 'sub-sidebar-bottom' | 'sub-sidebar-after-logo' |
|
||||
'tabbar-start' | 'tabbar-end' |
|
||||
'toolbar-start' | 'toolbar-end' |
|
||||
'free-position'
|
||||
|
||||
function tryLoadComponent(name: Slots) {
|
||||
const componentMap = import.meta.glob('./*/index.vue', { eager: true })
|
||||
const path = `./${pascalCase(name as unknown as string)}/index.vue`
|
||||
const component = componentMap[path as keyof typeof componentMap]
|
||||
if (!component) {
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'SlotsInvalidComponent',
|
||||
render: () => null,
|
||||
}),
|
||||
}
|
||||
}
|
||||
return component
|
||||
}
|
||||
|
||||
export function useSlots(name: Slots) {
|
||||
const component = tryLoadComponent(name)
|
||||
return defineComponent((component as any).default)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Settings } from '#/global'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import settingsDefault from '@/settings'
|
||||
import { merge } from '@/utils/object'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
|
||||
const useSettingsStore = defineStore(
|
||||
// 唯一ID
|
||||
@@ -144,7 +145,7 @@ const useSettingsStore = defineStore(
|
||||
|
||||
// 更新应用配置
|
||||
function updateSettings(data: Settings.all, fromBase = false) {
|
||||
settings.value = merge(data, fromBase ? settingsDefault : settings.value)
|
||||
settings.value = merge(data, fromBase ? cloneDeep(settingsDefault) : settings.value)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -90,7 +90,7 @@ const useUserStore = defineStore(
|
||||
// 修改密码
|
||||
async function editPassword(data: {
|
||||
password: string
|
||||
newpassword: string
|
||||
newPassword: string
|
||||
}) {
|
||||
await apiUser.passwordEdit(data)
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -12,6 +12,7 @@ declare module 'vue' {
|
||||
FaButton: typeof import('./../ui/components/FaButton/index.vue')['default']
|
||||
FaCard: typeof import('./../ui/components/FaCard/index.vue')['default']
|
||||
FaCheckbox: typeof import('./../ui/components/FaCheckbox/index.vue')['default']
|
||||
FaContextMenu: typeof import('./../ui/components/FaContextMenu/index.vue')['default']
|
||||
FaDivider: typeof import('./../ui/components/FaDivider/index.vue')['default']
|
||||
FaDrawer: typeof import('./../ui/components/FaDrawer/index.vue')['default']
|
||||
FaDropdown: typeof import('./../ui/components/FaDropdown/index.vue')['default']
|
||||
@@ -19,8 +20,6 @@ declare module 'vue' {
|
||||
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']
|
||||
FaLoading: typeof import('./../ui/components/FaLoading/index.vue')['default']
|
||||
FaMaskScrollContainer: typeof import('./../ui/components/FaMaskScrollContainer/index.vue')['default']
|
||||
FaModal: typeof import('./../ui/components/FaModal/index.vue')['default']
|
||||
FaNotAllowed: typeof import('./../ui/components/FaNotAllowed/index.vue')['default']
|
||||
FaNotification: typeof import('./../ui/components/FaNotification/index.vue')['default']
|
||||
@@ -29,6 +28,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']
|
||||
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']
|
||||
FaSmartFixedBlock: typeof import('./../ui/components/FaSmartFixedBlock/index.vue')['default']
|
||||
|
||||
Vendored
+2
-2
@@ -74,7 +74,7 @@ declare namespace Settings {
|
||||
}
|
||||
interface layout {
|
||||
/**
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 992px 时自动切换为移动端展示
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 1024px 时自动切换为移动端展示
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableMobileAdaptation?: boolean
|
||||
@@ -97,7 +97,7 @@ declare namespace Settings {
|
||||
* 主导航点击模式
|
||||
* @默认值 `'switch'` 切换
|
||||
* @可选值 `'jump'` 跳转
|
||||
* @可选值 `'smart'` 智能选择
|
||||
* @可选值 `'smart'` 智能选择,判断次导航是否只有且只有一个可访问的菜单进行切换或跳转操作
|
||||
*/
|
||||
mainMenuClickMode?: 'switch' | 'jump' | 'smart'
|
||||
/**
|
||||
|
||||
Vendored
+3
-3
@@ -5,9 +5,9 @@ declare interface Window {
|
||||
|
||||
declare const __SYSTEM_INFO__: {
|
||||
pkg: {
|
||||
version: Recordable<string>
|
||||
dependencies: Recordable<string>
|
||||
devDependencies: Recordable<string>
|
||||
version: string
|
||||
dependencies: Record<string, string>
|
||||
devDependencies: Record<string, string>
|
||||
}
|
||||
lastBuildTime: string
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ const isCheck = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot v-if="isCheck" />
|
||||
<slot v-else name="no-auth" />
|
||||
<div class="contents">
|
||||
<slot v-if="isCheck" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'radix-vue'
|
||||
import { ContextMenuRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuRootProps>()
|
||||
const emits = defineEmits<ContextMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuCheckboxItem,
|
||||
type ContextMenuCheckboxItemEmits,
|
||||
type ContextMenuCheckboxItemProps,
|
||||
ContextMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</ContextMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</ContextMenuCheckboxItem>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
type ContextMenuContentEmits,
|
||||
type ContextMenuContentProps,
|
||||
ContextMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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 />
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuGroup, type ContextMenuGroupProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuGroup v-bind="props">
|
||||
<slot />
|
||||
</ContextMenuGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuItem,
|
||||
type ContextMenuItemEmits,
|
||||
type ContextMenuItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
const emits = defineEmits<ContextMenuItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuLabel
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('px-2 py-1.5 text-sm font-semibold text-foreground',
|
||||
inset && 'pl-8', props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuLabel>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuPortal, type ContextMenuPortalProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuPortalProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuPortal v-bind="props">
|
||||
<slot />
|
||||
</ContextMenuPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuRadioGroup,
|
||||
type ContextMenuRadioGroupEmits,
|
||||
type ContextMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuRadioGroupProps>()
|
||||
const emits = defineEmits<ContextMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRadioGroup v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuRadioGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuItemIndicator,
|
||||
ContextMenuRadioItem,
|
||||
type ContextMenuRadioItemEmits,
|
||||
type ContextMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</ContextMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</ContextMenuRadioItem>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuSeparator,
|
||||
type ContextMenuSeparatorProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-border', props.class)" />
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuSub,
|
||||
type ContextMenuSubEmits,
|
||||
type ContextMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuSubProps>()
|
||||
const emits = defineEmits<ContextMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSub v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuSub>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSubContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 />
|
||||
</ContextMenuSubContent>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuSubTrigger,
|
||||
type ContextMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSubTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</ContextMenuSubTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuTrigger, type ContextMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuTrigger v-bind="forwardedProps">
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
export { default as ContextMenu } from './ContextMenu.vue'
|
||||
export { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue'
|
||||
export { default as ContextMenuContent } from './ContextMenuContent.vue'
|
||||
export { default as ContextMenuGroup } from './ContextMenuGroup.vue'
|
||||
export { default as ContextMenuItem } from './ContextMenuItem.vue'
|
||||
export { default as ContextMenuLabel } from './ContextMenuLabel.vue'
|
||||
export { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue'
|
||||
export { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue'
|
||||
export { default as ContextMenuSeparator } from './ContextMenuSeparator.vue'
|
||||
export { default as ContextMenuShortcut } from './ContextMenuShortcut.vue'
|
||||
export { default as ContextMenuSub } from './ContextMenuSub.vue'
|
||||
export { default as ContextMenuSubContent } from './ContextMenuSubContent.vue'
|
||||
export { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue'
|
||||
export { default as ContextMenuTrigger } from './ContextMenuTrigger.vue'
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from './context-menu'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaContextMenu',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
items: {
|
||||
label: string
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
hide?: boolean
|
||||
handle?: () => void
|
||||
}[][]
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
default?: () => VNode
|
||||
label?: () => VNode
|
||||
}>()
|
||||
|
||||
const myItems = computed(() => {
|
||||
return props.items.map((item) => {
|
||||
return item.filter(v => !v.hide)
|
||||
}).filter(v => v.length)
|
||||
})
|
||||
|
||||
const isItemsHasIcon = computed(() => {
|
||||
return myItems.value.some(item => item.some(v => v.icon))
|
||||
})
|
||||
|
||||
function handleItemClick(item: { handle?: () => void }) {
|
||||
item.handle?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenu :modal="false">
|
||||
<ContextMenuTrigger as-child>
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent class="z-1050">
|
||||
<template v-if="!!slots.label">
|
||||
<ContextMenuLabel>
|
||||
<slot name="label" />
|
||||
</ContextMenuLabel>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<template v-for="(item, index) in myItems" :key="index">
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem v-for="(v, i) in item" :key="i" :disabled="v.disabled" class="cursor-pointer" @click="handleItemClick(v)">
|
||||
<div v-if="isItemsHasIcon" class="size-4 flex-center">
|
||||
<FaIcon v-if="v.icon" :name="v.icon" class="size-4" />
|
||||
</div>
|
||||
{{ v.label }}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
<ContextMenuSeparator v-if="index !== myItems.length - 1" />
|
||||
</template>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -157,11 +156,13 @@ function handleAnimationEnd() {
|
||||
</slot>
|
||||
</SheetHeader>
|
||||
<div class="m-0 flex-1 of-y-hidden">
|
||||
<OverlayScrollbarsComponent :options="{ scrollbars: { autoHide: 'leave', autoHideDelay: 300 } }" defer class="h-full p-4">
|
||||
<slot />
|
||||
</OverlayScrollbarsComponent>
|
||||
<FaScrollArea class="h-full">
|
||||
<div class="p-4">
|
||||
<slot />
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" :size="36" />
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter
|
||||
|
||||
@@ -50,7 +50,7 @@ function handleItemClick(item: { handle?: () => void }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu :modal="false">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
@@ -65,7 +65,7 @@ function handleItemClick(item: { handle?: () => void }) {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem v-for="(v, i) in item" :key="i" :disabled="v.disabled" @click="handleItemClick(v)">
|
||||
<div v-if="isItemsHasIcon" class="size-4 flex-center">
|
||||
<FaIcon v-if="v.icon" :name="v.icon" :size="16" />
|
||||
<FaIcon v-if="v.icon" :name="v.icon" class="size-4" />
|
||||
</div>
|
||||
{{ v.label }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -42,35 +42,9 @@ function onScroll() {
|
||||
<template>
|
||||
<FaSmartFixedBlock position="bottom">
|
||||
<div ref="actionBarRef">
|
||||
<div class="fixed-action-bar bottom-0 z-4 border-t bg-background p-5 text-center transition" :class="{ mask: !isBottom }">
|
||||
<div class="relative bottom-0 z-4 border-t bg-background p-5 text-center transition before:(pointer-events-none absolute left-0 z-1 h-12 w-full from-transparent to-[var(--g-main-area-bg)] bg-gradient-to-b opacity-0 transition-opacity content-empty -top-1px -translate-y-full)" :class="{ 'before:(opacity-100)': !isBottom }">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</FaSmartFixedBlock>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fixed-action-bar {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-image: linear-gradient(180deg, transparent, var(--g-main-area-bg));
|
||||
box-shadow: 0 1px 0 0 hsl(var(--border));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
&.mask::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { UseImage } from '@vueuse/components'
|
||||
|
||||
@@ -8,10 +10,7 @@ defineOptions({
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
flip?: 'horizontal' | 'vertical' | 'both'
|
||||
rotate?: number
|
||||
color?: string
|
||||
size?: string | number
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const outputType = computed(() => {
|
||||
@@ -31,47 +30,21 @@ const outputType = computed(() => {
|
||||
return 'svg'
|
||||
}
|
||||
})
|
||||
|
||||
const style = computed(() => {
|
||||
const transform = []
|
||||
if (props.flip) {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
transform.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
transform.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
transform.push('rotateX(180deg)')
|
||||
transform.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (props.rotate) {
|
||||
transform.push(`rotate(${props.rotate % 360}deg)`)
|
||||
}
|
||||
return {
|
||||
...(props.color && { color: props.color }),
|
||||
...(props.size && { fontSize: typeof props.size === 'number' ? `${props.size}px` : props.size }),
|
||||
...(transform.length && { transform: transform.join(' ') }),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i class="relative h-[1em] w-[1em] flex-inline items-center justify-center fill-current leading-[1em]" :style="style">
|
||||
<i v-if="outputType === 'unocss'" class="h-[1em] w-[1em] shrink-0" :class="name" />
|
||||
<Icon v-else-if="outputType === 'iconify'" :icon="name" />
|
||||
<svg v-else-if="outputType === 'svg'" class="h-[1em] w-[1em] shrink-0" aria-hidden="true">
|
||||
<i :class="cn('relative size-[1em] flex-inline items-center justify-center fill-current leading-[1em]', props.class)">
|
||||
<i v-if="outputType === 'unocss'" class="size-inherit shrink-0" :class="name" />
|
||||
<Icon v-else-if="outputType === 'iconify'" :icon="name" class="shrink-0 size-inherit!" />
|
||||
<svg v-else-if="outputType === 'svg'" class="size-inherit shrink-0" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
<UseImage v-else-if="outputType === 'img'" :src="name" class="h-[1em] w-[1em] shrink-0">
|
||||
<UseImage v-else-if="outputType === 'img'" :src="name" class="size-inherit shrink-0">
|
||||
<template #loading>
|
||||
<i class="i-line-md:loading-loop h-[1em] w-[1em]" />
|
||||
<i class="i-line-md:loading-loop size-inherit" />
|
||||
</template>
|
||||
<template #error>
|
||||
<i class="i-tdesign:image-error h-[1em] w-[1em]" />
|
||||
<i class="i-tdesign:image-error size-inherit" />
|
||||
</template>
|
||||
</UseImage>
|
||||
</i>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaMaskScrollContainer',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
scroll?: 'x' | 'y'
|
||||
gradientColor?: string
|
||||
}>(),
|
||||
{
|
||||
scroll: 'y',
|
||||
gradientColor: 'hsl(var(--background))',
|
||||
},
|
||||
)
|
||||
|
||||
const maskScrollContainerOuterRef = useTemplateRef('maskScrollContainerOuterRef')
|
||||
const maskScrollContainerInnerRef = useTemplateRef('maskScrollContainerInnerRef')
|
||||
|
||||
const showMaskStart = ref(false)
|
||||
const showMaskEnd = ref(false)
|
||||
|
||||
function updateMask() {
|
||||
if (props.scroll === 'x') {
|
||||
const scrollLeft = maskScrollContainerOuterRef.value?.scrollLeft ?? 0
|
||||
showMaskStart.value = scrollLeft > 0
|
||||
const clientWidth = maskScrollContainerOuterRef.value?.clientWidth ?? 0
|
||||
const scrollWidth = maskScrollContainerOuterRef.value?.scrollWidth ?? 0
|
||||
showMaskEnd.value = Math.ceil(scrollLeft + clientWidth) < scrollWidth
|
||||
}
|
||||
else {
|
||||
const scrollTop = maskScrollContainerOuterRef.value?.scrollTop ?? 0
|
||||
showMaskStart.value = scrollTop > 0
|
||||
const clientHeight = maskScrollContainerOuterRef.value?.clientHeight ?? 0
|
||||
const scrollHeight = maskScrollContainerOuterRef.value?.scrollHeight ?? 0
|
||||
showMaskEnd.value = Math.ceil(scrollTop + clientHeight) < scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
if (props.scroll === 'x') {
|
||||
maskScrollContainerOuterRef.value?.scrollBy({
|
||||
left: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
else {
|
||||
maskScrollContainerOuterRef.value?.scrollBy({
|
||||
top: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { x, y } = useScroll(maskScrollContainerOuterRef)
|
||||
watch([() => x.value, () => y.value], updateMask)
|
||||
|
||||
const { width, height } = useElementSize(maskScrollContainerInnerRef)
|
||||
watch([() => width.value, () => height.value], () => {
|
||||
if (width.value > 0 || height.value > 0) {
|
||||
updateMask()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
function scrollTo(scrollNumber: number) {
|
||||
if (props.scroll === 'x') {
|
||||
maskScrollContainerOuterRef.value?.scrollTo({
|
||||
left: scrollNumber,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
else {
|
||||
maskScrollContainerOuterRef.value?.scrollTo({
|
||||
top: scrollNumber,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
ref: useTemplateRef('maskScrollContainerRef'),
|
||||
scrollTo,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="maskScrollContainerRef" class="relative flex overflow-hidden after:(pointer-events-none absolute z-1 from-transparent to-[var(--mask-scroll-container-gradient-color)] opacity-0 transition-opacity content-empty) before:(pointer-events-none absolute z-1 from-transparent to-[var(--mask-scroll-container-gradient-color)] opacity-0 transition-opacity content-empty)" :class="{
|
||||
'after:(bg-gradient-to-r h-full w-12 right-0) before:(bg-gradient-to-l h-full w-12)': props.scroll === 'x',
|
||||
'after:(bg-gradient-to-b h-12 w-full bottom-0) before:(bg-gradient-to-t h-12 w-full)': props.scroll === 'y',
|
||||
'before:(opacity-100!)': showMaskStart,
|
||||
'after:(opacity-100!)': showMaskEnd,
|
||||
}" :style="{
|
||||
'--mask-scroll-container-gradient-color': props.gradientColor,
|
||||
}"
|
||||
>
|
||||
<div ref="maskScrollContainerOuterRef" class="relative z-0 flex-1 overflow-auto scrollbar-none" @wheel.prevent="onWheel">
|
||||
<div ref="maskScrollContainerInnerRef" class="h-full w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,6 +35,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
function handleMaximize() {
|
||||
emits('toggleMaximize', !props.maximize)
|
||||
}
|
||||
|
||||
const dialogContentRef = useTemplateRef('dialogContentRef')
|
||||
|
||||
defineExpose({
|
||||
el: dialogContentRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -45,16 +51,17 @@ function handleMaximize() {
|
||||
})"
|
||||
/>
|
||||
<DialogContent
|
||||
ref="dialogContentRef"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-2000 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'fixed left-1/2 top-1/2 z-2000 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-0 data-[state=closed]:slide-out-to-top-1/5 data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-1/5 sm:rounded-lg',
|
||||
props.class,
|
||||
)"
|
||||
@animationend="emits('animationEnd')"
|
||||
>
|
||||
<slot />
|
||||
<div class="absolute right-4 top-4 flex-center gap-2">
|
||||
<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">
|
||||
<Maximize v-if="!props.maximize" class="h-4 w-4" />
|
||||
<Minimize v-else class="h-4 w-4" />
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
|
||||
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-start', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,10 @@ export interface ModalProps {
|
||||
closable?: boolean
|
||||
maximize?: boolean
|
||||
maximizable?: boolean
|
||||
centered?: boolean
|
||||
bordered?: boolean
|
||||
draggable?: boolean
|
||||
center?: boolean
|
||||
border?: boolean
|
||||
alignCenter?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
showConfirmButton?: boolean
|
||||
@@ -39,12 +41,12 @@ export interface ModalEmits {
|
||||
'cancel': []
|
||||
}
|
||||
|
||||
type alertOptions = Pick<ModalProps, 'title' | 'description' | 'icon' | 'centered' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
type alertOptions = Pick<ModalProps, 'title' | 'description' | 'icon' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
onConfirm?: () => any
|
||||
}
|
||||
|
||||
type confirmOptions = Pick<ModalProps, 'title' | 'description' | 'centered' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
type confirmOptions = Pick<ModalProps, 'title' | 'description' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
onConfirm?: () => any
|
||||
onCancel?: () => any
|
||||
@@ -59,7 +61,8 @@ export function useFaModal() {
|
||||
modelValue: true,
|
||||
icon: 'info',
|
||||
closable: false,
|
||||
bordered: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
@@ -79,7 +82,8 @@ export function useFaModal() {
|
||||
modelValue: true,
|
||||
icon: 'success',
|
||||
closable: false,
|
||||
bordered: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
@@ -99,7 +103,8 @@ export function useFaModal() {
|
||||
modelValue: true,
|
||||
icon: 'warning',
|
||||
closable: false,
|
||||
bordered: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
@@ -119,7 +124,8 @@ export function useFaModal() {
|
||||
modelValue: true,
|
||||
icon: 'error',
|
||||
closable: false,
|
||||
bordered: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
@@ -138,7 +144,8 @@ export function useFaModal() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
closable: false,
|
||||
bordered: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModalEmits, ModalProps } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { VisuallyHidden } from 'radix-vue'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './dialog'
|
||||
import { useDraggable } from './use-draggable'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaModal',
|
||||
@@ -22,8 +24,10 @@ const props = withDefaults(
|
||||
closable: true,
|
||||
maximize: false,
|
||||
maximizable: false,
|
||||
centered: false,
|
||||
bordered: true,
|
||||
draggable: false,
|
||||
center: false,
|
||||
border: true,
|
||||
alignCenter: false,
|
||||
overlay: true,
|
||||
overlayBlur: false,
|
||||
showConfirmButton: true,
|
||||
@@ -47,12 +51,47 @@ const slots = defineSlots<{
|
||||
footer?: () => VNode
|
||||
}>()
|
||||
|
||||
const dialogContentRef = useTemplateRef('dialogContentRef')
|
||||
const dialogHeaderRef = ref()
|
||||
const dialogAreaRef = useTemplateRef('dialogAreaRef')
|
||||
const dialogRef = ref()
|
||||
|
||||
defineExpose({
|
||||
areaRef: dialogAreaRef,
|
||||
})
|
||||
|
||||
const isOpen = ref(props.modelValue)
|
||||
const isMaximize = ref(props.maximize)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
isOpen.value = newValue
|
||||
})
|
||||
|
||||
const { isDragging, transform } = useDraggable(
|
||||
dialogRef,
|
||||
dialogHeaderRef,
|
||||
computed(() => props.draggable && props.header && !isMaximize.value),
|
||||
)
|
||||
function setTransform() {
|
||||
if (isMaximize.value) {
|
||||
dialogRef.value.style.transform = 'none'
|
||||
}
|
||||
else {
|
||||
dialogRef.value.style.transform = `translate(${transform.offsetX}px, ${transform.offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
watch(isOpen, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
if (dialogContentRef.value) {
|
||||
dialogRef.value = dialogContentRef.value.el?.$el
|
||||
setTransform()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function updateOpen(value: boolean) {
|
||||
isOpen.value = value
|
||||
emits('update:modelValue', value)
|
||||
@@ -64,8 +103,6 @@ function updateOpen(value: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
const isMaximize = ref(props.maximize)
|
||||
|
||||
function onConfirm() {
|
||||
updateOpen(false)
|
||||
emits('confirm')
|
||||
@@ -95,6 +132,7 @@ function handleEscapeKeyDown(e: KeyboardEvent) {
|
||||
|
||||
function handleMaximize(val: boolean) {
|
||||
isMaximize.value = val
|
||||
setTransform()
|
||||
}
|
||||
|
||||
function handleAnimationEnd() {
|
||||
@@ -110,12 +148,15 @@ function handleAnimationEnd() {
|
||||
<template>
|
||||
<Dialog :modal="props.overlay" :open="isOpen" @update:open="updateOpen">
|
||||
<DialogContent
|
||||
ref="dialogContentRef"
|
||||
:closable="props.closable"
|
||||
:overlay-blur="props.overlayBlur"
|
||||
:maximize="isMaximize"
|
||||
:maximizable="props.maximizable"
|
||||
:class="cn('flex flex-col p-0 gap-0 max-h-full sm:max-h-[90vh] duration-0', props.class, {
|
||||
'size-full max-w-full max-h-full sm:max-h-full': isMaximize,
|
||||
:class="cn('left-0 right-0 top-0 md:top-[5vh] flex flex-col p-0 gap-0 mx-auto h-[calc-size(auto,size)] min-h-full md:min-h-auto max-h-full md:max-h-[90vh] translate-x-0 translate-y-0', props.class, {
|
||||
'md:top-0 size-full max-w-full max-h-full md:max-h-full': isMaximize,
|
||||
'md:top-1/2 md:-translate-y-1/2!': props.alignCenter,
|
||||
'duration-0': isDragging,
|
||||
})"
|
||||
@close-auto-focus="handleFocusOutside"
|
||||
@focus-outside="handleFocusOutside"
|
||||
@@ -126,19 +167,24 @@ function handleAnimationEnd() {
|
||||
@animation-end="handleAnimationEnd"
|
||||
>
|
||||
<DialogHeader
|
||||
v-if="header" :class="cn('p-4', props.headerClass, {
|
||||
'border-b': props.bordered,
|
||||
v-if="header" ref="dialogHeaderRef" :class="cn('p-4', props.headerClass, {
|
||||
'cursor-move select-none': props.draggable,
|
||||
'border-b': props.border,
|
||||
})"
|
||||
>
|
||||
<VisuallyHidden v-if="!!slots.header">
|
||||
<DialogTitle />
|
||||
<DialogDescription />
|
||||
</VisuallyHidden>
|
||||
<slot name="header">
|
||||
<DialogTitle class="flex-center-start gap-x-2" :class="{ 'justify-center': props.centered }">
|
||||
<DialogTitle class="flex-center gap-x-2 md:justify-start" :class="{ 'md:justify-center': props.center }">
|
||||
<FaIcon
|
||||
v-if="props.icon" :name="{
|
||||
info: 'i-ant-design:info-circle-filled',
|
||||
success: 'i-ant-design:check-circle-filled',
|
||||
warning: 'i-ant-design:exclamation-circle-filled',
|
||||
error: 'i-ant-design:close-circle-filled',
|
||||
}[props.icon]" :size="24" :class="{
|
||||
}[props.icon]" class="size-6" :class="{
|
||||
'text-blue-600 dark:text-blue-400': props.icon === 'info',
|
||||
'text-green-600 dark:text-green-400': props.icon === 'success',
|
||||
'text-yellow-600 dark:text-yellow-400': props.icon === 'warning',
|
||||
@@ -147,25 +193,23 @@ function handleAnimationEnd() {
|
||||
/>
|
||||
{{ title }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="!!description" :class="{ 'text-center': props.centered }">
|
||||
<DialogDescription v-if="!!description" :class="{ 'text-center': props.center }">
|
||||
{{ description }}
|
||||
</DialogDescription>
|
||||
</slot>
|
||||
</DialogHeader>
|
||||
<div
|
||||
v-if="!!slots.default" :class="cn('relative min-h-40 flex-1 p-4 of-y-auto', props.contentClass, {
|
||||
'of-hidden': props.loading,
|
||||
})"
|
||||
>
|
||||
<slot />
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" :size="36" />
|
||||
<FaScrollArea v-if="!!slots.default" ref="dialogAreaRef" class="flex-1">
|
||||
<div :class="cn('min-h-40 p-4', props.contentClass)">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<DialogFooter
|
||||
v-if="footer" :class="cn('p-2 gap-y-2', props.footerClass, {
|
||||
'sm:justify-center': props.centered,
|
||||
'border-t': props.bordered,
|
||||
'md:justify-center': props.center,
|
||||
'border-t': props.border,
|
||||
})"
|
||||
>
|
||||
<slot name="footer">
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* fork: https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-draggable/index.ts
|
||||
* reference: https://github.com/vbenjs/vue-vben-admin/blob/main/packages/%40core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
|
||||
*/
|
||||
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { unrefElement } from '@vueuse/core'
|
||||
|
||||
export function useDraggable(
|
||||
targetRef: Ref<HTMLElement | undefined>,
|
||||
dragRef: Ref<HTMLElement | undefined>,
|
||||
draggable: ComputedRef<boolean>,
|
||||
overflow?: ComputedRef<boolean>,
|
||||
) {
|
||||
const isDragging = ref(false)
|
||||
const transform = reactive({
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
})
|
||||
|
||||
const onMousedown = (e: MouseEvent) => {
|
||||
const downX = e.clientX
|
||||
const downY = e.clientY
|
||||
const { offsetX, offsetY } = transform
|
||||
|
||||
const targetRect = targetRef.value!.getBoundingClientRect()
|
||||
const targetLeft = targetRect.left
|
||||
const targetTop = targetRect.top
|
||||
const targetWidth = targetRect.width
|
||||
const targetHeight = targetRect.height
|
||||
|
||||
const clientWidth = document.documentElement.clientWidth
|
||||
const clientHeight = document.documentElement.clientHeight
|
||||
|
||||
const minLeft = -targetLeft + offsetX
|
||||
const minTop = -targetTop + offsetY
|
||||
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX
|
||||
const maxTop = clientHeight - targetTop - targetHeight + offsetY
|
||||
|
||||
const onMousemove = (e: MouseEvent) => {
|
||||
let moveX = offsetX + e.clientX - downX
|
||||
let moveY = offsetY + e.clientY - downY
|
||||
|
||||
if (!overflow?.value) {
|
||||
moveX = Math.min(Math.max(moveX, minLeft), maxLeft)
|
||||
moveY = Math.min(Math.max(moveY, minTop), maxTop)
|
||||
}
|
||||
|
||||
transform.offsetX = moveX
|
||||
transform.offsetY = moveY
|
||||
|
||||
if (targetRef.value) {
|
||||
isDragging.value = true
|
||||
targetRef.value.style.transform = `translate(${moveX}px, ${moveY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseup = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', onMousemove)
|
||||
document.removeEventListener('mouseup', onMouseup)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMousemove)
|
||||
document.addEventListener('mouseup', onMouseup)
|
||||
}
|
||||
|
||||
const onDraggable = () => {
|
||||
const dragRefElement = unrefElement(dragRef)
|
||||
if (dragRefElement && targetRef.value) {
|
||||
dragRefElement.addEventListener('mousedown', onMousedown)
|
||||
}
|
||||
}
|
||||
|
||||
const offDraggable = () => {
|
||||
const dragRefElement = unrefElement(dragRef)
|
||||
if (dragRefElement && targetRef.value) {
|
||||
dragRefElement.removeEventListener('mousedown', onMousedown)
|
||||
}
|
||||
}
|
||||
|
||||
const resetPosition = () => {
|
||||
transform.offsetX = 0
|
||||
transform.offsetY = 0
|
||||
if (targetRef.value) {
|
||||
targetRef.value.style.transform = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
if (draggable.value) {
|
||||
onDraggable()
|
||||
}
|
||||
else {
|
||||
offDraggable()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
offDraggable()
|
||||
})
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
transform,
|
||||
resetPosition,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaPageHeader',
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
description?: string
|
||||
class?: HTMLAttributes['class']
|
||||
mainClass?: HTMLAttributes['class']
|
||||
defaultClass?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
@@ -16,8 +22,8 @@ const slots = defineSlots<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header mb-4 flex flex-wrap items-center justify-between gap-5 border-b bg-background px-5 py-4 transition-[background-color,border-color]">
|
||||
<div class="main flex-[1_1_70%]">
|
||||
<div :class="cn('mb-4 flex flex-wrap items-center justify-between gap-5 border-b bg-background px-5 py-4 transition-[background-color,border-color]', props.class)">
|
||||
<div :class="cn('flex-[1_1_70%]', props.mainClass)">
|
||||
<div class="text-2xl">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
@@ -29,7 +35,7 @@ const slots = defineSlots<{
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!!slots.default" class="ml-a flex-none">
|
||||
<div v-if="!!slots.default" :class="cn('ml-a flex-none', props.defaultClass)">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaPageMain',
|
||||
})
|
||||
@@ -8,6 +11,9 @@ const props = withDefaults(
|
||||
title?: string
|
||||
collaspe?: boolean
|
||||
height?: string
|
||||
class?: HTMLAttributes['class']
|
||||
titleClass?: HTMLAttributes['class']
|
||||
mainClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
{
|
||||
title: '',
|
||||
@@ -28,17 +34,21 @@ function handleCollaspe() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-main m-4 flex flex-col border rounded-lg bg-card transition-[background-color,border-color]" :class="{ 'overflow-hidden': collaspe }">
|
||||
<div v-if="!!slots.title || title" class="title-container border-b px-5 py-4 transition-border-color">
|
||||
<div
|
||||
:class="cn('m-4 flex flex-col border rounded-lg bg-card transition-[background-color,border-color]', {
|
||||
'overflow-hidden': collaspe,
|
||||
}, props.class)"
|
||||
>
|
||||
<div v-if="!!slots.title || title" :class="cn('border-b px-5 py-4 transition-border-color', props.titleClass)">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
class="main-container group relative h-[calc-size(auto,size)] p-5 transition-height after:(pointer-events-none absolute bottom-0 left-0 z-1 h-12 max-h-full w-full from-transparent to-[hsl(var(--card))] bg-gradient-to-b opacity-0 transition-opacity content-empty)" :class="{
|
||||
:class="cn('main-container group relative h-[calc-size(auto,size)] p-5 transition-height after:(pointer-events-none absolute bottom-0 left-0 z-1 h-12 max-h-full w-full from-transparent to-[hsl(var(--card))] bg-gradient-to-b opacity-0 transition-opacity content-empty)', {
|
||||
'overflow-hidden': collaspe,
|
||||
'after:(opacity-100)': isCollaspe,
|
||||
}" :style="{
|
||||
}, props.mainClass)" :style="{
|
||||
height: isCollaspe ? height : '',
|
||||
}"
|
||||
>
|
||||
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { ScrollArea, ScrollBar } from './scroll-area'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaScrollArea',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
horizontal?: boolean
|
||||
scrollbar?: boolean
|
||||
mask?: boolean
|
||||
gradientColor?: string
|
||||
class?: HTMLAttributes['class']
|
||||
contentClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
{
|
||||
horizontal: false,
|
||||
scrollbar: true,
|
||||
mask: false,
|
||||
gradientColor: 'hsl(var(--background))',
|
||||
},
|
||||
)
|
||||
|
||||
const scrollAreaRef = useTemplateRef('scrollAreaRef')
|
||||
|
||||
const arrivedState = ref<{
|
||||
left: boolean
|
||||
right: boolean
|
||||
top: boolean
|
||||
bottom: boolean
|
||||
}>()
|
||||
const showMaskStart = computed(() => {
|
||||
if (props.horizontal) {
|
||||
return !arrivedState.value?.left
|
||||
}
|
||||
return !arrivedState.value?.top
|
||||
})
|
||||
const showMaskEnd = computed(() => {
|
||||
if (props.horizontal) {
|
||||
return !arrivedState.value?.right
|
||||
}
|
||||
return !arrivedState.value?.bottom
|
||||
})
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
if (props.horizontal) {
|
||||
scrollAreaRef.value?.el?.viewportElement?.scrollBy({
|
||||
left: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
else {
|
||||
scrollAreaRef.value?.el?.viewportElement?.scrollBy({
|
||||
top: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const scrollContainerRef = useTemplateRef('scrollContainerRef')
|
||||
|
||||
onMounted(() => {
|
||||
const { arrivedState: arrivedStateValue } = useScroll(scrollAreaRef.value?.el?.viewportElement)
|
||||
watch(arrivedStateValue, (value) => {
|
||||
arrivedState.value = value
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
const { width, height } = useElementSize(scrollContainerRef.value)
|
||||
watch([width, height], () => {
|
||||
scrollAreaRef.value?.el?.viewportElement?.dispatchEvent(new Event('scroll'))
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
})
|
||||
|
||||
function scrollTo(scrollNumber: number) {
|
||||
if (props.horizontal) {
|
||||
scrollAreaRef.value?.el?.viewportElement?.scrollTo({
|
||||
left: scrollNumber,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
else {
|
||||
scrollAreaRef.value?.el?.viewportElement?.scrollTo({
|
||||
top: scrollNumber,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
ref: scrollAreaRef,
|
||||
scrollTo,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="scrollContainerRef" :class="cn('relative flex overflow-hidden after:(pointer-events-none absolute z-1 from-transparent to-[var(--mask-scroll-container-gradient-color)] opacity-0 transition-opacity content-empty) before:(pointer-events-none absolute z-1 from-transparent to-[var(--mask-scroll-container-gradient-color)] opacity-0 transition-opacity content-empty)', {
|
||||
'after:(bg-gradient-to-r rtl:bg-gradient-to-l h-full w-12 end-0) before:(bg-gradient-to-l rtl:bg-gradient-to-r h-full w-12 start-0)': props.horizontal,
|
||||
'after:(bg-gradient-to-b h-12 w-full bottom-0) before:(bg-gradient-to-t h-12 w-full)': !props.horizontal,
|
||||
'before:(opacity-100!)': props.mask && showMaskStart,
|
||||
'after:(opacity-100!)': props.mask && showMaskEnd,
|
||||
}, props.class)" :style="props.mask ? {
|
||||
'--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">
|
||||
<slot />
|
||||
<ScrollBar v-if="props.horizontal" orientation="horizontal" :class="{ 'opacity-0 pointer-events-none': !props.scrollbar }" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
type ScrollAreaRootProps,
|
||||
ScrollAreaViewport,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import ScrollBar from './ScrollBar.vue'
|
||||
|
||||
const props = defineProps<ScrollAreaRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
scrollbar?: boolean
|
||||
onWheel?: (event: WheelEvent) => void
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, onWheel, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const viewportRef = useTemplateRef('viewportRef')
|
||||
|
||||
defineExpose({
|
||||
el: viewportRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar :class="{ 'opacity-0 pointer-events-none': !props.scrollbar }" />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.scroll-area-viewport > div) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
import { ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical'
|
||||
&& 'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal'
|
||||
&& 'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
props.class)"
|
||||
>
|
||||
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
@@ -20,8 +20,9 @@ defineProps<{
|
||||
inset-inline: 50% 0;
|
||||
z-index: 1000;
|
||||
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
transition: width 0.3s, max-width 0.3s, transform 0.3s;
|
||||
padding-right: var(--scrollbar-width, 0);
|
||||
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
|
||||
transition: width 0.3s, max-width 0.3s, transform 0.3s;
|
||||
|
||||
&.top {
|
||||
top: var(--g-header-actual-height);
|
||||
@@ -31,25 +32,9 @@ defineProps<{
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
[data-app-width-mode="center"] & {
|
||||
max-width: calc(var(--g-app-width) - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
|
||||
[data-app-width-mode="center-max-width"] & {
|
||||
max-width: calc(var(--g-app-width) - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: translateX(50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2 * -1)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2 * -1));
|
||||
}
|
||||
|
||||
[data-mode="mobile"] & {
|
||||
width: 100% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: translateX(50%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaSystemInfo',
|
||||
@@ -10,27 +10,35 @@ const isShow = ref(false)
|
||||
const { pkg, lastBuildTime } = __SYSTEM_INFO__
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-system-info-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
hotkeys('command+i, ctrl+i', () => {
|
||||
isShow.value = true
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDrawer v-model="isShow" title="系统信息">
|
||||
<div class="px-4">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
<FaDrawer v-model="isShow" title="系统信息" :footer="false">
|
||||
<div v-if="pkg.version">
|
||||
<FaDivider>
|
||||
系统信息
|
||||
</FaDivider>
|
||||
<div class="text-center text-lg font-bold font-sans">
|
||||
{{ pkg.version }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>
|
||||
最后编译时间
|
||||
</h2>
|
||||
<div class="my-4 text-center text-lg font-sans">
|
||||
</FaDivider>
|
||||
<div class="text-center text-lg font-bold font-sans">
|
||||
{{ lastBuildTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
<div>
|
||||
<FaDivider>
|
||||
生产环境依赖
|
||||
</h2>
|
||||
<ul class="my-4 list-none text-sm">
|
||||
</FaDivider>
|
||||
<ul class="list-none text-sm">
|
||||
<li v-for="(val, key) in (pkg.dependencies as object)" :key="key" class="flex items-center justify-between rounded-lg px-2 py-1.5 hover-bg-secondary">
|
||||
<div class="font-bold">
|
||||
{{ key }}
|
||||
@@ -41,11 +49,11 @@ onMounted(() => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
<div>
|
||||
<FaDivider>
|
||||
开发环境依赖
|
||||
</h2>
|
||||
<ul class="my-4 list-none text-sm">
|
||||
</FaDivider>
|
||||
<ul class="list-none text-sm">
|
||||
<li v-for="(val, key) in (pkg.devDependencies as object)" :key="key" class="flex items-center justify-between rounded-lg px-2 py-1.5 hover-bg-secondary">
|
||||
<div class="font-bold">
|
||||
{{ key }}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import zhCN from 'element-plus/es/locale/lang/zh-cn.mjs'
|
||||
|
||||
// 跟随框架主题,暗黑模式下略有 bug
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
// 跟随框架主题
|
||||
const isSupprotColorMix = CSS.supports('color', 'color-mix(in srgb, #fff, #000)')
|
||||
if (isSupprotColorMix) {
|
||||
document.body.style.setProperty('--el-bg-color', 'hsl(var(--background))')
|
||||
document.body.style.setProperty('--el-color-primary', 'hsl(var(--primary))')
|
||||
document.body.style.setProperty('--el-color-white', 'hsl(var(--primary-foreground))')
|
||||
document.body.style.setProperty('--el-color-black', 'hsl(var(--primary-foreground))')
|
||||
for (let index = 1; index < 10; index++) {
|
||||
document.body.style.setProperty(`--el-color-primary-light-${index}`, `color-mix(in srgb, var(--el-color-primary), #fff ${index * 10}%)`)
|
||||
document.body.style.setProperty(`--el-color-primary-dark-${index}`, `color-mix(in srgb, var(--el-color-primary), #000 ${index * 10}%)`)
|
||||
}
|
||||
}
|
||||
watch(() => settingsStore.currentColorScheme, (val) => {
|
||||
if (val === 'light') {
|
||||
for (let index = 1; index < 10; index++) {
|
||||
document.body.style.setProperty(`--el-color-primary-light-${index}`, `color-mix(in hsl, hsl(var(--primary)), #fff ${index * 10}%)`)
|
||||
document.body.style.setProperty(`--el-color-primary-dark-${index}`, `color-mix(in hsl, hsl(var(--primary)), #000 ${index * 10}%)`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let index = 1; index < 10; index++) {
|
||||
document.body.style.setProperty(`--el-color-primary-light-${index}`, `color-mix(in hsl, hsl(var(--primary)), #000 ${index * 10}%)`)
|
||||
document.body.style.setProperty(`--el-color-primary-dark-${index}`, `color-mix(in hsl, hsl(var(--primary)), #fff ${index * 10}%)`)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -22,28 +22,22 @@ export default function useTabbar() {
|
||||
|
||||
function go(delta: number) {
|
||||
const tabId = getId()
|
||||
if (checkClose(tabId, false)) {
|
||||
router.go(delta)
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
router.go(delta)
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
|
||||
function replace(to: RouteLocationRaw) {
|
||||
const tabId = getId()
|
||||
if (checkClose(tabId, false)) {
|
||||
router.replace(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
router.replace(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
|
||||
function close(to: RouteLocationRaw) {
|
||||
const tabId = getId()
|
||||
if (checkClose(tabId, false)) {
|
||||
router.push(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
router.push(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
|
||||
function closeById(tabId = getId()) {
|
||||
|
||||
+18
-28
@@ -1,5 +1,4 @@
|
||||
<route lang="yaml">
|
||||
name: home
|
||||
meta:
|
||||
title: 主页
|
||||
icon: ant-design:home-twotone
|
||||
@@ -9,7 +8,7 @@ meta:
|
||||
const versionType = ref('basic')
|
||||
watch(versionType, (val) => {
|
||||
if (val === 'pro') {
|
||||
location.href = location.href.replace('basic-example', 'pro-example')
|
||||
location.href = `${location.origin}${location.pathname}`.replace('basic-example', 'pro-example')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,11 +65,10 @@ function open(url: string) {
|
||||
<div class="flex items-center gap-4">
|
||||
欢迎使用 Fantastic-admin
|
||||
<FaTabs
|
||||
v-model="versionType"
|
||||
:list="[
|
||||
v-model="versionType" :list="[
|
||||
{ label: '基础版', value: 'basic' },
|
||||
{ label: '专业版', value: 'pro' },
|
||||
]"
|
||||
]" class="-mb-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -106,7 +104,7 @@ function open(url: string) {
|
||||
</div>
|
||||
</FaPageHeader>
|
||||
<div class="w-full flex flex-col gap-4 px-4 xl-flex-row">
|
||||
<FaPageMain class="ecology">
|
||||
<FaPageMain class="m-0 flex-1" title-class="flex flex-wrap items-center justify-between gap-4">
|
||||
<template #title>
|
||||
<div class="title-info">
|
||||
<img src="https://cn.vuejs.org/logo.svg">
|
||||
@@ -129,12 +127,12 @@ function open(url: string) {
|
||||
</li>
|
||||
</ul>
|
||||
</FaPageMain>
|
||||
<FaPageMain class="ecology">
|
||||
<FaPageMain class="m-0 flex-1" title-class="flex flex-wrap items-center justify-between gap-4">
|
||||
<template #title>
|
||||
<div class="title-info">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.png">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg">
|
||||
<div>
|
||||
<h1 class="c-[#e60000]">
|
||||
<h1 class="c-[#41b883]">
|
||||
Fantastic-admin
|
||||
</h1>
|
||||
<h2>一款开箱即用的 Vue 中后台管理系统框架</h2>
|
||||
@@ -153,7 +151,7 @@ function open(url: string) {
|
||||
</ElCarousel>
|
||||
<ElImageViewer v-if="fantasticAdminInfo.imageVisible" :url-list="fantasticAdminInfo.data" :initial-index="fantasticAdminInfo.index" @close="fantasticAdminInfo.imageVisible = false" />
|
||||
</FaPageMain>
|
||||
<FaPageMain class="ecology">
|
||||
<FaPageMain class="m-0 flex-1" title-class="flex flex-wrap items-center justify-between gap-4">
|
||||
<template #title>
|
||||
<div class="title-info">
|
||||
<img src="https://one-step-admin.hurui.me/logo.png">
|
||||
@@ -186,27 +184,19 @@ function open(url: string) {
|
||||
text-emphasis-style: "❤";
|
||||
}
|
||||
|
||||
.ecology {
|
||||
--uno: flex-1 m-0;
|
||||
.title-info {
|
||||
--uno: flex items-center gap-4;
|
||||
|
||||
:deep(.title-container) {
|
||||
--uno: flex items-center justify-between flex-wrap gap-4;
|
||||
img {
|
||||
--uno: block w-12 h-12;
|
||||
}
|
||||
|
||||
.title-info {
|
||||
--uno: flex items-center gap-4;
|
||||
h1 {
|
||||
--uno: m-0 text-2xl;
|
||||
}
|
||||
|
||||
img {
|
||||
--uno: block w-12 h-12;
|
||||
}
|
||||
|
||||
h1 {
|
||||
--uno: m-0 text-2xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
--uno: m-0 text-base text-secondary-foreground/50 font-normal;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
--uno: m-0 text-base text-secondary-foreground/50 font-normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ const formType = ref<'login' | 'register' | 'resetPassword'>('login')
|
||||
</div>
|
||||
<div class="login-box">
|
||||
<div class="login-banner">
|
||||
<img src="@/assets/images/logo.png" class="absolute inset-s-4 inset-t-4 h-30px rounded ring ring-border">
|
||||
<img src="@/assets/images/logo.svg" class="absolute inset-s-4 inset-t-4 h-8 rounded">
|
||||
<img src="@/assets/images/login-banner.png" class="banner">
|
||||
</div>
|
||||
<div class="login-form flex-col-center">
|
||||
@@ -73,9 +73,9 @@ const formType = ref<'login' | 'register' | 'resetPassword'>('login')
|
||||
background:
|
||||
radial-gradient(closest-side, hsl(var(--border) / 10%) 30%, hsl(var(--primary) / 20%) 30%, hsl(var(--border) / 30%) 50%) no-repeat,
|
||||
radial-gradient(closest-side, hsl(var(--border) / 10%) 30%, hsl(var(--primary) / 20%) 30%, hsl(var(--border) / 30%) 50%) no-repeat;
|
||||
filter: blur(100px);
|
||||
background-position: 100% 100%, 0% 0%;
|
||||
background-size: 200vw 200vh;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
[data-mode="mobile"] {
|
||||
@@ -140,9 +140,9 @@ const formType = ref<'login' | 'register' | 'resetPassword'>('login')
|
||||
background:
|
||||
radial-gradient(closest-side, hsl(var(--border) / 10%) 30%, hsl(var(--primary) / 20%) 30%, hsl(var(--border) / 30%) 50%) no-repeat,
|
||||
radial-gradient(closest-side, hsl(var(--border) / 10%) 30%, hsl(var(--primary) / 20%) 30%, hsl(var(--border) / 30%) 50%) no-repeat;
|
||||
filter: blur(100px);
|
||||
background-position: 100% 100%, 0% 0%;
|
||||
background-size: 200vw 200vh;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
.banner {
|
||||
|
||||
@@ -39,8 +39,6 @@ export const lightTheme = {
|
||||
'--g-main-sidebar-menu-active-color': 'hsl(var(--primary-foreground))',
|
||||
// 次导航
|
||||
'--g-sub-sidebar-bg': 'hsl(var(--background))',
|
||||
'--g-sub-sidebar-logo-bg': 'hsl(var(--primary))',
|
||||
'--g-sub-sidebar-logo-color': 'hsl(var(--primary-foreground))',
|
||||
'--g-sub-sidebar-menu-color': 'hsl(var(--accent-foreground))',
|
||||
'--g-sub-sidebar-menu-hover-bg': 'hsl(var(--accent))',
|
||||
'--g-sub-sidebar-menu-hover-color': 'hsl(var(--accent-foreground))',
|
||||
@@ -99,8 +97,6 @@ export const darkTheme = {
|
||||
'--g-main-sidebar-menu-active-color': 'hsl(var(--accent-foreground))',
|
||||
// 次导航
|
||||
'--g-sub-sidebar-bg': 'hsl(var(--background))',
|
||||
'--g-sub-sidebar-logo-bg': 'hsl(var(--primary))',
|
||||
'--g-sub-sidebar-logo-color': 'hsl(var(--primary-foreground))',
|
||||
'--g-sub-sidebar-menu-color': 'hsl(var(--muted-foreground))',
|
||||
'--g-sub-sidebar-menu-hover-bg': 'hsl(var(--muted))',
|
||||
'--g-sub-sidebar-menu-hover-color': 'hsl(var(--muted-foreground))',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PresetOrFactory } from '@unocss/core'
|
||||
import type { Theme } from 'unocss/preset-uno'
|
||||
import { entriesToCss, toArray } from '@unocss/core'
|
||||
import presetLegacyCompat from '@unocss/preset-legacy-compat'
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify,
|
||||
@@ -115,6 +116,9 @@ export default defineConfig<Theme>({
|
||||
}),
|
||||
presetTypography(),
|
||||
presetScrollbar(),
|
||||
presetLegacyCompat({
|
||||
legacyColorSpace: true,
|
||||
}) as PresetOrFactory<Theme>,
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
|
||||
@@ -31,6 +31,7 @@ export default function createVitePlugins(mode: string, isBuild = false) {
|
||||
modernPolyfills: [
|
||||
'es.array.at',
|
||||
'es.array.find-last',
|
||||
'es.object.has-own',
|
||||
],
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user