mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 03:43:30 +08:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf16db603a | ||
|
|
e68d67d606 | ||
|
|
1bce49cce6 | ||
|
|
52c1bb4b58 | ||
|
|
1ea3345556 | ||
|
|
35d31a078e | ||
|
|
2b18c3f138 | ||
|
|
10c8401a88 | ||
|
|
b0ac06172e | ||
|
|
2a902473a8 | ||
|
|
f02298bf1b | ||
|
|
5fcd3fe5ed | ||
|
|
ee47369579 | ||
|
|
ce7477b10d | ||
|
|
609e77c159 | ||
|
|
db4aa406eb | ||
|
|
5bfb3c8fb0 | ||
|
|
5e59d12d73 | ||
|
|
097e700464 | ||
|
|
501e82dd44 | ||
|
|
dcad2c6ecf | ||
|
|
2b08e82c5e | ||
|
|
b533c5a040 | ||
|
|
33db1040be | ||
|
|
ebf61b0738 | ||
|
|
7daea6d605 | ||
|
|
1473a6c7e3 | ||
|
|
87f5b60211 | ||
|
|
6d58f1c2f3 | ||
|
|
abac8e0152 | ||
|
|
643d458ac2 | ||
|
|
c5217aabc1 | ||
|
|
b3b8947b62 | ||
|
|
ea929ac427 | ||
|
|
2100bbe7b0 | ||
|
|
39ba382fec | ||
|
|
e87c46ad0a | ||
|
|
a6e66c3f47 | ||
|
|
2080d0be5f | ||
|
|
62f489baf4 | ||
|
|
6f31b04ae8 | ||
|
|
cfb4f23f18 | ||
|
|
eff07c2833 | ||
|
|
7f98ab21b3 | ||
|
|
fcb7cb541d | ||
|
|
3ab394e772 | ||
|
|
75411af22f | ||
|
|
2909caaf0e |
Executable
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"mikestead.dotenv",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vue.volar",
|
||||
"antfu.unocss"
|
||||
]
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"eslint.useFlatConfig": true,
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"postcss",
|
||||
"scss",
|
||||
"vue"
|
||||
],
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
+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,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>
|
||||
|
||||
+2
-2
@@ -8,9 +8,9 @@
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"framework": "vite",
|
||||
"aliases": {
|
||||
"components": "@/ui/shadcn",
|
||||
"utils": "@/utils"
|
||||
"utils": "@/utils",
|
||||
"lib": "@/utils"
|
||||
}
|
||||
}
|
||||
|
||||
+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">
|
||||
|
||||
+52
-50
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "5.0.0-beta.2",
|
||||
"version": "5.2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -19,98 +19,100 @@
|
||||
"lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"postinstall": "simple-git-hooks",
|
||||
"taze": "taze minor -wIr",
|
||||
"commit": "git cz",
|
||||
"release": "bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vee-validate/zod": "^4.15.0",
|
||||
"@vueuse/components": "^12.3.0",
|
||||
"@vueuse/core": "^12.3.0",
|
||||
"@vueuse/integrations": "^12.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"@vueuse/components": "^13.0.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/integrations": "^13.0.0",
|
||||
"axios": "^1.8.4",
|
||||
"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.7",
|
||||
"eruda": "^3.4.1",
|
||||
"es-toolkit": "^1.31.0",
|
||||
"es-toolkit": "^1.34.1",
|
||||
"hotkeys-js": "^3.13.9",
|
||||
"lucide-vue-next": "^0.469.0",
|
||||
"lucide-vue-next": "^0.487.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"pinia": "^2.3.0",
|
||||
"qs": "^6.13.1",
|
||||
"radix-vue": "^1.9.12",
|
||||
"pinia": "^3.0.1",
|
||||
"qs": "^6.14.0",
|
||||
"reka-ui": "^2.2.0",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"vconsole": "^3.15.1",
|
||||
"vee-validate": "^4.15.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-sonner": "^1.3.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "3.12.1",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@iconify/json": "^2.2.291",
|
||||
"@antfu/eslint-config": "4.3.0",
|
||||
"@faker-js/faker": "^9.6.0",
|
||||
"@iconify/json": "^2.2.323",
|
||||
"@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",
|
||||
"@unocss/preset-legacy-compat": "^65.4.2",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@unocss/eslint-plugin": "^66.0.0",
|
||||
"@unocss/preset-legacy-compat": "^66.0.0",
|
||||
"@vitejs/plugin-legacy": "^6.0.2",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"boxen": "^8.0.1",
|
||||
"bumpp": "^9.9.2",
|
||||
"cz-git": "^1.11.0",
|
||||
"eslint": "^9.17.0",
|
||||
"bumpp": "^10.1.0",
|
||||
"cz-git": "^1.11.1",
|
||||
"eslint": "^9.23.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.5.2",
|
||||
"lint-staged": "^15.5.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"sass-embedded": "^1.83.1",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-recess-order": "^5.1.1",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"simple-git-hooks": "^2.12.1",
|
||||
"stylelint": "^16.17.0",
|
||||
"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.11.1",
|
||||
"svgo": "^3.3.2",
|
||||
"typescript": "^5.7.2",
|
||||
"unocss": "^0.65.3",
|
||||
"unocss-preset-animations": "^1.1.0",
|
||||
"unocss-preset-scrollbar": "^0.3.1",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-turbo-console": "^1.11.1",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-app-loading": "^0.3.0",
|
||||
"vite-plugin-archiver": "^0.1.1",
|
||||
"taze": "^19.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"unocss": "^66.0.0",
|
||||
"unocss-preset-animations": "^1.1.1",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-turbo-console": "^1.11.3",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.5",
|
||||
"vite-plugin-app-loading": "^0.3.1",
|
||||
"vite-plugin-archiver": "^0.1.2",
|
||||
"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-pages": "^0.32.4",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-pages": "^0.33.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vite-plugin-vue-meta-layouts": "^0.5.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged",
|
||||
|
||||
+3653
-3209
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 |
+64
-72
@@ -2,82 +2,74 @@ import { exec } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { lookupCollection, lookupCollections } from '@iconify/json'
|
||||
import { checkbox, confirm } from '@inquirer/prompts'
|
||||
import fs from 'fs-extra'
|
||||
import inquirer from 'inquirer'
|
||||
|
||||
async function generateIcons() {
|
||||
// 拿到全部图标集的原始数据
|
||||
const raw = await lookupCollections()
|
||||
// 拿到全部图标集的原始数据
|
||||
const raw = await lookupCollections()
|
||||
|
||||
let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8')
|
||||
lastChoose = JSON.parse(lastChoose)
|
||||
let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8')
|
||||
lastChoose = JSON.parse(lastChoose)
|
||||
|
||||
// 取出可使用的图标集数据用于 inquirer 选择,并按名称排序
|
||||
const collections = Object.entries(raw).map(([id, item]) => ({
|
||||
...item,
|
||||
id,
|
||||
})).sort((a, b) => a.name.localeCompare(b.name))
|
||||
// 取出可使用的图标集数据用于 inquirer 选择,并按名称排序
|
||||
const collections = Object.entries(raw).map(([id, item]) => ({
|
||||
...item,
|
||||
id,
|
||||
})).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
/**
|
||||
* 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用
|
||||
* (1) src/iconify/index.json 记录用户 inquirer 的交互信息
|
||||
* (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名
|
||||
* (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用
|
||||
*/
|
||||
inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
message: '请选择需要生成的图标集',
|
||||
name: 'collections',
|
||||
choices: collections.map(item => ({
|
||||
name: `${item.name} (${item.total} icons)`,
|
||||
value: item.id,
|
||||
})),
|
||||
default: lastChoose.collections,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'isOfflineUse',
|
||||
message: '是否需要离线使用',
|
||||
default: false,
|
||||
},
|
||||
]).then(async (answers) => {
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/index.json'),
|
||||
{
|
||||
collections: answers.collections,
|
||||
isOfflineUse: answers.isOfflineUse,
|
||||
},
|
||||
)
|
||||
|
||||
const outputDir = path.resolve(process.cwd(), 'public/icons')
|
||||
await fs.ensureDir(outputDir)
|
||||
await fs.emptyDir(outputDir)
|
||||
|
||||
const collectionsMeta: object[] = []
|
||||
for (const info of answers.collections) {
|
||||
const setData = await lookupCollection(info)
|
||||
|
||||
collectionsMeta.push({
|
||||
prefix: setData.prefix,
|
||||
info: setData.info,
|
||||
icons: Object.keys(setData.icons),
|
||||
})
|
||||
|
||||
const offlineFilePath = path.join(outputDir, `${info}-raw.json`)
|
||||
|
||||
if (answers.isOfflineUse) {
|
||||
await fs.writeJSON(offlineFilePath, setData)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/data.json'),
|
||||
collectionsMeta,
|
||||
)
|
||||
|
||||
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
|
||||
})
|
||||
/**
|
||||
* 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用
|
||||
* (1) src/iconify/index.json 记录用户 inquirer 的交互信息
|
||||
* (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名
|
||||
* (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用
|
||||
*/
|
||||
const answers = {
|
||||
collections: await checkbox({
|
||||
message: '请选择需要生成的图标集',
|
||||
choices: collections.map(item => ({
|
||||
name: `${item.name} (${item.total} icons)`,
|
||||
value: item.id,
|
||||
checked: lastChoose.collections.includes(item.id),
|
||||
})),
|
||||
}),
|
||||
isOfflineUse: await confirm({
|
||||
message: '是否需要离线使用',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
generateIcons()
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/index.json'),
|
||||
{
|
||||
collections: answers.collections,
|
||||
isOfflineUse: answers.isOfflineUse,
|
||||
},
|
||||
)
|
||||
|
||||
const outputDir = path.resolve(process.cwd(), 'public/icons')
|
||||
await fs.ensureDir(outputDir)
|
||||
await fs.emptyDir(outputDir)
|
||||
|
||||
const collectionsMeta: object[] = []
|
||||
for (const info of answers.collections) {
|
||||
const setData = await lookupCollection(info)
|
||||
|
||||
collectionsMeta.push({
|
||||
prefix: setData.prefix,
|
||||
info: setData.info,
|
||||
icons: Object.keys(setData.icons),
|
||||
})
|
||||
|
||||
const offlineFilePath = path.join(outputDir, `${info}-raw.json`)
|
||||
|
||||
if (answers.isOfflineUse) {
|
||||
await fs.writeJSON(offlineFilePath, setData)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/data.json'),
|
||||
collectionsMeta,
|
||||
)
|
||||
|
||||
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
|
||||
|
||||
@@ -47,6 +47,7 @@ onMounted(() => {
|
||||
<component :is="Component" v-if="isAuth" />
|
||||
<FaNotAllowed v-else />
|
||||
</RouterView>
|
||||
<FaBackToTop />
|
||||
<FaToast />
|
||||
<FaNotification />
|
||||
<FaSystemInfo />
|
||||
|
||||
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 |
@@ -46,8 +46,8 @@
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const appRadius = computed<number[]>({
|
||||
get() {
|
||||
return [settingsStore.settings.app.radius]
|
||||
},
|
||||
set(value) {
|
||||
settingsStore.settings.app.radius = value[0]
|
||||
},
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.menu.mode, (value) => {
|
||||
if (value === 'single') {
|
||||
menuStore.setActived(0)
|
||||
@@ -49,7 +58,7 @@ function handleCopy() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块" :footer="isSupported">
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块" :footer="isSupported" content-class="sm:min-w-md">
|
||||
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
|
||||
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
|
||||
</div>
|
||||
@@ -70,19 +79,7 @@ function handleCopy() {
|
||||
<div class="label">
|
||||
圆角系数
|
||||
</div>
|
||||
<div class="flex-center-start gap-1">
|
||||
<FaButton
|
||||
v-for="(item, index) in [
|
||||
{ label: 0, value: 0 },
|
||||
{ label: 0.25, value: 0.25 },
|
||||
{ label: 0.5, value: 0.5 },
|
||||
{ label: 0.75, value: 0.75 },
|
||||
{ label: 1, value: 1 },
|
||||
]" :key="index" :variant="settingsStore.settings.app.radius === item.value ? 'default' : 'outline'" size="sm" class="w-12" @click="settingsStore.settings.app.radius = (item.value as any)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</FaButton>
|
||||
</div>
|
||||
<FaSlider v-model="appRadius" :min="0" :max="1" :step="0.25" class="w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'">
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'Copyright',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="settingsStore.settings.copyright.enable" class="copyright">
|
||||
<span>Copyright</span>
|
||||
<FaIcon name="i-ri:copyright-line" :size="18" />
|
||||
<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>
|
||||
<span v-else>{{ settingsStore.settings.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.settings.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ settingsStore.settings.copyright.beian }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.copyright {
|
||||
--uno: flex items-center justify-center flex-wrap my-4 px-4 text-sm text-secondary-foreground/50;
|
||||
|
||||
span,
|
||||
a {
|
||||
--uno: px-1;
|
||||
}
|
||||
|
||||
a {
|
||||
--uno: text-center no-underline hover-text-secondary-foreground transition;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@ const { switchTo } = useMenu()
|
||||
<div class="header-container">
|
||||
<Logo class="title" />
|
||||
<component :is="useSlots('header-after-logo')" />
|
||||
<FaScrollArea horizontal :scrollbar="false" mask gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1">
|
||||
<FaScrollArea :scrollbar="false" mask horizontal 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">
|
||||
|
||||
@@ -54,6 +54,14 @@ onMounted(() => {
|
||||
</div>
|
||||
激活下一个主导航
|
||||
</li>
|
||||
<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' ? '⇧' : 'Shift' }}</FaKbd>
|
||||
<FaKbd>`</FaKbd>
|
||||
</div>
|
||||
激活上一个主导航
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
defineOptions({
|
||||
@@ -12,6 +13,25 @@ const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const { switchTo } = useMenu()
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('alt+`', (e) => {
|
||||
if (settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.mode)) {
|
||||
e.preventDefault()
|
||||
switchTo(menuStore.actived + 1 < menuStore.allMenus.length ? menuStore.actived + 1 : 0)
|
||||
}
|
||||
})
|
||||
hotkeys('alt+shift+`', (e) => {
|
||||
if (settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.mode)) {
|
||||
e.preventDefault()
|
||||
switchTo(menuStore.actived - 1 >= 0 ? menuStore.actived - 1 : menuStore.allMenus.length - 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('alt+`')
|
||||
hotkeys.unbind('alt+shift+`')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuInjection, MenuProps } from './types'
|
||||
import { cn } from '@/utils'
|
||||
import Item from './item.vue'
|
||||
import SubMenu from './sub.vue'
|
||||
import { rootMenuInjectionKey } from './types'
|
||||
@@ -158,10 +159,10 @@ provide(rootMenuInjectionKey, reactive({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full w-full flex flex-col of-hidden transition-all-300" :class="{
|
||||
'flex-row! w-auto!': isMenuPopup && props.mode === 'horizontal',
|
||||
:class="cn('h-full w-full flex flex-col of-hidden transition-all-300', {
|
||||
'flex-row w-auto': isMenuPopup && props.mode === 'horizontal',
|
||||
'py-1': props.mode === 'vertical',
|
||||
}"
|
||||
})"
|
||||
>
|
||||
<template v-for="item in menu" :key="item.path ?? JSON.stringify(item)">
|
||||
<template v-if="item.meta?.menu !== false">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SubMenuItemProps } from './types'
|
||||
import { cn } from '@/utils'
|
||||
import { rootMenuInjectionKey } from './types'
|
||||
|
||||
defineOptions({
|
||||
@@ -29,13 +30,6 @@ const isItemActive = computed(() => {
|
||||
return isActived.value && (!props.subMenu || rootMenu.isMenuPopup)
|
||||
})
|
||||
|
||||
// 缩进样式
|
||||
const indentStyle = computed(() => {
|
||||
return !rootMenu.isMenuPopup
|
||||
? `padding-left: ${20 * (props.level ?? 0)}px`
|
||||
: ''
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
ref: itemRef,
|
||||
})
|
||||
@@ -43,11 +37,11 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="itemRef" class="menu-item relative transition-all" :class="{
|
||||
ref="itemRef" :class="cn('menu-item relative transition-all', {
|
||||
'active': isItemActive,
|
||||
'py-1 px-2': (rootMenu.isMenuPopup && rootMenu.props.mode === 'vertical') || (rootMenu.isMenuPopup && level !== 0 && rootMenu.props.mode === 'horizontal') || !rootMenu.isMenuPopup,
|
||||
'px-1 py-2': rootMenu.isMenuPopup && level === 0 && rootMenu.props.mode === 'horizontal',
|
||||
}"
|
||||
})"
|
||||
>
|
||||
<router-link v-slot="{ href, navigate }" custom :to="uniqueKey.at(-1) ?? ''">
|
||||
<FaTooltip :disabled="!rootMenu.isMenuPopup || level !== 0 || subMenu" :text="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" :side="rootMenu.props.mode === 'vertical' ? 'right' : 'bottom'" class="h-full w-full">
|
||||
@@ -58,39 +52,40 @@ defineExpose({
|
||||
target: item.meta?.link ? '_blank' : '_self',
|
||||
class: 'no-underline',
|
||||
}),
|
||||
}" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-4 py-3 text-[var(--g-sub-sidebar-menu-color)] transition-all hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])" :class="{
|
||||
}" :class="cn('group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-4 py-3 text-[var(--g-sub-sidebar-menu-color)] transition-all hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])', {
|
||||
'text-[var(--g-sub-sidebar-menu-active-color)]! bg-[var(--g-sub-sidebar-menu-active-bg)]!': isItemActive,
|
||||
'px-3!': rootMenu.isMenuPopup && level === 0,
|
||||
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" v-on="{
|
||||
'px-3': rootMenu.isMenuPopup && level === 0,
|
||||
})" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" v-on="{
|
||||
...(!subMenu && {
|
||||
click: navigate,
|
||||
}),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="inline-flex flex-1 items-center justify-center gap-[12px]" :class="{
|
||||
:class="cn('inline-flex flex-1 items-center justify-center gap-[12px] pl-[calc(var(--indent-level)*20px)]', {
|
||||
'flex-col': rootMenu.isMenuPopup && level === 0 && rootMenu.props.mode === 'vertical',
|
||||
'gap-1!': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
|
||||
'gap-1': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
|
||||
'w-full': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'vertical',
|
||||
}" :style="indentStyle"
|
||||
})" :style="{
|
||||
'--indent-level': !rootMenu.isMenuPopup ? props.level ?? 0 : 0,
|
||||
}"
|
||||
>
|
||||
<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="{
|
||||
v-if="!(rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName)" :class="cn('w-0 flex-1 truncate text-sm transition-height transition-opacity transition-width', {
|
||||
'opacity-0 w-0 h-0': rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName,
|
||||
'w-full text-center': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
|
||||
}"
|
||||
})"
|
||||
>
|
||||
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="subMenu && !(rootMenu.isMenuPopup && level === 0)" class="relative ms-1 w-[10px] after:absolute before:absolute after:h-[1.5px] after:w-[6px] before:h-[1.5px] before:w-[6px] after:bg-current before:bg-current after:transition-transform-200 before:transition-transform-200 after:content-empty before:content-empty after:-translate-y-[1px] before:-translate-y-[1px]" :class="[
|
||||
expand ? 'before:-rotate-45 before:-translate-x-[2px] after:rotate-45 after:translate-x-[2px]' : 'before:rotate-45 before:-translate-x-[2px] after:-rotate-45 after:translate-x-[2px]',
|
||||
rootMenu.isMenuPopup && level === 0 && 'opacity-0',
|
||||
rootMenu.isMenuPopup && level !== 0 && '-rotate-90 -top-[1.5px]',
|
||||
]"
|
||||
v-if="subMenu && !(rootMenu.isMenuPopup && level === 0)" :class="cn('relative ms-1 w-[10px] after:absolute before:absolute after:h-[1.5px] after:w-[6px] before:h-[1.5px] before:w-[6px] after:bg-current before:bg-current after:transition-transform-200 before:transition-transform-200 after:content-empty before:content-empty after:-translate-y-[1px] before:-translate-y-[1px]', {
|
||||
[expand ? 'before:-rotate-45 before:-translate-x-[2px] after:rotate-45 after:translate-x-[2px]' : 'before:rotate-45 before:-translate-x-[2px] after:-rotate-45 after:translate-x-[2px]']: true,
|
||||
'opacity-0': rootMenu.isMenuPopup && level === 0,
|
||||
'-rotate-90 -top-[1.5px]': rootMenu.isMenuPopup && level !== 0,
|
||||
})"
|
||||
/>
|
||||
</component>
|
||||
</FaTooltip>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SubMenuProps } from './types'
|
||||
import { cn } from '@/utils'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import Item from './item.vue'
|
||||
import { rootMenuInjectionKey } from './types'
|
||||
@@ -45,40 +46,42 @@ const transitionEvent = computed(() => {
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
: {
|
||||
enter(el: HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
el.dataset.height = el.offsetHeight.toString()
|
||||
el.style.maxHeight = '0'
|
||||
void el.offsetHeight
|
||||
el.style.maxHeight = `${el.dataset.height}px`
|
||||
: CSS.supports('height', 'calc-size(auto, size)')
|
||||
? {}
|
||||
: {
|
||||
enter(el: HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
el.dataset.height = el.offsetHeight.toString()
|
||||
el.style.maxHeight = '0'
|
||||
void el.offsetHeight
|
||||
el.style.maxHeight = `${el.dataset.height}px`
|
||||
el.style.overflow = 'hidden'
|
||||
})
|
||||
},
|
||||
afterEnter(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
enterCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
beforeLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
})
|
||||
},
|
||||
afterEnter(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
enterCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
beforeLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
leave(el: HTMLElement) {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
leaveCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
},
|
||||
leave(el: HTMLElement) {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
leaveCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const transitionClass = computed(() => {
|
||||
@@ -93,25 +96,16 @@ const transitionClass = computed(() => {
|
||||
}
|
||||
: {
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: 'opacity-0 translate-y-4 scale-95 blur-4',
|
||||
enterFromClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
enterToClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveToClass: 'opacity-0 translate-y-4 scale-95 blur-4',
|
||||
leaveToClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
}
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
let flag = true
|
||||
if (props.menu.children) {
|
||||
if (props.menu.children.every((item: any) => item.meta?.menu === false)) {
|
||||
flag = false
|
||||
}
|
||||
}
|
||||
else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
return props.menu.children?.some((item: any) => item.meta?.menu !== false) ?? false
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
@@ -149,7 +143,7 @@ function handleMouseenter() {
|
||||
top = el.getBoundingClientRect().top + el.scrollTop
|
||||
left = el.getBoundingClientRect().left + el.getBoundingClientRect().width
|
||||
if (top + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
top = window.innerHeight - subMenuEl.offsetHeight
|
||||
top = Math.max(0, window.innerHeight - subMenuEl.offsetHeight)
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -197,12 +191,12 @@ function handleMouseleave() {
|
||||
<Teleport v-if="hasChildren" to="body" :disabled="!rootMenu.isMenuPopup">
|
||||
<Transition v-bind="transitionClass" v-on="transitionEvent">
|
||||
<FaScrollArea
|
||||
v-if="opened" ref="subMenuRef" :scrollbar="false" :mask="rootMenu.isMenuPopup" class="sub-menu static rounded-lg" :class="{
|
||||
v-if="opened" ref="subMenuRef" :scrollbar="false" :mask="rootMenu.isMenuPopup" :class="cn('sub-menu static h-[calc-size(auto,size)] rounded-lg', {
|
||||
'bg-[var(--g-sub-sidebar-bg)]': rootMenu.isMenuPopup,
|
||||
'border shadow-xl fixed! z-3000 w-[200px]': rootMenu.isMenuPopup,
|
||||
'border shadow-xl fixed z-3000 w-[200px]': rootMenu.isMenuPopup,
|
||||
'mx-1': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
|
||||
'py-1': rootMenu.isMenuPopup,
|
||||
}"
|
||||
})"
|
||||
>
|
||||
<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" />
|
||||
|
||||
@@ -43,38 +43,40 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="enableSidebar" class="sub-sidebar-container" :class="{
|
||||
'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',
|
||||
<Transition name="sub-sidebar">
|
||||
<div
|
||||
v-if="enableSidebar" class="sub-sidebar-container" :class="{
|
||||
'is-collapse': settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse,
|
||||
}"
|
||||
/>
|
||||
<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">
|
||||
<Menu
|
||||
:menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" :class="{
|
||||
'-mt-2': !['head', 'single'].includes(settingsStore.settings.menu.mode),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TransitionGroup>
|
||||
</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" />
|
||||
</FaButton>
|
||||
>
|
||||
<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="{
|
||||
single: settingsStore.settings.menu.mode === 'single',
|
||||
}"
|
||||
/>
|
||||
<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">
|
||||
<Menu
|
||||
:menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" :class="{
|
||||
'-mt-2': !((settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse) || ['head', 'single'].includes(settingsStore.settings.menu.mode)),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TransitionGroup>
|
||||
</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" class="size-4" />
|
||||
</FaButton>
|
||||
</div>
|
||||
<component :is="useSlots('sub-sidebar-bottom')" />
|
||||
</div>
|
||||
<component :is="useSlots('sub-sidebar-bottom')" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -94,7 +96,7 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
width: var(--g-sub-sidebar-collapse-width);
|
||||
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
&:not(.single) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -104,19 +106,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%;
|
||||
}
|
||||
@@ -160,4 +149,15 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
.sub-sidebar-y-end-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* 次侧边栏动画 */
|
||||
.sub-sidebar-enter-active,
|
||||
.sub-sidebar-leave-active {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.sub-sidebar-enter-from,
|
||||
.sub-sidebar-leave-to {
|
||||
transform: translateX(calc(var(--g-sub-sidebar-width) * -1));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,25 +8,29 @@ const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
// 面包屑备份
|
||||
let breadcrumbListBackup: any = []
|
||||
const breadcrumbList = computed(() => {
|
||||
const breadcrumbList = []
|
||||
if (route.name === 'reload') {
|
||||
return breadcrumbListBackup
|
||||
}
|
||||
const list = []
|
||||
if (settingsStore.settings.home.enable) {
|
||||
breadcrumbList.push({
|
||||
list.push({
|
||||
path: settingsStore.settings.home.fullPath,
|
||||
title: settingsStore.settings.home.title,
|
||||
})
|
||||
}
|
||||
if (route.meta.breadcrumbNeste) {
|
||||
route.meta.breadcrumbNeste.forEach((item) => {
|
||||
if (item.hide === false) {
|
||||
breadcrumbList.push({
|
||||
path: item.path,
|
||||
title: item.title,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return breadcrumbList
|
||||
route.matched.forEach((item) => {
|
||||
if (item.meta?.breadcrumb !== false) {
|
||||
list.push({
|
||||
path: item.path,
|
||||
title: item.meta?.title,
|
||||
})
|
||||
}
|
||||
})
|
||||
breadcrumbListBackup = list
|
||||
return list
|
||||
})
|
||||
|
||||
function pathCompile(path: string) {
|
||||
|
||||
@@ -37,12 +37,12 @@ function toggleColorScheme(event: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaButton variant="ghost" size="icon" @click="toggleColorScheme">
|
||||
<FaButton variant="ghost" size="icon" class="size-9" @click="toggleColorScheme">
|
||||
<FaIcon
|
||||
:name="{
|
||||
light: 'i-ri:sun-line',
|
||||
dark: 'i-ri:moon-line',
|
||||
}[settingsStore.currentColorScheme!]" :size="16"
|
||||
}[settingsStore.currentColorScheme!]" class="size-4"
|
||||
/>
|
||||
</FaButton>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const { isFullscreen, toggle } = useFullscreen()
|
||||
</script>
|
||||
|
||||
<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" />
|
||||
<FaButton v-if="settingsStore.mode === 'pc'" variant="ghost" size="icon" class="size-9" @click="toggle">
|
||||
<FaIcon :name="isFullscreen ? 'i-ri:fullscreen-exit-line' : 'i-ri:fullscreen-line'" class="size-4" />
|
||||
</FaButton>
|
||||
</template>
|
||||
|
||||
@@ -12,11 +12,11 @@ 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 h-9': settingsStore.mode === 'pc', 'size-9': 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">
|
||||
<FaKbd v-if="settingsStore.settings.navSearch.enableHotkeys" class="-me-1">
|
||||
{{ settingsStore.os === 'mac' ? '⌘' : 'Ctrl' }} K
|
||||
</FaKbd>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,10 @@ import type { Menu } from '@/types/global'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import { useFocus } from '@vueuse/core'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
defineOptions({
|
||||
@@ -18,6 +19,7 @@ const isShow = defineModel<boolean>({
|
||||
|
||||
const router = useRouter()
|
||||
const settingsStore = useSettingsStore()
|
||||
const routeStore = useRouteStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
interface listTypes {
|
||||
@@ -25,12 +27,11 @@ interface listTypes {
|
||||
icon?: string
|
||||
title?: string | (() => string)
|
||||
link?: string
|
||||
breadcrumb: {
|
||||
title?: string | (() => string)
|
||||
}[]
|
||||
}
|
||||
|
||||
const searchInput = ref('')
|
||||
const searchInputRef = useTemplateRef('searchInputRef')
|
||||
const { focused: searchInputFocused } = useFocus(searchInputRef)
|
||||
const sourceList = ref<listTypes[]>([])
|
||||
const actived = ref(-1)
|
||||
|
||||
@@ -57,18 +58,8 @@ const resultList = computed(() => {
|
||||
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
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (b.title?.includes(searchInput.value)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
if (routeStore.getRouteMatchedByPath(item.path).some((b) => {
|
||||
return typeof b.meta?.title === 'function' ? b.meta?.title().includes(searchInput.value) : b.meta?.title?.includes(searchInput.value)
|
||||
})) {
|
||||
flag = true
|
||||
}
|
||||
@@ -122,7 +113,7 @@ onUnmounted(() => {
|
||||
function initSourceList() {
|
||||
sourceList.value = []
|
||||
menuStore.allMenus.forEach((item) => {
|
||||
getSourceListByMenus(item.children)
|
||||
getSourceList(item.children)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,26 +124,18 @@ function hasChildren(item: Menu.recordRaw) {
|
||||
}
|
||||
return flag
|
||||
}
|
||||
function getSourceListByMenus(arr: Menu.recordRaw[], basePath?: string, icon?: string, breadcrumb?: { title?: string | (() => string) }[]) {
|
||||
function getSourceList(arr: Menu.recordRaw[], basePath?: string, icon?: string) {
|
||||
arr.forEach((item) => {
|
||||
if (item.meta?.menu !== false) {
|
||||
const breadcrumbTemp = cloneDeep(breadcrumb) || []
|
||||
if (item.children && hasChildren(item)) {
|
||||
breadcrumbTemp.push({
|
||||
title: item.meta?.title,
|
||||
})
|
||||
getSourceListByMenus(item.children, resolveRoutePath(basePath, item.path), item.meta?.icon ?? icon, breadcrumbTemp)
|
||||
getSourceList(item.children, resolveRoutePath(basePath, item.path), item.meta?.icon ?? icon)
|
||||
}
|
||||
else {
|
||||
breadcrumbTemp.push({
|
||||
title: item.meta?.title,
|
||||
})
|
||||
sourceList.value.push({
|
||||
path: resolveRoutePath(basePath, item.path),
|
||||
icon: item.meta?.icon ?? icon,
|
||||
title: item.meta?.title,
|
||||
link: item.meta?.link,
|
||||
breadcrumb: breadcrumbTemp,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -217,15 +200,15 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<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" @opened="searchInputFocused = true">
|
||||
<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" :size="18" class="text-foreground/30" />
|
||||
<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">
|
||||
<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.prevent="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" />
|
||||
<FaIcon name="i-carbon:close" class="size-4" @click="isShow = false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -234,16 +217,16 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
<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" />
|
||||
<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" :size="14" />
|
||||
<FaIcon name="i-ant-design:caret-up-filled" class="size-4" />
|
||||
</FaKbd>
|
||||
<FaKbd>
|
||||
<FaIcon name="i-ant-design:caret-down-filled" :size="14" />
|
||||
<FaIcon name="i-ant-design:caret-down-filled" class="size-4" />
|
||||
</FaKbd>
|
||||
<span>切换</span>
|
||||
</div>
|
||||
@@ -260,12 +243,12 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
<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 }" />
|
||||
<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) ?? '[ 无标题 ]' }}
|
||||
<Breadcrumb v-if="routeStore.getRouteMatchedByPath(item.path).length" class="truncate">
|
||||
<BreadcrumbItem v-for="(bc, bcIndex) in routeStore.getRouteMatchedByPath(item.path)" :key="bcIndex" class="text-xs">
|
||||
{{ (typeof bc.meta?.title === 'function' ? bc.meta?.title() : bc.meta?.title) ?? '[ 无标题 ]' }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
@@ -274,7 +257,7 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</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" />
|
||||
<FaIcon name="i-tabler:mood-smile" class="size-10" />
|
||||
<p class="m-2 text-base">
|
||||
输入你要搜索的导航
|
||||
</p>
|
||||
@@ -282,7 +265,7 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</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" />
|
||||
<FaIcon name="i-tabler:mood-empty" class="size-10" />
|
||||
<p class="m-2 text-base">
|
||||
没有找到你想要的
|
||||
</p>
|
||||
|
||||
@@ -1,22 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
defineOptions({
|
||||
name: 'PageReload',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const mainPage = useMainPage()
|
||||
|
||||
const isAnimating = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', (e) => {
|
||||
if (settingsStore.settings.toolbar.pageReload) {
|
||||
e.preventDefault()
|
||||
mainPage.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
isAnimating.value = true
|
||||
mainPage.reload()
|
||||
}
|
||||
|
||||
function handleCtrlClick() {
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaButton variant="ghost" size="icon" @click="handleClick" @animationend="isAnimating = false">
|
||||
<FaIcon name="i-iconoir:refresh-double" :size="16" :class="{ animation: isAnimating }" />
|
||||
</FaButton>
|
||||
<FaTooltip side="bottom" :disabled="settingsStore.os === 'mac'">
|
||||
<template #content>
|
||||
<div class="flex-col-center gap-2">
|
||||
<p>按住 <FaKbd>Ctrl</FaKbd> 键并点击</p>
|
||||
<p>可切换为浏览器原生刷新</p>
|
||||
</div>
|
||||
</template>
|
||||
<FaButton variant="ghost" size="icon" class="size-9" @click.exact="handleClick" @click.ctrl.exact="handleCtrlClick" @animationend="isAnimating = false">
|
||||
<FaIcon name="i-iconoir:refresh-double" class="size-4" :class="{ animation: isAnimating }" />
|
||||
</FaButton>
|
||||
</FaTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -13,7 +13,7 @@ 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>
|
||||
<component :is="useSlots('toolbar-start')" />
|
||||
<Breadcrumb v-if="settingsStore.settings.toolbar.breadcrumb" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
+5
-29
@@ -4,10 +4,7 @@ import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import BackTop from './components/BackTop/index.vue'
|
||||
import Copyright from './components/Copyright/index.vue'
|
||||
import Header from './components/Header/index.vue'
|
||||
import HotkeysIntro from './components/HotkeysIntro/index.vue'
|
||||
import MainSidebar from './components/MainSidebar/index.vue'
|
||||
@@ -25,9 +22,6 @@ const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const mainPage = useMainPage()
|
||||
const menu = useMenu()
|
||||
|
||||
// 头部当前实际高度
|
||||
const headerActualHeight = computed(() => {
|
||||
let actualHeight = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-header-height'))
|
||||
@@ -91,25 +85,6 @@ watch(() => routeInfo.path, () => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', (e) => {
|
||||
if (settingsStore.settings.toolbar.pageReload) {
|
||||
e.preventDefault()
|
||||
mainPage.reload()
|
||||
}
|
||||
})
|
||||
hotkeys('alt+`', (e) => {
|
||||
if (settingsStore.settings.menu.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
menu.switchTo(menuStore.actived + 1 < menuStore.allMenus.length ? menuStore.actived + 1 : 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
hotkeys.unbind('alt+`')
|
||||
})
|
||||
|
||||
const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
</script>
|
||||
|
||||
@@ -134,7 +109,7 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
<Topbar />
|
||||
<div class="main">
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<Transition :name="!settingsStore.isReloading ? 'slide-right' : ''" mode="out-in">
|
||||
<KeepAlive :include="keepAliveStore.list">
|
||||
<component :is="Component" v-show="!isLink" :key="route.fullPath" />
|
||||
</KeepAlive>
|
||||
@@ -142,7 +117,7 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
</RouterView>
|
||||
<LinkView v-if="isLink" />
|
||||
</div>
|
||||
<Copyright />
|
||||
<FaCopyright />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +128,6 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
</div>
|
||||
<AppSetting />
|
||||
</template>
|
||||
<BackTop />
|
||||
<component :is="useSlots('free-position')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -211,7 +185,9 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
transition: width 0.3s, transform 0.3s, box-shadow 0.3s, top 0.3s;
|
||||
|
||||
&:has(> .main-sidebar-container.main-sidebar-enter-active),
|
||||
&:has(> .main-sidebar-container.main-sidebar-leave-active) {
|
||||
&:has(> .main-sidebar-container.main-sidebar-leave-active),
|
||||
&:has(> .sub-sidebar-container.sub-sidebar-enter-active),
|
||||
&:has(> .sub-sidebar-container.sub-sidebar-leave-active) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
+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',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useTabbarStore from '@/store/modules/tabbar'
|
||||
|
||||
function getId(router: Router) {
|
||||
return router.currentRoute.value.fullPath
|
||||
}
|
||||
|
||||
function extendPush(router: Router) {
|
||||
const originalPush = router.push
|
||||
router.push = function (to: RouteLocationRaw) {
|
||||
const settingsStore = useSettingsStore(pinia)
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
const tabbarStore = useTabbarStore(pinia)
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === getId(router))
|
||||
tabbarStore.$patch({
|
||||
leaveIndex: index,
|
||||
})
|
||||
}
|
||||
return originalPush(to)
|
||||
}
|
||||
}
|
||||
|
||||
function extendGo(router: Router) {
|
||||
const originalGo = router.go
|
||||
router.go = function (delta: number) {
|
||||
const settingsStore = useSettingsStore(pinia)
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
const tabId = getId(router)
|
||||
const tabbarStore = useTabbarStore(pinia)
|
||||
originalGo(delta)
|
||||
if (delta < 0) {
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
}
|
||||
else {
|
||||
originalGo(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extendReplace(router: Router) {
|
||||
const originalReplace = router.replace
|
||||
router.replace = function (to: RouteLocationRaw) {
|
||||
const settingsStore = useSettingsStore(pinia)
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
const tabId = getId(router)
|
||||
const tabbarStore = useTabbarStore(pinia)
|
||||
return originalReplace(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
else {
|
||||
return originalReplace(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function setupExtensions(router: Router) {
|
||||
extendPush(router)
|
||||
extendGo(router)
|
||||
extendReplace(router)
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function setupRoutes(router: Router) {
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// 获取用户权限
|
||||
// 获取用户权限
|
||||
settingsStore.settings.app.enablePermission && await userStore.getPermissions()
|
||||
// 生成动态路由
|
||||
switch (settingsStore.settings.app.routeBaseOn) {
|
||||
@@ -73,19 +73,21 @@ function setupRoutes(router: Router) {
|
||||
// 注册并记录路由数据
|
||||
// 记录的数据会在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
|
||||
const removeRoutes: (() => void)[] = []
|
||||
routeStore.flatRoutes.forEach((route) => {
|
||||
routeStore.routes.forEach((route) => {
|
||||
if (!/^(?:https?:|mailto:|tel:)/.test(route.path)) {
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
}
|
||||
})
|
||||
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.flatSystemRoutes.forEach((route) => {
|
||||
routeStore.systemRoutes.forEach((route) => {
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
})
|
||||
}
|
||||
routeStore.setCurrentRemoveRoutes(removeRoutes)
|
||||
}
|
||||
catch {}
|
||||
catch {
|
||||
userStore.logout()
|
||||
}
|
||||
// 动态路由生成并注册后,重新进入当前路由
|
||||
next({
|
||||
path: to.path,
|
||||
@@ -133,7 +135,7 @@ function setupTitle(router: Router) {
|
||||
router.afterEach((to) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
|
||||
settingsStore.setTitle(to.meta.breadcrumbNeste?.at(-1)?.title ?? to.meta.title)
|
||||
settingsStore.setTitle(to.matched?.at(-1)?.meta?.title ?? to.meta.title)
|
||||
}
|
||||
else {
|
||||
settingsStore.setTitle(to.meta.title)
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { loadingFadeOut } from 'virtual:app-loading'
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import setupExtensions from './extensions'
|
||||
import setupGuards from './guards'
|
||||
// 路由相关数据
|
||||
import { constantRoutes, constantRoutesByFilesystem } from './routes'
|
||||
@@ -13,6 +13,7 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
setupGuards(router)
|
||||
setupExtensions(router)
|
||||
|
||||
router.isReady().then(() => {
|
||||
loadingFadeOut()
|
||||
|
||||
@@ -47,7 +47,7 @@ const isProfileShow = ref(false)
|
||||
</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 }}
|
||||
|
||||
+63
-78
@@ -1,9 +1,9 @@
|
||||
import type { Route } from '#/global'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { RouteRecordRaw, RouterMatcher } from 'vue-router'
|
||||
import apiApp from '@/api/modules/app'
|
||||
import { systemRoutes } from '@/router/routes'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
import { systemRoutes as systemRoutesRaw } from '@/router/routes'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import { createRouterMatcher } from 'vue-router'
|
||||
import useSettingsStore from './settings'
|
||||
|
||||
const useRouteStore = defineStore(
|
||||
@@ -13,79 +13,15 @@ const useRouteStore = defineStore(
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const isGenerate = ref(false)
|
||||
// 原始路由
|
||||
const routesRaw = ref<Route.recordMainRaw[]>([])
|
||||
// 文件系统原始路由
|
||||
const filesystemRoutesRaw = ref<RouteRecordRaw[]>([])
|
||||
// 已注册的路由,用于登出时删除路由
|
||||
const currentRemoveRoutes = ref<(() => void)[]>([])
|
||||
|
||||
// 将多层嵌套路由处理成两层,保留顶层和最子层路由,中间层级将被拍平
|
||||
function flatAsyncRoutes<T extends RouteRecordRaw>(route: T): T {
|
||||
if (route.children) {
|
||||
route.children = flatAsyncRoutesRecursive(route.children, [{
|
||||
path: route.path,
|
||||
title: route.meta?.title,
|
||||
icon: route.meta?.icon,
|
||||
hide: !route.meta?.breadcrumb && route.meta?.breadcrumb === false,
|
||||
}], route.path)
|
||||
}
|
||||
return route
|
||||
}
|
||||
function flatAsyncRoutesRecursive(routes: RouteRecordRaw[], breadcrumb: Route.breadcrumb[] = [], baseUrl = ''): RouteRecordRaw[] {
|
||||
const res: RouteRecordRaw[] = []
|
||||
routes.forEach((route) => {
|
||||
if (route.children) {
|
||||
const childrenBaseUrl = resolveRoutePath(baseUrl, route.path)
|
||||
const tmpBreadcrumb = cloneDeep(breadcrumb)
|
||||
tmpBreadcrumb.push({
|
||||
path: childrenBaseUrl,
|
||||
title: route.meta?.title,
|
||||
icon: route.meta?.icon,
|
||||
hide: !route.meta?.breadcrumb && route.meta?.breadcrumb === false,
|
||||
})
|
||||
const tmpRoute = cloneDeep(route)
|
||||
tmpRoute.path = childrenBaseUrl
|
||||
if (!tmpRoute.meta) {
|
||||
tmpRoute.meta = {}
|
||||
}
|
||||
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
|
||||
delete tmpRoute.children
|
||||
res.push(tmpRoute)
|
||||
const childrenRoutes = flatAsyncRoutesRecursive(route.children, tmpBreadcrumb, childrenBaseUrl)
|
||||
childrenRoutes.forEach((item) => {
|
||||
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
|
||||
if (res.some(v => v.path === item.path)) {
|
||||
res.forEach((v, i) => {
|
||||
if (v.path === item.path) {
|
||||
res[i] = item
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
res.push(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
const tmpRoute = cloneDeep(route)
|
||||
tmpRoute.path = resolveRoutePath(baseUrl, tmpRoute.path)
|
||||
// 处理面包屑导航
|
||||
const tmpBreadcrumb = cloneDeep(breadcrumb)
|
||||
tmpBreadcrumb.push({
|
||||
path: tmpRoute.path,
|
||||
title: tmpRoute.meta?.title,
|
||||
icon: tmpRoute.meta?.icon,
|
||||
hide: !tmpRoute.meta?.breadcrumb && tmpRoute.meta?.breadcrumb === false,
|
||||
})
|
||||
if (!tmpRoute.meta) {
|
||||
tmpRoute.meta = {}
|
||||
}
|
||||
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
|
||||
res.push(tmpRoute)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
// 扁平化路由(将三级及以上路由数据拍平成二级)
|
||||
const flatRoutes = computed(() => {
|
||||
// 实际路由
|
||||
const routes = computed(() => {
|
||||
const returnRoutes: RouteRecordRaw[] = []
|
||||
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
|
||||
if (routesRaw.value) {
|
||||
@@ -100,7 +36,12 @@ const useRouteStore = defineStore(
|
||||
})
|
||||
returnRoutes.push(...tmpRoutes)
|
||||
})
|
||||
returnRoutes.forEach(item => flatAsyncRoutes(item))
|
||||
returnRoutes.forEach((item) => {
|
||||
if (item.children) {
|
||||
item.children = deleteMiddleRouteComponent(item.children)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -108,16 +49,51 @@ const useRouteStore = defineStore(
|
||||
}
|
||||
return returnRoutes
|
||||
})
|
||||
const flatSystemRoutes = computed(() => {
|
||||
const routes = [...systemRoutes]
|
||||
routes.forEach(item => flatAsyncRoutes(item))
|
||||
// 系统路由
|
||||
const systemRoutes = computed(() => {
|
||||
const routes = [...systemRoutesRaw]
|
||||
routes.forEach((item) => {
|
||||
if (item.children) {
|
||||
item.children = deleteMiddleRouteComponent(item.children)
|
||||
}
|
||||
})
|
||||
return routes
|
||||
})
|
||||
// 删除路由中间层级对应的组件
|
||||
function deleteMiddleRouteComponent(routes: RouteRecordRaw[]) {
|
||||
const res: RouteRecordRaw[] = []
|
||||
routes.forEach((route) => {
|
||||
if (route.children?.length) {
|
||||
delete route.component
|
||||
route.children = deleteMiddleRouteComponent(route.children)
|
||||
}
|
||||
else {
|
||||
delete route.children
|
||||
}
|
||||
res.push(route)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 路由匹配器
|
||||
const routesMatcher = ref<RouterMatcher>()
|
||||
// 根据路径获取匹配的路由
|
||||
function getRouteMatchedByPath(path: string) {
|
||||
return routesMatcher.value?.resolve({ path }, undefined!)?.matched ?? []
|
||||
}
|
||||
|
||||
// 生成路由(前端生成)
|
||||
function generateRoutesAtFront(asyncRoutes: Route.recordMainRaw[]) {
|
||||
// 设置 routes 数据
|
||||
routesRaw.value = cloneDeep(asyncRoutes) as any
|
||||
// 创建路由匹配器
|
||||
const routes: RouteRecordRaw[] = []
|
||||
routesRaw.value.forEach((route) => {
|
||||
if (route.children) {
|
||||
routes.push(...route.children)
|
||||
}
|
||||
})
|
||||
routesMatcher.value = createRouterMatcher(routes, {})
|
||||
isGenerate.value = true
|
||||
}
|
||||
// 格式化后端路由数据
|
||||
@@ -146,6 +122,14 @@ const useRouteStore = defineStore(
|
||||
await apiApp.routeList().then((res) => {
|
||||
// 设置 routes 数据
|
||||
routesRaw.value = formatBackRoutes(res.data) as any
|
||||
// 创建路由匹配器
|
||||
const routes: RouteRecordRaw[] = []
|
||||
routesRaw.value.forEach((route) => {
|
||||
if (route.children) {
|
||||
routes.push(...route.children)
|
||||
}
|
||||
})
|
||||
routesMatcher.value = createRouterMatcher(routes, {})
|
||||
isGenerate.value = true
|
||||
}).catch(() => {})
|
||||
}
|
||||
@@ -174,8 +158,9 @@ const useRouteStore = defineStore(
|
||||
isGenerate,
|
||||
routesRaw,
|
||||
currentRemoveRoutes,
|
||||
flatRoutes,
|
||||
flatSystemRoutes,
|
||||
routes,
|
||||
systemRoutes,
|
||||
getRouteMatchedByPath,
|
||||
generateRoutesAtFront,
|
||||
generateRoutesAtBack,
|
||||
generateRoutesAtFilesystem,
|
||||
|
||||
@@ -87,6 +87,13 @@ const useSettingsStore = defineStore(
|
||||
break
|
||||
}
|
||||
|
||||
// 页面是否刷新
|
||||
const isReloading = ref(false)
|
||||
// 切换当前页面是否刷新
|
||||
function setIsReloading(value?: boolean) {
|
||||
isReloading.value = value ?? !isReloading.value
|
||||
}
|
||||
|
||||
// 页面标题
|
||||
const title = ref<RouteMeta['title']>()
|
||||
// 记录页面标题
|
||||
@@ -152,6 +159,8 @@ const useSettingsStore = defineStore(
|
||||
settings,
|
||||
currentColorScheme,
|
||||
os,
|
||||
isReloading,
|
||||
setIsReloading,
|
||||
title,
|
||||
setTitle,
|
||||
mode,
|
||||
|
||||
@@ -32,7 +32,7 @@ const useTabbarStore = defineStore(
|
||||
tabId,
|
||||
fullPath: route.fullPath,
|
||||
title: typeof meta?.title === 'function' ? meta.title() : meta?.title,
|
||||
icon: meta?.icon ?? meta?.breadcrumbNeste?.findLast(item => item.icon)?.icon,
|
||||
icon: meta?.icon ?? route.matched?.findLast(item => item.meta?.icon)?.meta?.icon,
|
||||
name: names,
|
||||
}
|
||||
if (leaveIndex.value >= 0) {
|
||||
|
||||
Vendored
+4
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -9,10 +10,12 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
FaAuth: typeof import('./../ui/components/FaAuth/index.vue')['default']
|
||||
FaAvatar: typeof import('./../ui/components/FaAvatar/index.vue')['default']
|
||||
FaBackToTop: typeof import('./../ui/components/FaBackToTop/index.vue')['default']
|
||||
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']
|
||||
FaCopyright: typeof import('./../ui/components/FaCopyright/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']
|
||||
@@ -31,6 +34,7 @@ declare module 'vue' {
|
||||
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']
|
||||
FaSlider: typeof import('./../ui/components/FaSlider/index.vue')['default']
|
||||
FaSmartFixedBlock: typeof import('./../ui/components/FaSmartFixedBlock/index.vue')['default']
|
||||
FaSwitch: typeof import('./../ui/components/FaSwitch/index.vue')['default']
|
||||
FaSystemInfo: typeof import('./../ui/components/FaSystemInfo/index.vue')['default']
|
||||
|
||||
Vendored
-1
@@ -1,4 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
// Auto generate by env-parse
|
||||
/**
|
||||
|
||||
Vendored
+6
-13
@@ -11,17 +11,17 @@ declare namespace Settings {
|
||||
interface app {
|
||||
/**
|
||||
* 颜色方案
|
||||
* @默认值 `''` 跟随系统
|
||||
* @可选值 `'light'` 明亮模式
|
||||
* @默认值 `'light'` 明亮模式
|
||||
* @可选值 `'dark'` 暗黑模式
|
||||
* @可选值 `''` 跟随系统
|
||||
*/
|
||||
colorScheme?: '' | 'light' | 'dark'
|
||||
colorScheme?: 'light' | 'dark' | ''
|
||||
/**
|
||||
* 圆角系数
|
||||
* @默认值 `0.5`
|
||||
* @可选值 `0` / `0.25` / `0.5` / `0.75` / `1`
|
||||
* @可选值 `0到1区间的任意值`
|
||||
*/
|
||||
radius?: 0 | 0.25 | 0.5 | 0.75 | 1
|
||||
radius?: number
|
||||
/**
|
||||
* 是否开启哀悼模式
|
||||
* @默认值 `false`
|
||||
@@ -74,7 +74,7 @@ declare namespace Settings {
|
||||
}
|
||||
interface layout {
|
||||
/**
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 992px 时自动切换为移动端展示
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 1024px 时自动切换为移动端展示
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableMobileAdaptation?: boolean
|
||||
@@ -251,7 +251,6 @@ declare module 'vue-router' {
|
||||
cache?: boolean | string | string[]
|
||||
noCache?: string | string[]
|
||||
link?: string
|
||||
breadcrumbNeste?: Route.breadcrumb[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +263,6 @@ declare namespace Route {
|
||||
}
|
||||
children: RouteRecordRaw[]
|
||||
}
|
||||
interface breadcrumb {
|
||||
path: string
|
||||
title?: string | (() => string)
|
||||
icon?: string
|
||||
hide: boolean
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Menu {
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { AvatarVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { AvatarRoot } from 'radix-vue'
|
||||
import { avatarVariant, type AvatarVariants } from '.'
|
||||
import { AvatarRoot } from 'reka-ui'
|
||||
import { avatarVariant } from '.'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
||||
import type { AvatarFallbackProps } from 'reka-ui'
|
||||
import { AvatarFallback } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
||||
import type { AvatarImageProps } from 'reka-ui'
|
||||
import { AvatarImage } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover">
|
||||
<slot />
|
||||
</AvatarImage>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Avatar } from './Avatar.vue'
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { buttonVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
@@ -7,22 +8,21 @@ export const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
@@ -16,7 +16,7 @@ const id = useId()
|
||||
|
||||
<template>
|
||||
<div class="flex-center-start gap-2">
|
||||
<Checkbox :id v-model:checked="value" :disabled />
|
||||
<Checkbox :id v-model="value" :disabled />
|
||||
<label :for="id" class="cursor-pointer text-sm">
|
||||
<slot />
|
||||
</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'radix-vue'
|
||||
import { ContextMenuRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'reka-ui'
|
||||
import { ContextMenuRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuRootProps>()
|
||||
const emits = defineEmits<ContextMenuRootEmits>()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuCheckboxItem,
|
||||
type ContextMenuCheckboxItemEmits,
|
||||
type ContextMenuCheckboxItemProps,
|
||||
ContextMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
|
||||
@@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</ContextMenuItemIndicator>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
type ContextMenuContentEmits,
|
||||
type ContextMenuContentProps,
|
||||
ContextMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuGroup, type ContextMenuGroupProps } from 'radix-vue'
|
||||
import type { ContextMenuGroupProps } from 'reka-ui'
|
||||
import { ContextMenuGroup } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuItem,
|
||||
type ContextMenuItemEmits,
|
||||
type ContextMenuItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
const emits = defineEmits<ContextMenuItemEmits>()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { ContextMenuLabel } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
@@ -15,10 +17,7 @@ const delegatedProps = computed(() => {
|
||||
<template>
|
||||
<ContextMenuLabel
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('px-2 py-1.5 text-sm font-semibold text-foreground',
|
||||
inset && 'pl-8', props.class,
|
||||
)"
|
||||
:class="cn('px-2 py-1.5 text-sm font-semibold text-foreground', inset && 'pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuLabel>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuPortal, type ContextMenuPortalProps } from 'radix-vue'
|
||||
import type { ContextMenuPortalProps } from 'reka-ui'
|
||||
import { ContextMenuPortal } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuPortalProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRadioGroupEmits, ContextMenuRadioGroupProps } from 'reka-ui'
|
||||
import {
|
||||
ContextMenuRadioGroup,
|
||||
type ContextMenuRadioGroupEmits,
|
||||
type ContextMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuRadioGroupProps>()
|
||||
const emits = defineEmits<ContextMenuRadioGroupEmits>()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuItemIndicator,
|
||||
ContextMenuRadioItem,
|
||||
type ContextMenuRadioItemEmits,
|
||||
type ContextMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuRadioItemEmits>()
|
||||
@@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</ContextMenuItemIndicator>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuSeparator,
|
||||
type ContextMenuSeparatorProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuSubEmits, ContextMenuSubProps } from 'reka-ui'
|
||||
import {
|
||||
ContextMenuSub,
|
||||
type ContextMenuSubEmits,
|
||||
type ContextMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuSubProps>()
|
||||
const emits = defineEmits<ContextMenuSubEmits>()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
ContextMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuSubTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuSubTrigger,
|
||||
type ContextMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuTrigger, type ContextMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import type { ContextMenuTriggerProps } from 'reka-ui'
|
||||
import { ContextMenuTrigger, useForwardProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuTriggerProps>()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function handleItemClick(item: { handle?: () => void }) {
|
||||
<ContextMenuTrigger as-child>
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent class="z-1050">
|
||||
<ContextMenuContent class="z-2050">
|
||||
<template v-if="!!slots.label">
|
||||
<ContextMenuLabel>
|
||||
<slot name="label" />
|
||||
@@ -59,7 +59,7 @@ function handleItemClick(item: { handle?: () => void }) {
|
||||
<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" :size="16" />
|
||||
<FaIcon v-if="v.icon" :name="v.icon" class="size-4" />
|
||||
</div>
|
||||
{{ v.label }}
|
||||
</ContextMenuItem>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaCopyright',
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="settingsStore.settings.copyright.enable" class="my-4 flex flex-wrap items-center justify-center px-4 text-sm text-secondary-foreground/50">
|
||||
<span class="px-1">Copyright</span>
|
||||
<FaIcon name="i-ri:copyright-line" class="size-5" />
|
||||
<span v-if="settingsStore.settings.copyright.dates" class="px-1">{{ 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" class="px-1 text-center no-underline transition hover-text-secondary-foreground">{{ settingsStore.settings.copyright.company }}</a>
|
||||
<span v-else class="px-1">{{ settingsStore.settings.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.settings.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="px-1 text-center no-underline transition hover-text-secondary-foreground">{{ settingsStore.settings.copyright.beian }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaDivider',
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
position?: 'start' | 'end'
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
@@ -20,7 +22,7 @@ const slots = defineSlots<{
|
||||
'before:(flex-basis-0)': position === 'start',
|
||||
'after:(flex-basis-0)': position === 'end',
|
||||
'gap-4': !!slots.default,
|
||||
})"
|
||||
}, props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,8 @@ const props = withDefaults(
|
||||
footer?: boolean
|
||||
closeOnClickOverlay?: boolean
|
||||
closeOnPressEscape?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
{
|
||||
@@ -73,6 +72,9 @@ const emits = defineEmits<{
|
||||
'cancel': []
|
||||
}>()
|
||||
|
||||
const id = useId()
|
||||
provide('DrawerId', id)
|
||||
|
||||
const isOpen = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
@@ -106,14 +108,16 @@ function handleFocusOutside(e: Event) {
|
||||
}
|
||||
|
||||
function handleClickOutside(e: Event) {
|
||||
if (!props.closeOnClickOverlay) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== id) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscapeKeyDown(e: KeyboardEvent) {
|
||||
if (!props.closeOnPressEscape) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +132,15 @@ function handleAnimationEnd() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sheet :modal="props.overlay" :open="isOpen" @update:open="updateOpen">
|
||||
<Sheet :modal="false" :open="isOpen" @update:open="updateOpen">
|
||||
<SheetContent
|
||||
:closable="props.closable"
|
||||
:open="isOpen"
|
||||
:overlay="props.overlay"
|
||||
:overlay-blur="props.overlayBlur"
|
||||
class="w-full flex flex-col gap-0 p-0"
|
||||
:class="cn('w-full flex flex-col gap-0 p-0', props.contentClass)"
|
||||
:side="props.side"
|
||||
@open-auto-focus="handleFocusOutside"
|
||||
@close-auto-focus="handleFocusOutside"
|
||||
@focus-outside="handleFocusOutside"
|
||||
@pointer-down-outside="handleClickOutside"
|
||||
@@ -150,7 +157,7 @@ function handleAnimationEnd() {
|
||||
<SheetTitle :class="{ 'text-center': props.centered }">
|
||||
{{ title }}
|
||||
</SheetTitle>
|
||||
<SheetDescription v-if="!!description" :class="{ 'text-center': props.centered }">
|
||||
<SheetDescription class="empty:hidden" :class="{ 'text-center': props.centered }">
|
||||
{{ description }}
|
||||
</SheetDescription>
|
||||
</slot>
|
||||
@@ -162,7 +169,7 @@ function handleAnimationEnd() {
|
||||
</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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
import type { DialogCloseProps } from 'reka-ui'
|
||||
import { DialogClose } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { SheetVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { type SheetVariants, sheetVariants } from '.'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { sheetVariants } from '.'
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
class?: HTMLAttributes['class']
|
||||
open?: boolean
|
||||
side?: SheetVariants['side']
|
||||
closable?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
}
|
||||
|
||||
@@ -37,15 +40,42 @@ const delegatedProps = computed(() => {
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
const showOverlay = computed(() => props.open && props.overlay)
|
||||
const isLocked = useScrollLock(document.body)
|
||||
watch(showOverlay, (val) => {
|
||||
if (val) {
|
||||
isLocked.value = true
|
||||
}
|
||||
else {
|
||||
isLocked.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const id = inject('DrawerId')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
:class="cn('fixed inset-0 z-2000 data-[state=closed]:animate-out data-[state=open]:animate-in bg-black/50 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0', {
|
||||
'backdrop-blur-sm': props.overlayBlur,
|
||||
})"
|
||||
/>
|
||||
<Transition
|
||||
v-bind="{
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: 'opacity-0',
|
||||
enterToClass: 'opacity-100',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0',
|
||||
}"
|
||||
:appear="true"
|
||||
>
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
:data-drawer-id="id"
|
||||
:class="cn('fixed inset-0 z-2000 data-[state=closed]:animate-out data-[state=open]:animate-in bg-black/50 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0', {
|
||||
'backdrop-blur-sm': props.overlayBlur,
|
||||
})"
|
||||
/>
|
||||
</Transition>
|
||||
<DialogContent
|
||||
:class="cn(sheetVariants({ side }), props.class)"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { DialogDescription, type DialogDescriptionProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { DialogDescription } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { DialogTitle, type DialogTitleProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { DialogTitle } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from 'reka-ui'
|
||||
import { DialogTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Sheet } from './Sheet.vue'
|
||||
export { default as SheetClose } from './SheetClose.vue'
|
||||
@@ -7,18 +8,19 @@ export { default as SheetDescription } from './SheetDescription.vue'
|
||||
export { default as SheetFooter } from './SheetFooter.vue'
|
||||
export { default as SheetHeader } from './SheetHeader.vue'
|
||||
export { default as SheetTitle } from './SheetTitle.vue'
|
||||
export { default as SheetTrigger } from './SheetTrigger.vue'
|
||||
|
||||
export const sheetVariants = cva(
|
||||
'fixed z-2000 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:animate-duration-300 data-[state=open]:animate-duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
top: 'inset-x-0 top-0 shadow-[0_1px_0_0_hsl(var(--border))] data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',
|
||||
'inset-x-0 bottom-0 shadow-[0_-1px_0_0_hsl(var(--border))] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 shadow-[1px_0_0_0_hsl(var(--border))] data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md',
|
||||
'inset-y-0 right-0 h-full w-3/4 shadow-[-1px_0_0_0_hsl(var(--border))] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui'
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue'
|
||||
import type { DropdownMenuGroupProps } from 'reka-ui'
|
||||
import { DropdownMenuGroup } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { DropdownMenuLabel, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from 'reka-ui'
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
type DropdownMenuRadioGroupEmits,
|
||||
type DropdownMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
type DropdownMenuSeparatorProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from 'reka-ui'
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
type DropdownMenuSubEmits,
|
||||
type DropdownMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import type { DropdownMenuTriggerProps } from 'reka-ui'
|
||||
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
|
||||
@@ -13,4 +13,4 @@ export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { DropdownMenuPortal } from 'radix-vue'
|
||||
export { DropdownMenuPortal } from 'reka-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentProps } from 'radix-vue'
|
||||
import type { DropdownMenuContentProps } from 'reka-ui'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user