mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 03:43:30 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57ce39bf98 | ||
|
|
5253eed72c | ||
|
|
6cae3ba328 | ||
|
|
5e4c1c20f4 | ||
|
|
aa5de0ebf0 | ||
|
|
632a2d389f | ||
|
|
a16f9232f9 | ||
|
|
ca25b1ae89 | ||
|
|
16a5779b1f | ||
|
|
0fe0df52d2 | ||
|
|
7b9dfe3eb8 | ||
|
|
4a52c99290 | ||
|
|
2d5e29eed7 | ||
|
|
263bc43e9a | ||
|
|
3c8cb20cdc | ||
|
|
efc54f49ef | ||
|
|
0b9d11fcfa | ||
|
|
f5593aeea3 | ||
|
|
9bfe2fb2d8 | ||
|
|
c19d749c61 | ||
|
|
2e904eb8e4 | ||
|
|
90e69b157a | ||
|
|
e086017689 | ||
|
|
3def174a9a | ||
|
|
06458467c5 | ||
|
|
9e9544bd70 | ||
|
|
18119a4e86 | ||
|
|
9e5df6a747 | ||
|
|
d390daeead | ||
|
|
6549b8ffbf | ||
|
|
2344fd78c1 | ||
|
|
5551c703b2 | ||
|
|
0e174e26ff | ||
|
|
b153bda743 | ||
|
|
43a85f5b39 | ||
|
|
ccb8bd9a18 | ||
|
|
cc9bb1a194 |
@@ -9,13 +9,13 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: latest
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: example
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
[中文](./README.md) | **English**
|
||||
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Fantastic-admin</h1>
|
||||
|
||||
<p align="center">A <b>out-of-the-box</b> Vue3 back-office management system framework</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fantastic-admin.hurui.me" target="_blank">Official Website</a>
|
||||
<span> | </span>
|
||||
<a href="https://fantastic-admin.pages.dev" target="_blank">Backup URL</a>
|
||||
<p>
|
||||
|
||||
<p align="center">
|
||||
<a href="###"><img src="https://img.shields.io/github/license/fantastic-admin/basic?label=License&style=flat-square" /></a>
|
||||
<a href="https://github.com/fantastic-admin/basic/releases" target="_blank"><img src="https://img.shields.io/github/v/release/fantastic-admin/basic?label=Version&style=flat-square" /></a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- Freely replaceable UI component library, default using Element Plus
|
||||
- Rich layouts and themes, covering various back-office application scenarios in the market, compatible with PC, tablet and mobile
|
||||
- Provides system configuration files for easy personalized customization
|
||||
- Automatically generates navigation bar based on route configuration
|
||||
- File system-based routing
|
||||
- Supports comprehensive permission verification
|
||||
- Built-in multi-level routing best caching solution
|
||||
- Easy internationalization and multi-language adaptation
|
||||
- Provides tab functionality with an experience close to native browser tab operations
|
||||
|
||||
## Download
|
||||
|
||||
> This repository is the basic version
|
||||
|
||||
**Directly pulling the source code may include unreleased content. It is recommended to download the stable version zip package from the [Github Releases](https://github.com/fantastic-admin/basic/releases) page**. If you are sure you need to pull the source code, please refer to the following branch descriptions:
|
||||
|
||||
- `main` Vue3 version framework source code branch, without example code, can be directly used for actual development
|
||||
- `example` Vue3 version demo source code branch, same as the online demo site, contains many examples, can be used for reference learning
|
||||
- ~~`vue2` Vue2 version framework source code branch, without example code, can be directly used for actual development~~ (discontinued)
|
||||
- ~~`vue2-example` Vue2 version demo source code branch, contains many examples, can be used for reference learning~~ (discontinued)
|
||||
|
||||
## Preview
|
||||
|
||||
> Preview screenshots are from Vue3 Professional version
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview3.png" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview4.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview5.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview6.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Support
|
||||
|
||||
If you think the Fantastic-admin framework is good, or you are already using it, I hope you can give me a ⭐ on **Github**, which will be a great encouragement to me.
|
||||
|
||||
[](https://github.com/fantastic-admin/basic)
|
||||
|
||||
<details>
|
||||
<summary>Github Stars Curve</summary>
|
||||
|
||||
[](https://starchart.cc/fantastic-admin/basic)
|
||||
</details>
|
||||
|
||||
## Ecosystem
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
<a href="https://hooray.github.io/fantastic-startkit/" target="_blank">Fantastic-startkit</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
A simple and easy-to-use Vue3 project startup kit
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
<a href="https://one-step-admin.hurui.me" target="_blank">One-step-admin</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
A Vue middle and back-office management system framework that's always one step ahead
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview3.png" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview4.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview5.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview6.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="4" align="center">
|
||||
<a href="https://fantastic-mobile.hurui.me" target="_blank">Fantastic-mobile</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" align="center">
|
||||
A unique mobile H5 framework
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview3.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview4.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1,3 +1,5 @@
|
||||
**中文** | [English](./README.EN.md)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" />
|
||||
</p>
|
||||
@@ -37,8 +39,8 @@
|
||||
|
||||
- `main` Vue3 版本框架源码分支,不含示例代码,可直接用于实际开发
|
||||
- `example` Vue3 版本演示源码分支,同线上演示站,包含大量示例,可用于参考学习
|
||||
- `vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发(停止维护)
|
||||
- `vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习(停止维护)
|
||||
- ~~`vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发~~(停止维护)
|
||||
- ~~`vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习~~(停止维护)
|
||||
|
||||
## 预览
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ export default antfu(
|
||||
ignores: [
|
||||
'public',
|
||||
'dist*',
|
||||
'src/ui/shadcn/ui',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+52
-50
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "5.5.0",
|
||||
"version": "5.7.0",
|
||||
"packageManager": "pnpm@10.16.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
@@ -25,94 +26,95 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/components": "^13.4.0",
|
||||
"@vueuse/core": "^13.4.0",
|
||||
"@vueuse/integrations": "^13.4.0",
|
||||
"axios": "^1.10.0",
|
||||
"@vueuse/components": "^13.9.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/integrations": "^13.9.0",
|
||||
"axios": "^1.12.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"defu": "^6.1.4",
|
||||
"disable-devtool": "^0.3.8",
|
||||
"element-plus": "^2.10.2",
|
||||
"disable-devtool": "^0.3.9",
|
||||
"element-plus": "^2.11.2",
|
||||
"eruda": "^3.4.3",
|
||||
"es-toolkit": "^1.39.5",
|
||||
"hotkeys-js": "^3.13.14",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"es-toolkit": "^1.39.10",
|
||||
"filesize": "^11.0.2",
|
||||
"hotkeys-js": "^3.13.15",
|
||||
"lucide-vue-next": "^0.544.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"pinia": "^3.0.3",
|
||||
"qs": "^6.14.0",
|
||||
"reka-ui": "^2.3.1",
|
||||
"reka-ui": "^2.5.0",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ua-parser-js": "^2.0.4",
|
||||
"ua-parser-js": "^2.0.5",
|
||||
"vconsole": "^3.15.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.17",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-sonner": "^2.0.1",
|
||||
"zod": "^3.25.67"
|
||||
"vue-sonner": "^2.0.8",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.16.1",
|
||||
"@antfu/eslint-config": "^5.3.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"@iconify/json": "^2.2.353",
|
||||
"@faker-js/faker": "^10.0.0",
|
||||
"@iconify/json": "^2.2.383",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@stylistic/stylelint-config": "^2.0.0",
|
||||
"@stylistic/stylelint-config": "^3.0.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@unocss/eslint-plugin": "^66.3.2",
|
||||
"@unocss/preset-legacy-compat": "^66.3.2",
|
||||
"@vitejs/plugin-legacy": "^7.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"@unocss/eslint-plugin": "^66.5.1",
|
||||
"@unocss/preset-legacy-compat": "^66.5.1",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"boxen": "^8.0.1",
|
||||
"bumpp": "^10.2.0",
|
||||
"cz-git": "^1.11.2",
|
||||
"eslint": "^9.30.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"bumpp": "^10.2.3",
|
||||
"cz-git": "^1.12.0",
|
||||
"eslint": "^9.35.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"http-server": "^14.1.1",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^16.1.6",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"picocolors": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"plop": "^4.0.2",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"stylelint": "^16.21.0",
|
||||
"stylelint-config-recess-order": "^7.1.0",
|
||||
"stylelint-config-standard-scss": "^15.0.1",
|
||||
"sass-embedded": "^1.92.1",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"stylelint": "^16.24.0",
|
||||
"stylelint-config-recess-order": "^7.3.0",
|
||||
"stylelint-config-standard-scss": "^16.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-scss": "^6.12.1",
|
||||
"svgo": "^4.0.0",
|
||||
"taze": "^19.1.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^66.3.2",
|
||||
"taze": "^19.6.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2",
|
||||
"unocss": "^66.5.1",
|
||||
"unocss-preset-animations": "^1.2.1",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-turbo-console": "^2.1.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.0",
|
||||
"unplugin-auto-import": "^20.1.0",
|
||||
"unplugin-turbo-console": "^2.2.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vite": "^7.1.5",
|
||||
"vite-plugin-app-loading": "^0.4.0",
|
||||
"vite-plugin-archiver": "^0.2.0",
|
||||
"vite-plugin-banner": "^0.8.1",
|
||||
"vite-plugin-compression2": "^2.2.0",
|
||||
"vite-plugin-compression2": "^2.2.1",
|
||||
"vite-plugin-env-parse": "^1.0.15",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-pages": "^0.33.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vite-plugin-vue-meta-layouts": "^0.5.1",
|
||||
"vue-tsc": "^2.2.10"
|
||||
"vite-plugin-vue-devtools": "^8.0.2",
|
||||
"vite-plugin-vue-meta-layouts": "^0.6.0",
|
||||
"vue-tsc": "^3.0.7"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged",
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: 'src/store/{{camelCase name}}.ts',
|
||||
path: 'src/store/modules/{{camelCase name}}.ts',
|
||||
templateFile: 'plop-templates/store/index.hbs',
|
||||
},
|
||||
]
|
||||
|
||||
Generated
+7071
-1980
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,9 @@ const answers = await p.group(
|
||||
},
|
||||
)
|
||||
|
||||
const spinner = p.spinner()
|
||||
spinner.start('正在生成图标集...')
|
||||
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/index.json'),
|
||||
{
|
||||
@@ -101,5 +104,5 @@ eslint.on('close', (code) => {
|
||||
p.log.error(`ESLint 执行失败,退出码: ${code}`)
|
||||
process.exit(code)
|
||||
}
|
||||
p.outro('图标集生成完成!')
|
||||
spinner.stop('图标集生成完成!')
|
||||
})
|
||||
|
||||
+27
-27
@@ -44,21 +44,22 @@ api.interceptors.request.use(
|
||||
function handleError(error: any) {
|
||||
if (error.status === 401) {
|
||||
useUserStore().requestLogout()
|
||||
throw error
|
||||
}
|
||||
let message = error.message
|
||||
if (message === 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
else {
|
||||
let message = error.message
|
||||
if (message === 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
}
|
||||
else if (message.includes('timeout')) {
|
||||
message = '接口请求超时'
|
||||
}
|
||||
else if (message.includes('Request failed with status code')) {
|
||||
message = `接口${message.substr(message.length - 3)}异常`
|
||||
}
|
||||
toast.error('Error', {
|
||||
description: message,
|
||||
})
|
||||
}
|
||||
else if (message.includes('timeout')) {
|
||||
message = '接口请求超时'
|
||||
}
|
||||
else if (message.includes('Request failed with status code')) {
|
||||
message = `接口${message.substr(message.length - 3)}异常`
|
||||
}
|
||||
toast.error('Error', {
|
||||
description: message,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -70,42 +71,41 @@ api.interceptors.response.use(
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error !== '') {
|
||||
toast.warning('Warning', {
|
||||
description: response.data.error,
|
||||
})
|
||||
return Promise.reject(response.data)
|
||||
if (typeof response.data === 'object') {
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error !== '') {
|
||||
toast.warning('Warning', {
|
||||
description: response.data.error,
|
||||
})
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
useUserStore().requestLogout()
|
||||
}
|
||||
return Promise.resolve(response.data)
|
||||
}
|
||||
else {
|
||||
useUserStore().requestLogout()
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
async (error) => {
|
||||
// 获取请求配置
|
||||
const config = error.config
|
||||
|
||||
// 如果配置不存在或未启用重试,则直接处理错误
|
||||
if (!config || !config.retry) {
|
||||
return handleError(error)
|
||||
}
|
||||
|
||||
// 设置重试次数
|
||||
config.retryCount = config.retryCount || 0
|
||||
|
||||
// 判断是否超过重试次数
|
||||
if (config.retryCount >= MAX_RETRY_COUNT) {
|
||||
return handleError(error)
|
||||
}
|
||||
|
||||
// 重试次数自增
|
||||
config.retryCount += 1
|
||||
|
||||
// 延迟重试
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
|
||||
|
||||
// 重新发起请求
|
||||
return api(config)
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'RegisterForm',
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'FileUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
max?: number
|
||||
files?: UploadUserFile[]
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
max: 3,
|
||||
files: () => [],
|
||||
notip: false,
|
||||
ext: () => ['zip', 'rar'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
file: UploadUserFile,
|
||||
fileList: UploadUserFile[],
|
||||
]
|
||||
onRemove: [
|
||||
file: UploadUserFile,
|
||||
fileList: UploadUserFile[],
|
||||
]
|
||||
}>()
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1) ?? ''
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
toast.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
toast.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
|
||||
const onExceed: UploadProps['onExceed'] = () => {
|
||||
toast.warning('文件上传超过限制')
|
||||
}
|
||||
|
||||
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
emits('onSuccess', res, file, fileList)
|
||||
}
|
||||
|
||||
const onRemove: UploadProps['onRemove'] = (file, fileList) => {
|
||||
emits('onRemove', file, fileList)
|
||||
}
|
||||
|
||||
const onPreview: UploadProps['onPreview'] = (e) => {
|
||||
const getBlob = (url: string) => new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
getBlob(e.url!).then((blob: any) => {
|
||||
const a = document.createElement('a')
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const event = new MouseEvent('click')
|
||||
a.target = '_blank'
|
||||
a.download = e.name
|
||||
a.href = url
|
||||
a.dispatchEvent(event)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElUpload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:on-remove="onRemove"
|
||||
:on-preview="onPreview"
|
||||
:http-request="httpRequest"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<FaIcon name="i-ep:upload-filled" class="el-icon--upload" />
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-upload.is-drag) {
|
||||
display: inline-block;
|
||||
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'ImageUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
width?: number
|
||||
height?: number
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
width: 150,
|
||||
height: 150,
|
||||
placeholder: '',
|
||||
notip: false,
|
||||
ext: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview() {
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove() {
|
||||
url.value = ''
|
||||
}
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1) ?? ''
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
toast.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emits('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<ElUpload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<ElImage v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<template #error>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
<div v-else class="image">
|
||||
<ElImage :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span @click.stop="preview">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span @click.stop="remove">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,278 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'ImagesUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
max?: number
|
||||
width?: number
|
||||
height?: number
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
max: 3,
|
||||
width: 150,
|
||||
height: 150,
|
||||
placeholder: '',
|
||||
notip: false,
|
||||
ext: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string[]>({
|
||||
default: [],
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview(index: number) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove(index: number) {
|
||||
url.value.splice(index, 1)
|
||||
}
|
||||
// 移动
|
||||
function move(index: number, type: 'left' | 'right') {
|
||||
if (type === 'left' && index !== 0) {
|
||||
url.value[index] = url.value.splice(index - 1, 1, url.value[index])[0]
|
||||
}
|
||||
if (type === 'right' && index !== url.value.length - 1) {
|
||||
url.value[index] = url.value.splice(index + 1, 1, url.value[index])[0]
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1) ?? ''
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
toast.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emits('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in (url as string[])" :key="index" class="images">
|
||||
<ElImage v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span @click="preview(index)">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span @click="remove(index)">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<FaIcon name="i-ep:back" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<FaIcon name="i-ep:right" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElUpload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.images {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-inline-end: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -43,7 +43,7 @@ const isMenuPopup = computed<MenuInjection['isMenuPopup']>(() => {
|
||||
function initItems(menu: MenuProps['menu'], parentPaths: string[] = []) {
|
||||
menu.forEach((item) => {
|
||||
const index = item.path ?? getUseId(item)
|
||||
if (item.children) {
|
||||
if (item.children?.some(item => item.meta?.menu !== false)) {
|
||||
const indexPath = [...parentPaths, index]
|
||||
subMenus.value[index] = {
|
||||
index,
|
||||
|
||||
@@ -103,6 +103,34 @@ function contextMenuItems(routeItem: Tabbar.recordRaw) {
|
||||
]
|
||||
}
|
||||
|
||||
const visibleTabIndex = ref<number[]>([])
|
||||
function getVisibleTabs() {
|
||||
const containerWidth = tabsRef.value?.ref?.$el.clientWidth ?? 0
|
||||
const scrollLeft = tabsRef.value?.ref?.el?.viewportElement?.scrollLeft ?? 0
|
||||
visibleTabIndex.value = []
|
||||
if (tabRef.value) {
|
||||
for (let i = 0; i < tabRef.value.length; i++) {
|
||||
const tab = tabRef.value[i]
|
||||
const tabLeft = tab.offsetLeft
|
||||
const tabRight = tabLeft + tab.offsetWidth
|
||||
// 检查标签页是否在可视区域内
|
||||
if (tabLeft < scrollLeft + containerWidth && tabRight > scrollLeft) {
|
||||
if (i >= 0 && i < tabbarStore.list.length) {
|
||||
visibleTabIndex.value.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getVisibleTabIndex(arrayIndex: number) {
|
||||
return visibleTabIndex.value.findIndex(visibleTab => visibleTab === arrayIndex) ?? -1
|
||||
}
|
||||
watch(() => keys.alt, (val) => {
|
||||
if (val) {
|
||||
getVisibleTabs()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('alt+left,alt+right,alt+w,alt+1,alt+2,alt+3,alt+4,alt+5,alt+6,alt+7,alt+8,alt+9,alt+0', (e, handle) => {
|
||||
if (settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys) {
|
||||
@@ -138,7 +166,9 @@ onMounted(() => {
|
||||
case 'alt+9':
|
||||
{
|
||||
const number = Number(handle.key.split('+')[1])
|
||||
tabbarStore.list[number - 1]?.fullPath && router.push(tabbarStore.list[number - 1].fullPath)
|
||||
if (visibleTabIndex.value[number - 1] !== undefined) {
|
||||
router.push(tabbarStore.list[visibleTabIndex.value[number - 1]].fullPath)
|
||||
}
|
||||
break
|
||||
}
|
||||
// 切换到最后一个标签页
|
||||
@@ -179,8 +209,8 @@ onUnmounted(() => {
|
||||
<div v-if="tabbarStore.list.length > 1" class="action-icon" @click.stop="tabbar.closeById(element.tabId)">
|
||||
<FaIcon name="i-ri:close-fill" />
|
||||
</div>
|
||||
<div v-show="keys.alt && index < 9" class="hotkey-number">
|
||||
{{ index + 1 }}
|
||||
<div v-show="keys.alt && getVisibleTabIndex(index) >= 0 && getVisibleTabIndex(index) < 9" class="hotkey-number">
|
||||
{{ getVisibleTabIndex(index) + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<template #content>
|
||||
|
||||
@@ -66,13 +66,15 @@ declare module 'vue-router' {
|
||||
|
||||
function extendClose(router: Router) {
|
||||
router.close = function (to: RouteLocationRaw) {
|
||||
const settingsStore = useSettingsStore(pinia)
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
const tabId = getId(router)
|
||||
const tabbarStore = useTabbarStore(pinia)
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
return router.push(to)
|
||||
const currentRoute = router.currentRoute.value
|
||||
const tabId = getId(router)
|
||||
return router.push(to).then(() => {
|
||||
const settingsStore = useSettingsStore(pinia)
|
||||
if (settingsStore.settings.tabbar.enable && currentRoute.meta.tabMerge !== 'activeMenu') {
|
||||
const tabbarStore = useTabbarStore(pinia)
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-45
@@ -160,54 +160,49 @@ function setupTitle(router: Router) {
|
||||
|
||||
// 页面缓存
|
||||
function setupKeepAlive(router: Router) {
|
||||
router.afterEach((to, from) => {
|
||||
router.afterEach(async (to, from) => {
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
|
||||
if (to.meta.cache) {
|
||||
const componentName = to.matched.at(-1)?.components?.default.name
|
||||
if (componentName) {
|
||||
keepAliveStore.add(componentName)
|
||||
}
|
||||
else {
|
||||
// turbo-console-disable-next-line
|
||||
console.warn('[Fantastic-admin] 该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
}
|
||||
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
|
||||
if (from.meta.cache) {
|
||||
const componentName = from.matched.at(-1)?.components?.default.name
|
||||
if (componentName) {
|
||||
// 通过 meta.cache 判断针对哪些页面进行缓存
|
||||
switch (typeof from.meta.cache) {
|
||||
case 'string':
|
||||
if (from.meta.cache !== to.name) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
if (!from.meta.cache.includes(to.name as string)) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
// 通过 meta.noCache 判断针对哪些页面不需要进行缓存
|
||||
if (from.meta.noCache) {
|
||||
switch (typeof from.meta.noCache) {
|
||||
case 'string':
|
||||
if (from.meta.noCache === to.name) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
if (from.meta.noCache.includes(to.name as string)) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
if (to.fullPath !== from.fullPath) {
|
||||
if (to.meta.cache) {
|
||||
const componentName = to.matched.at(-1)?.components?.default.name
|
||||
if (componentName) {
|
||||
// 缓存当前页面前,先判断是否需要进行清除缓存,判断依据:
|
||||
// 1. 如果 to.meta.cache 为 boolean 类型,并且不为 true,则需要清除缓存
|
||||
// 2. 如果 to.meta.cache 为 string 类型,并且与 from.name 不一致,则需要清除缓存
|
||||
// 3. 如果 to.meta.cache 为 array 类型,并且不包含 from.name,则需要清除缓存
|
||||
// 4. 如果 to.meta.noCache 为 string 类型,并且与 from.name 一致,则需要清除缓存
|
||||
// 5. 如果 to.meta.noCache 为 array 类型,并且包含 from.name,则需要清除缓存
|
||||
// 6. 如果是刷新页面,则需要清除缓存
|
||||
let shouldClearCache = false
|
||||
if (typeof to.meta.cache === 'boolean') {
|
||||
shouldClearCache = !to.meta.cache
|
||||
}
|
||||
else if (typeof to.meta.cache === 'string') {
|
||||
shouldClearCache = to.meta.cache !== from.name
|
||||
}
|
||||
else if (Array.isArray(to.meta.cache)) {
|
||||
shouldClearCache = !to.meta.cache.includes(from.name as string)
|
||||
}
|
||||
if (to.meta.noCache) {
|
||||
if (typeof to.meta.noCache === 'string') {
|
||||
shouldClearCache = to.meta.noCache === from.name
|
||||
}
|
||||
else if (Array.isArray(to.meta.noCache)) {
|
||||
shouldClearCache = to.meta.noCache.includes(from.name as string)
|
||||
}
|
||||
}
|
||||
if (from.name === 'reload') {
|
||||
shouldClearCache = true
|
||||
}
|
||||
if (shouldClearCache) {
|
||||
keepAliveStore.remove(componentName)
|
||||
await nextTick()
|
||||
}
|
||||
keepAliveStore.add(componentName)
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name === 'reload') {
|
||||
keepAliveStore.remove(componentName)
|
||||
else {
|
||||
// turbo-console-disable-next-line
|
||||
console.warn('[Fantastic-admin] 该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const useMenuStore = defineStore(
|
||||
})
|
||||
function getDeepestPath(menu: Menu.recordRaw, rootPath = '') {
|
||||
let retnPath = ''
|
||||
if (menu.children) {
|
||||
if (menu.children?.some(item => item.meta?.menu !== false)) {
|
||||
const item = menu.children.find(item => item.meta?.menu !== false)
|
||||
if (item) {
|
||||
retnPath = getDeepestPath(item, resolveRoutePath(rootPath, menu.path))
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useTabbarStore = defineStore(
|
||||
}
|
||||
})
|
||||
const meta = route.matched.at(-1)?.meta
|
||||
const tabId = route.fullPath
|
||||
const tabId = route.path
|
||||
if (route.name !== 'reload') {
|
||||
// 记录查找到的标签页
|
||||
const findTab = list.value.find((item) => {
|
||||
|
||||
Vendored
+3
-1
@@ -19,12 +19,14 @@ declare global {
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isShallow: typeof import('vue')['isShallow']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -94,6 +96,6 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
Vendored
+4
-3
@@ -16,15 +16,18 @@ declare module 'vue' {
|
||||
FaButtonGroup: typeof import('./../ui/components/FaButtonGroup/index.vue')['default']
|
||||
FaCard: typeof import('./../ui/components/FaCard/index.vue')['default']
|
||||
FaCheckbox: typeof import('./../ui/components/FaCheckbox/index.vue')['default']
|
||||
FaCollapsible: typeof import('./../ui/components/FaCollapsible/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']
|
||||
FaFileUpload: typeof import('./../ui/components/FaFileUpload/index.vue')['default']
|
||||
FaFixedActionBar: typeof import('./../ui/components/FaFixedActionBar/index.vue')['default']
|
||||
FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
|
||||
FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
|
||||
FaImagePreview: typeof import('./../ui/components/FaImagePreview/index.vue')['default']
|
||||
FaImageUpload: typeof import('./../ui/components/FaImageUpload/index.vue')['default']
|
||||
FaInput: typeof import('./../ui/components/FaInput/index.vue')['default']
|
||||
FaKbd: typeof import('./../ui/components/FaKbd/index.vue')['default']
|
||||
FaModal: typeof import('./../ui/components/FaModal/index.vue')['default']
|
||||
@@ -44,11 +47,9 @@ declare module 'vue' {
|
||||
FaSwitch: typeof import('./../ui/components/FaSwitch/index.vue')['default']
|
||||
FaSystemInfo: typeof import('./../ui/components/FaSystemInfo/index.vue')['default']
|
||||
FaTabs: typeof import('./../ui/components/FaTabs/index.vue')['default']
|
||||
FaTextarea: typeof import('./../ui/components/FaTextarea/index.vue')['default']
|
||||
FaToast: typeof import('./../ui/components/FaToast/index.vue')['default']
|
||||
FaTooltip: typeof import('./../ui/components/FaTooltip/index.vue')['default']
|
||||
FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
|
||||
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
|
||||
ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ const props = defineProps<{
|
||||
<style scoped>
|
||||
.horizontal-group {
|
||||
:deep(> button) {
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
&:first-child:not(:last-child) {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
&:last-child:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child, :last-child) {
|
||||
@@ -42,14 +42,14 @@ const props = defineProps<{
|
||||
|
||||
.vertical-group {
|
||||
:deep(> button) {
|
||||
&:first-child {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
&:first-child:not(:last-child) {
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
&:last-child:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child, :last-child) {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Check, Minus } from 'lucide-vue-next'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
@@ -21,11 +17,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', props.class)"
|
||||
:class="cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary bg-transparent ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground', props.class)"
|
||||
>
|
||||
<CheckboxIndicator class="h-full w-full flex items-center justify-center text-current">
|
||||
<slot>
|
||||
<Check class="h-4 w-4" />
|
||||
<Minus v-if="props.modelValue === 'indeterminate'" class="h-4 w-4" />
|
||||
<Check v-if="props.modelValue === true" class="h-4 w-4" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Checkbox } from './checkbox'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaCheckbox',
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const value = defineModel<boolean>()
|
||||
const value = defineModel<CheckboxRootProps['modelValue']>()
|
||||
|
||||
const id = useId()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center-start gap-2">
|
||||
<div :class="cn('flex-center-start gap-2', props.class)">
|
||||
<Checkbox :id v-model="value" :disabled />
|
||||
<label :for="id" class="cursor-pointer text-sm">
|
||||
<label :for="id" class="cursor-pointer text-sm empty:hidden">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CollapsibleRootProps>()
|
||||
const emits = defineEmits<CollapsibleRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
|
||||
<slot :open="open" />
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleContentProps } from 'reka-ui'
|
||||
import { CollapsibleContent } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CollapsibleContentProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleContent v-bind="props" class="CollapsibleContent">
|
||||
<slot />
|
||||
</CollapsibleContent>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.CollapsibleContent {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.CollapsibleContent[data-state="open"] {
|
||||
animation: collapsible-down 300ms ease-out;
|
||||
}
|
||||
|
||||
.CollapsibleContent[data-state="closed"] {
|
||||
animation: collapsible-up 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes collapsible-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
height: var(--reka-collapsible-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes collapsible-up {
|
||||
from {
|
||||
height: var(--reka-collapsible-content-height);
|
||||
}
|
||||
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleTriggerProps } from 'reka-ui'
|
||||
import { CollapsibleTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CollapsibleTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleTrigger v-bind="props">
|
||||
<slot />
|
||||
</CollapsibleTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Collapsible } from './Collapsible.vue'
|
||||
export { default as CollapsibleContent } from './CollapsibleContent.vue'
|
||||
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './collapsible'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaCollapsible',
|
||||
})
|
||||
|
||||
const open = defineModel<boolean>('modelValue', {
|
||||
default: false,
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Collapsible v-model:open="open">
|
||||
<CollapsibleTrigger v-if="!!slots.trigger">
|
||||
<slot name="trigger" :open />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<slot />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</template>
|
||||
@@ -1,23 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ContextMenuItem,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
const emits = defineEmits<ContextMenuItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ContextMenuLabel } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuItemIndicator,
|
||||
ContextMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ContextMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ContextMenuSeparator,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ContextMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuSubTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Component, HTMLAttributes } from 'vue'
|
||||
import { createVNode, isVNode, render } from 'vue'
|
||||
import Drawer from './index.vue'
|
||||
|
||||
export interface DrawerProps {
|
||||
id?: string
|
||||
modelValue?: boolean
|
||||
zIndex?: number
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
title?: string
|
||||
description?: string
|
||||
loading?: boolean
|
||||
closable?: boolean
|
||||
centered?: boolean
|
||||
bordered?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
showConfirmButton?: boolean
|
||||
showCancelButton?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
confirmButtonDisabled?: boolean
|
||||
confirmButtonLoading?: boolean
|
||||
beforeClose?: (
|
||||
action: 'confirm' | 'cancel' | 'close',
|
||||
done: () => void
|
||||
) => void
|
||||
header?: boolean
|
||||
footer?: boolean
|
||||
closeOnClickOverlay?: boolean
|
||||
closeOnPressEscape?: boolean
|
||||
destroyOnClose?: boolean
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export interface DrawerEmits {
|
||||
'update:modelValue': [value: boolean]
|
||||
'open': []
|
||||
'opened': []
|
||||
'close': []
|
||||
'closed': []
|
||||
'confirm': []
|
||||
'cancel': []
|
||||
}
|
||||
|
||||
type BaseOptions = Omit<DrawerProps, 'modelValue'> & {
|
||||
content?: Component | VNode | string
|
||||
onOpen?: () => void
|
||||
onOpened?: () => void
|
||||
onClose?: () => void
|
||||
onClosed?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function useFaDrawer() {
|
||||
function create(initialOptions: BaseOptions) {
|
||||
const container = document.createElement('div')
|
||||
const visible = ref(false)
|
||||
const options = reactive({ ...initialOptions })
|
||||
const instance = getCurrentInstance()
|
||||
let vnode: VNode | null = null
|
||||
|
||||
const updateVNode = () => {
|
||||
vnode = createVNode(Drawer, Object.assign({
|
||||
'id': instance && instance.uid ? `FaDrawer-${instance.uid}` : undefined,
|
||||
'modelValue': visible.value,
|
||||
'onUpdate:modelValue': (val: boolean) => {
|
||||
visible.value = val
|
||||
},
|
||||
...options,
|
||||
}), {
|
||||
default: () => {
|
||||
if (typeof options.content === 'string') {
|
||||
return options.content
|
||||
}
|
||||
else if (isVNode(options.content)) {
|
||||
return options.content
|
||||
}
|
||||
else if (options.content) {
|
||||
return h(options.content)
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
// 继承主应用的上下文
|
||||
if (instance && instance.appContext) {
|
||||
vnode.appContext = instance.appContext
|
||||
}
|
||||
render(vnode, container)
|
||||
}
|
||||
|
||||
// 监听 visible 和 options 变化,自动重新渲染
|
||||
watch([visible, options], () => {
|
||||
updateVNode()
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
// 挂载到当前实例
|
||||
instance?.proxy?.$el?.appendChild(container)
|
||||
|
||||
// 监听组件卸载,自动清理
|
||||
if (instance) {
|
||||
onUnmounted(() => {
|
||||
if (vnode) {
|
||||
render(null, container)
|
||||
vnode = null
|
||||
}
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const update = (newOptions: BaseOptions) => {
|
||||
Object.assign(options, newOptions)
|
||||
}
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
update,
|
||||
}
|
||||
}
|
||||
return {
|
||||
create,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { DrawerEmits, DrawerProps } from '.'
|
||||
import { VisuallyHidden } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
Sheet,
|
||||
@@ -15,34 +16,10 @@ defineOptions({
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
title: string
|
||||
description?: string
|
||||
loading?: boolean
|
||||
closable?: boolean
|
||||
centered?: boolean
|
||||
bordered?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
showConfirmButton?: boolean
|
||||
showCancelButton?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
confirmButtonDisabled?: boolean
|
||||
confirmButtonLoading?: boolean
|
||||
header?: boolean
|
||||
footer?: boolean
|
||||
closeOnClickOverlay?: boolean
|
||||
closeOnPressEscape?: boolean
|
||||
destroyOnClose?: boolean
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
defineProps<DrawerProps>(),
|
||||
{
|
||||
modelValue: false,
|
||||
zIndex: 2000,
|
||||
side: 'right',
|
||||
loading: false,
|
||||
closable: true,
|
||||
@@ -64,19 +41,15 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'open': []
|
||||
'opened': []
|
||||
'close': []
|
||||
'closed': []
|
||||
'confirm': []
|
||||
'cancel': []
|
||||
const emits = defineEmits<DrawerEmits>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
header?: () => VNode
|
||||
default?: () => VNode
|
||||
footer?: () => VNode
|
||||
}>()
|
||||
|
||||
const id = useId()
|
||||
provide('DrawerId', id)
|
||||
|
||||
const drawerId = shallowRef(props.id ?? useId())
|
||||
const isOpen = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
@@ -86,9 +59,16 @@ watch(() => props.modelValue, (newValue) => {
|
||||
const hasOpened = ref(false)
|
||||
const isClosed = ref(true)
|
||||
|
||||
watch(() => isOpen.value, (value) => {
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
isClosed.value = false
|
||||
if (value && !hasOpened.value) {
|
||||
if (val && !hasOpened.value) {
|
||||
hasOpened.value = true
|
||||
}
|
||||
}, {
|
||||
@@ -97,25 +77,62 @@ watch(() => isOpen.value, (value) => {
|
||||
|
||||
const forceMount = computed(() => !props.destroyOnClose && hasOpened.value)
|
||||
|
||||
function updateOpen(value: boolean) {
|
||||
isOpen.value = value
|
||||
emits('update:modelValue', value)
|
||||
async function updateOpen(value: boolean) {
|
||||
if (value) {
|
||||
isOpen.value = value
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
if (props.beforeClose) {
|
||||
await props.beforeClose(
|
||||
'close',
|
||||
() => {
|
||||
isOpen.value = value
|
||||
emits('close')
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
isOpen.value = value
|
||||
emits('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
updateOpen(false)
|
||||
emits('confirm')
|
||||
const isConfirmButtonLoading = ref(false)
|
||||
|
||||
async function onConfirm() {
|
||||
if (props.beforeClose) {
|
||||
isConfirmButtonLoading.value = true
|
||||
await props.beforeClose(
|
||||
'confirm',
|
||||
() => {
|
||||
isOpen.value = false
|
||||
emits('confirm')
|
||||
},
|
||||
)
|
||||
isConfirmButtonLoading.value = false
|
||||
}
|
||||
else {
|
||||
isOpen.value = false
|
||||
emits('confirm')
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
updateOpen(false)
|
||||
emits('cancel')
|
||||
async function onCancel() {
|
||||
if (props.beforeClose) {
|
||||
await props.beforeClose(
|
||||
'cancel',
|
||||
() => {
|
||||
isOpen.value = false
|
||||
emits('cancel')
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
isOpen.value = false
|
||||
emits('cancel')
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusOutside(e: Event) {
|
||||
@@ -124,7 +141,7 @@ function handleFocusOutside(e: Event) {
|
||||
}
|
||||
|
||||
function handleClickOutside(e: Event) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== id) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== drawerId.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
@@ -151,8 +168,10 @@ function handleAnimationEnd() {
|
||||
<template>
|
||||
<Sheet :modal="false" :open="isOpen" @update:open="updateOpen">
|
||||
<SheetContent
|
||||
:closable="props.closable"
|
||||
:drawer-id="drawerId"
|
||||
:open="isOpen"
|
||||
:z-index="props.zIndex"
|
||||
:closable="props.closable"
|
||||
:overlay="props.overlay"
|
||||
:overlay-blur="props.overlayBlur"
|
||||
:class="cn('w-full flex flex-col gap-0 p-0', props.contentClass, {
|
||||
@@ -173,6 +192,10 @@ function handleAnimationEnd() {
|
||||
'border-b': props.bordered,
|
||||
})"
|
||||
>
|
||||
<VisuallyHidden v-if="!!slots.header">
|
||||
<SheetTitle />
|
||||
<SheetDescription />
|
||||
</VisuallyHidden>
|
||||
<slot name="header">
|
||||
<SheetTitle :class="{ 'text-center': props.centered }">
|
||||
{{ title }}
|
||||
@@ -182,18 +205,22 @@ function handleAnimationEnd() {
|
||||
</SheetDescription>
|
||||
</slot>
|
||||
</SheetHeader>
|
||||
<VisuallyHidden v-else>
|
||||
<SheetTitle />
|
||||
<SheetDescription />
|
||||
</VisuallyHidden>
|
||||
<div class="m-0 flex-1 of-y-hidden">
|
||||
<FaScrollArea class="h-full">
|
||||
<FaScrollArea v-if="!!slots.default" class="h-full">
|
||||
<div class="p-4">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter
|
||||
v-if="footer" :class="cn('p-2 gap-y-2', props.footerClass, {
|
||||
v-if="footer" :class="cn('p-3 gap-y-2', props.footerClass, {
|
||||
'sm:justify-center': props.centered,
|
||||
'border-t': props.bordered,
|
||||
})"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { SheetVariants } from '.'
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { reactiveOmit, useScrollLock } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
@@ -10,17 +10,18 @@ import {
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { sheetVariants } from '.'
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
class?: HTMLAttributes['class']
|
||||
drawerId: string
|
||||
open?: boolean
|
||||
zIndex?: number
|
||||
side?: SheetVariants['side']
|
||||
closable?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
@@ -33,11 +34,7 @@ const emits = defineEmits<DialogContentEmits & {
|
||||
animationEnd: []
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, side, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'side')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
@@ -51,8 +48,6 @@ watch(showOverlay, (val) => {
|
||||
isLocked.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const id = inject('DrawerId')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -70,14 +65,20 @@ const id = inject('DrawerId')
|
||||
>
|
||||
<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', {
|
||||
:data-drawer-id="props.drawerId"
|
||||
:class="cn('fixed inset-0 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,
|
||||
})"
|
||||
:style="{
|
||||
zIndex: props.zIndex,
|
||||
}"
|
||||
/>
|
||||
</Transition>
|
||||
<DialogContent
|
||||
:class="cn(sheetVariants({ side }), props.class)"
|
||||
:style="{
|
||||
zIndex: props.zIndex,
|
||||
}"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
@animationend="emits('animationEnd')"
|
||||
>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogDescription } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogTitle } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -11,7 +11,7 @@ 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',
|
||||
'fixed 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: {
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -17,11 +17,7 @@ const props = withDefaults(
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuLabel, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { filesize } from 'filesize'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaFileUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
action: string
|
||||
method?: string
|
||||
headers?: Headers | Record<string, any>
|
||||
data?: Record<string, any>
|
||||
name?: string
|
||||
afterUpload?: (response: any) => string | Promise<string>
|
||||
multiple?: boolean
|
||||
ext?: string[]
|
||||
size?: number
|
||||
max?: number
|
||||
hideTips?: boolean
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
method: 'post',
|
||||
headers: () => ({}),
|
||||
data: () => ({}),
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
ext: () => [],
|
||||
size: 10 * 1024 * 1024,
|
||||
max: 0,
|
||||
hideTips: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [response: any, file: File]
|
||||
onClick: [fileItem: FileItem, index: number]
|
||||
}>()
|
||||
|
||||
const fileList = defineModel<FileItem[]>('modelValue', { required: true })
|
||||
|
||||
export interface FileItem {
|
||||
name: string
|
||||
size: number
|
||||
url?: string
|
||||
status?: 'uploading' | 'success' | 'error'
|
||||
progress?: number
|
||||
file?: File
|
||||
}
|
||||
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
const isDragging = ref(false)
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
isDragging.value = false
|
||||
if (e.dataTransfer?.files) {
|
||||
onSelectFile(e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectFile(files: FileList | File[] | null) {
|
||||
if (!files) {
|
||||
return
|
||||
}
|
||||
const selectedFiles = Array.from(files)
|
||||
// 数量限制
|
||||
const remain = props.max > 0 ? props.max - fileList.value.length : selectedFiles.length
|
||||
const filesToAdd: File[] = []
|
||||
for (const file of selectedFiles.slice(0, remain)) {
|
||||
// 类型校验
|
||||
if (props.ext.length > 0) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
if (!props.ext.map(e => e.toLowerCase()).includes(ext || '')) {
|
||||
toast.error(`上传文件只支持 ${props.ext.join(' / ')} 格式`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(`上传文件大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
}
|
||||
fileInputRef.value!.value = ''
|
||||
filesToAdd.forEach(file => uploadFile(file))
|
||||
}
|
||||
|
||||
function uploadFile(file: File, index?: number) {
|
||||
const formData = new FormData()
|
||||
Object.entries(props.data).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
formData.append(props.name, file)
|
||||
let headersObj: Record<string, any> = {}
|
||||
if (props.headers instanceof Headers) {
|
||||
props.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
}
|
||||
else {
|
||||
headersObj = { ...props.headers }
|
||||
}
|
||||
if (index === undefined) {
|
||||
fileList.value.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
status: 'uploading',
|
||||
progress: 0,
|
||||
file,
|
||||
})
|
||||
}
|
||||
const currentFileIndex = index ?? fileList.value.length - 1
|
||||
axios({
|
||||
url: props.action,
|
||||
method: props.method,
|
||||
headers: headersObj,
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
fileList.value[currentFileIndex].progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
const url = await props.afterUpload?.(response.data)
|
||||
if (url) {
|
||||
fileList.value[currentFileIndex].url = url
|
||||
}
|
||||
emits('onSuccess', response.data, file)
|
||||
fileList.value[currentFileIndex].status = 'success'
|
||||
})
|
||||
.catch(() => {
|
||||
fileList.value[currentFileIndex].status = 'error'
|
||||
})
|
||||
}
|
||||
|
||||
function removeFile(idx: number) {
|
||||
fileList.value.splice(idx, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
class="h-40 w-full flex flex-col cursor-pointer items-center justify-center border border-2 rounded-lg border-dashed bg-transparent p-4 transition-all"
|
||||
:class="{
|
||||
'border-primary bg-primary/5': isDragging,
|
||||
'opacity-50 cursor-not-allowed': props.disabled || (props.max > 0 && fileList.length >= props.max),
|
||||
}"
|
||||
:disabled="props.disabled || (props.max > 0 && fileList.length >= props.max)"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@click="fileInputRef?.click()"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:upload" class="mb-2 text-2xl text-card-foreground/50" />
|
||||
<div class="text-sm text-card-foreground/70">
|
||||
将文件拖到此处,或<span class="cursor-pointer text-primary font-bold">点击上传</span>
|
||||
</div>
|
||||
<input ref="fileInputRef" type="file" :multiple="props.multiple" :disabled="props.disabled" class="hidden" @change="e => onSelectFile((e.target as HTMLInputElement).files)">
|
||||
</button>
|
||||
<div v-if="!props.hideTips && !props.disabled" class="flex flex-wrap gap-1 text-xs text-card-foreground/50 empty:hidden">
|
||||
<div v-if="props.ext.length > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `支持 ${props.ext.join(' / ')} 格式` }}
|
||||
</div>
|
||||
<div v-if="props.size > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `大小不超过 ${filesize(props.size, { standard: 'jedec' })}` }}
|
||||
</div>
|
||||
<div v-if="props.max > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `数量不超过 ${props.max} 个` }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fileList.length > 0" class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
|
||||
<div
|
||||
v-for="(item, index) in fileList"
|
||||
:key="item.name + index"
|
||||
class="group/file-upload-item relative flex items-center gap-2 border rounded-lg py-2 pe-2 ps-3"
|
||||
:class="[
|
||||
item.status === 'error' ? 'border-red-500 bg-red-500/10' : '',
|
||||
]"
|
||||
@click="emits('onClick', item, index)"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<div class="truncate text-sm font-medium">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="text-xs text-card-foreground/50">
|
||||
{{ filesize(item.size, { standard: 'jedec' }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.status === 'uploading'" class="pointer-events-none absolute inset-0 z-0 bg-primary/5" :style="{ width: `${item.progress}%` }" />
|
||||
<div v-else-if="item.status === 'success'" class="size-10 flex-center">
|
||||
<FaIcon name="i-ix:upload-success" class="text-lg text-green-600" />
|
||||
</div>
|
||||
<div v-else-if="item.status === 'error'" class="size-10 flex-center">
|
||||
<FaIcon name="i-ix:upload-fail" class="text-lg text-red-600" />
|
||||
</div>
|
||||
<FaButtonGroup v-if="!props.disabled" class="absolute inset-e-2 top-1/2 opacity-0 transition-opacity -translate-y-1/2 group-hover/file-upload-item:opacity-100">
|
||||
<FaButton v-if="item.status === 'error'" variant="outline" size="icon" @click.stop="uploadFile(item.file!, index)">
|
||||
<FaIcon name="i-icon-park-outline:upload" class="cursor-pointer text-lg" />
|
||||
</FaButton>
|
||||
<FaButton v-if="item.status !== 'uploading'" variant="outline" size="icon" @click.stop="removeFile(index)">
|
||||
<FaIcon name="i-icon-park-outline:delete" class="cursor-pointer text-lg text-red-500" />
|
||||
</FaButton>
|
||||
</FaButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { HoverCardContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
HoverCardContent,
|
||||
HoverCardPortal,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -16,11 +16,7 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import Preview from './preview.vue'
|
||||
|
||||
export function useFaImagePreview() {
|
||||
function open(src: string | string[], index = 0) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Preview, {
|
||||
modelValue: true,
|
||||
src,
|
||||
index,
|
||||
})
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Dialog, DialogContent } from './dialog'
|
||||
import Preview from './preview.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaImagePreview',
|
||||
@@ -21,13 +21,6 @@ const isLoading = ref(true)
|
||||
const isError = ref(false)
|
||||
const isOpen = ref(false)
|
||||
|
||||
// 图片操作相关状态
|
||||
const scale = ref(1)
|
||||
const rotate = ref(0)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
|
||||
function handleLoad() {
|
||||
isLoading.value = false
|
||||
emits('load')
|
||||
@@ -44,87 +37,6 @@ function handleClick() {
|
||||
isOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 图片操作函数
|
||||
function handleZoomIn() {
|
||||
scale.value = Math.min(scale.value + 0.25, 3)
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
scale.value = Math.max(scale.value - 0.25, 0.5)
|
||||
}
|
||||
|
||||
function handleOriginalSize() {
|
||||
scale.value = 1
|
||||
}
|
||||
|
||||
function handleRotateLeft() {
|
||||
rotate.value = rotate.value - 90
|
||||
}
|
||||
|
||||
function handleRotateRight() {
|
||||
rotate.value = rotate.value + 90
|
||||
}
|
||||
|
||||
// 处理滚轮缩放
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault()
|
||||
if (e.deltaY < 0) {
|
||||
handleZoomIn()
|
||||
}
|
||||
else {
|
||||
handleZoomOut()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标按下事件
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
isDragging.value = true
|
||||
dragStart.value = {
|
||||
x: e.clientX - position.value.x,
|
||||
y: e.clientY - position.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标移动事件
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging.value) {
|
||||
return
|
||||
}
|
||||
position.value = {
|
||||
x: e.clientX - dragStart.value.x,
|
||||
y: e.clientY - dragStart.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标松开事件
|
||||
function handleMouseUp() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 重置图片状态
|
||||
function resetImageState() {
|
||||
scale.value = 1
|
||||
rotate.value = 0
|
||||
position.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
// 监听鼠标事件
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
function handleAnimationEnd() {
|
||||
if (!isOpen.value) {
|
||||
resetImageState()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -148,40 +60,6 @@ function handleAnimationEnd() {
|
||||
<FaIcon name="i-ph:image-broken-duotone" class="size-8 text-secondary-foreground/50" />
|
||||
</slot>
|
||||
</div>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="size-full" @animation-end="handleAnimationEnd">
|
||||
<div class="relative size-full" @wheel="handleWheel">
|
||||
<img
|
||||
:src="src"
|
||||
class="mx-auto max-h-full max-w-full object-contain"
|
||||
:class="{
|
||||
'transition-all duration-300': !isDragging,
|
||||
}"
|
||||
:style="{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotate}deg)`,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
}"
|
||||
@mousedown.prevent="handleMouseDown"
|
||||
>
|
||||
<FaButtonGroup class="absolute bottom-4 left-1/2 scale-125 -translate-x-1/2">
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomIn">
|
||||
<FaIcon name="i-carbon:zoom-in" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomOut">
|
||||
<FaIcon name="i-carbon:zoom-out" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleOriginalSize">
|
||||
<FaIcon name="i-lucide:maximize" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateLeft">
|
||||
<FaIcon name="i-carbon:rotate" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateRight">
|
||||
<FaIcon name="i-carbon:rotate-180" class="size-5" />
|
||||
</FaButton>
|
||||
</FaButtonGroup>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Preview v-model="isOpen" :src />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import { Dialog, DialogContent } from './dialog'
|
||||
|
||||
const props = defineProps<{
|
||||
src: string | string[]
|
||||
index?: number
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>()
|
||||
|
||||
// 多图操作
|
||||
const index = ref(props.index ?? 0)
|
||||
const srcList = computed(() => {
|
||||
if (Array.isArray(props.src)) {
|
||||
return props.src
|
||||
}
|
||||
return [props.src]
|
||||
})
|
||||
function handlePrev() {
|
||||
index.value = index.value - 1
|
||||
if (index.value < 0) {
|
||||
index.value = srcList.value.length - 1
|
||||
}
|
||||
}
|
||||
function handleNext() {
|
||||
index.value = index.value + 1
|
||||
if (index.value >= srcList.value.length) {
|
||||
index.value = 0
|
||||
}
|
||||
}
|
||||
watch(index, resetImageState)
|
||||
|
||||
// 图片操作相关状态
|
||||
const scale = ref(1)
|
||||
const rotate = ref(0)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
|
||||
// 图片操作函数
|
||||
function handleZoomIn() {
|
||||
scale.value = Math.min(scale.value + 0.25, 3)
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
scale.value = Math.max(scale.value - 0.25, 0.5)
|
||||
}
|
||||
|
||||
function handleOriginalSize() {
|
||||
scale.value = 1
|
||||
}
|
||||
|
||||
function handleRotateLeft() {
|
||||
rotate.value = rotate.value - 90
|
||||
}
|
||||
|
||||
function handleRotateRight() {
|
||||
rotate.value = rotate.value + 90
|
||||
}
|
||||
|
||||
// 处理滚轮缩放
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault()
|
||||
if (e.deltaY < 0) {
|
||||
handleZoomIn()
|
||||
}
|
||||
else {
|
||||
handleZoomOut()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标按下事件
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
isDragging.value = true
|
||||
dragStart.value = {
|
||||
x: e.clientX - position.value.x,
|
||||
y: e.clientY - position.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标移动事件
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging.value) {
|
||||
return
|
||||
}
|
||||
position.value = {
|
||||
x: e.clientX - dragStart.value.x,
|
||||
y: e.clientY - dragStart.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标松开事件
|
||||
function handleMouseUp() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 重置图片状态
|
||||
function resetImageState() {
|
||||
scale.value = 1
|
||||
rotate.value = 0
|
||||
position.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
// 监听鼠标事件
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
function handleAnimationEnd() {
|
||||
if (!isOpen.value) {
|
||||
resetImageState()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="size-full" @animation-end="handleAnimationEnd">
|
||||
<div class="relative size-full flex-center" @wheel="handleWheel">
|
||||
<img
|
||||
:src="srcList[index]"
|
||||
class="mx-auto max-h-full max-w-full object-contain"
|
||||
:class="{
|
||||
'transition-all duration-300': !isDragging,
|
||||
}"
|
||||
:style="{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotate}deg)`,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
}"
|
||||
@mousedown.prevent="handleMouseDown"
|
||||
>
|
||||
<template v-if="srcList.length > 1">
|
||||
<div class="absolute bottom-20 left-1/2 text-sm text-muted-foreground -translate-x-1/2">
|
||||
{{ index + 1 }} / {{ srcList.length }}
|
||||
</div>
|
||||
<FaButton variant="ghost" size="icon" class="absolute left-4 top-1/2 rounded-full bg-muted/50 -translate-y-1/2" @click="handlePrev">
|
||||
<FaIcon name="i-lucide:chevron-left" class="size-6" />
|
||||
</FaButton>
|
||||
<FaButton variant="ghost" size="icon" class="absolute right-4 top-1/2 rounded-full bg-muted/50 -translate-y-1/2" @click="handleNext">
|
||||
<FaIcon name="i-lucide:chevron-right" class="size-6" />
|
||||
</FaButton>
|
||||
</template>
|
||||
<FaButtonGroup class="absolute bottom-4 left-1/2 scale-125 -translate-x-1/2">
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomIn">
|
||||
<FaIcon name="i-carbon:zoom-in" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomOut">
|
||||
<FaIcon name="i-carbon:zoom-out" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleOriginalSize">
|
||||
<FaIcon name="i-lucide:maximize" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateLeft">
|
||||
<FaIcon name="i-carbon:rotate" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateRight">
|
||||
<FaIcon name="i-carbon:rotate-180" class="size-5" />
|
||||
</FaButton>
|
||||
</FaButtonGroup>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { filesize } from 'filesize'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useFaImagePreview } from '@/ui/components/FaImagePreview'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
action: string
|
||||
method?: string
|
||||
headers?: Headers | Record<string, any>
|
||||
data?: Record<string, any>
|
||||
name?: string
|
||||
afterUpload?: (response: any) => string | Promise<string>
|
||||
multiple?: boolean
|
||||
ext?: string[]
|
||||
max?: number
|
||||
width?: number
|
||||
height?: number
|
||||
dimension?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
size?: number
|
||||
hideTips?: boolean
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
method: 'post',
|
||||
headers: () => ({}),
|
||||
data: () => ({}),
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
ext: () => [],
|
||||
max: 1,
|
||||
width: 100,
|
||||
height: 100,
|
||||
size: 5 * 1024 * 1024,
|
||||
hideTips: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [response: any, file: File]
|
||||
}>()
|
||||
|
||||
const images = defineModel<string[]>('modelValue', { required: true })
|
||||
|
||||
const isUploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
|
||||
function onSelectFile(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (!input.files) {
|
||||
return
|
||||
}
|
||||
const selectedFiles = Array.from(input.files)
|
||||
const remain = props.max === 0 ? selectedFiles.length : props.max - images.value.length
|
||||
const filesToAdd: File[] = []
|
||||
for (const file of selectedFiles.slice(0, remain)) {
|
||||
const fileSize = file.size / 1024 / 1024
|
||||
if (fileSize > props.size) {
|
||||
toast.error(`上传图片大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`)
|
||||
return
|
||||
}
|
||||
// 类型校验
|
||||
if (props.ext.length > 0) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
if (!props.ext.map(e => e.toLowerCase()).includes(ext || '')) {
|
||||
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式`, {
|
||||
description: file.name,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(`上传图片大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`, {
|
||||
description: `${file.name} (${filesize(file.size, { standard: 'jedec' })})`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
}
|
||||
fileInputRef.value!.value = ''
|
||||
// 上传每个文件
|
||||
filesToAdd.forEach((file) => {
|
||||
isUploading.value = true
|
||||
uploadProgress.value = 0
|
||||
const formData = new FormData()
|
||||
// 附加额外参数
|
||||
Object.entries(props.data).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
formData.append(props.name, file)
|
||||
// 处理 headers,确保为普通对象
|
||||
let headersObj: Record<string, any> = {}
|
||||
if (props.headers instanceof Headers) {
|
||||
props.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
}
|
||||
else {
|
||||
headersObj = { ...props.headers }
|
||||
}
|
||||
return axios({
|
||||
url: props.action,
|
||||
method: props.method,
|
||||
headers: headersObj,
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
},
|
||||
}).then(async (response) => {
|
||||
const url = await props.afterUpload?.(response.data)
|
||||
if (url) {
|
||||
images.value.push(url)
|
||||
}
|
||||
emits('onSuccess', response.data, file)
|
||||
}).finally(() => {
|
||||
isUploading.value = false
|
||||
uploadProgress.value = 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onRemove(idx: number) {
|
||||
images.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function onPreview(index: number) {
|
||||
useFaImagePreview().open(images.value, index)
|
||||
}
|
||||
|
||||
function onMove(index: number, direction: 'forward' | 'backward') {
|
||||
if (direction === 'forward' && index !== 0) {
|
||||
images.value[index] = images.value.splice(index - 1, 1, images.value[index])[0]
|
||||
}
|
||||
if (direction === 'backward' && index !== images.value.length - 1) {
|
||||
images.value[index] = images.value.splice(index + 1, 1, images.value[index])[0]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="(img, index) in images" :key="img" class="group/image-upload relative flex items-center justify-center overflow-hidden border rounded-lg" :style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}"
|
||||
>
|
||||
<img :src="img" class="h-full w-full object-contain">
|
||||
<div v-if="!props.disabled" class="absolute inset-0 grid grid-cols-2 place-items-center rounded-lg bg-black/50 p-2 text-white opacity-0 transition-opacity group-hover/image-upload:opacity-100">
|
||||
<div class="flex-center cursor-pointer opacity-60 transition-all hover:(scale-110 opacity-100)" @click.prevent="onPreview(index)">
|
||||
<FaIcon name="i-icon-park-outline:preview-open" class="size-6" />
|
||||
</div>
|
||||
<div class="flex-center cursor-pointer opacity-60 transition-all hover:(scale-110 opacity-100)" @click.prevent="onRemove(index)">
|
||||
<FaIcon name="i-icon-park-outline:delete" class="size-6" />
|
||||
</div>
|
||||
<div
|
||||
v-show="images.length > 1" :class="cn('flex-center cursor-pointer opacity-60 transition-all', {
|
||||
'hover:(scale-110 opacity-100)': index !== 0,
|
||||
'cursor-not-allowed': index === 0,
|
||||
})" @click="onMove(index, 'forward')"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:arrow-circle-left" class="size-6" />
|
||||
</div>
|
||||
<div
|
||||
v-show="images.length > 1" :class="cn('flex-center cursor-pointer opacity-60 transition-all', {
|
||||
'hover:(scale-110 opacity-100)': index !== images.length - 1,
|
||||
'cursor-not-allowed': index === images.length - 1,
|
||||
})" @click="onMove(index, 'backward')"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:arrow-circle-right" class="size-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="images.length < props.max || props.max === 0" type="button" class="relative flex-center overflow-hidden bg-transparent" :class="{
|
||||
'cursor-not-allowed': props.disabled || isUploading,
|
||||
}" :style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}" :disabled="props.disabled || isUploading" @click="fileInputRef?.click()"
|
||||
>
|
||||
<slot>
|
||||
<div class="size-full flex-center border border-2 rounded-lg border-dashed p-0 text-primary">
|
||||
<FaIcon name="i-icon-park-outline:upload" class="text-2xl text-card-foreground/50" />
|
||||
</div>
|
||||
</slot>
|
||||
<template v-if="isUploading && uploadProgress > 0 && uploadProgress < 100">
|
||||
<FaProgress :model-value="uploadProgress" class="absolute inset-x-1 inset-b-1 h-1 w-auto" />
|
||||
</template>
|
||||
<input ref="fileInputRef" type="file" accept="image/*" :multiple="props.multiple" class="hidden" @change="onSelectFile">
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!props.hideTips && !props.disabled" class="flex flex-wrap gap-1 text-xs text-card-foreground/50 empty:hidden">
|
||||
<div v-if="props.dimension" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `建议尺寸为 ${props.dimension?.width}*${props.dimension?.height}` }}
|
||||
</div>
|
||||
<div v-if="props.ext.length > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `支持 ${props.ext.join(' / ')} 格式` }}
|
||||
</div>
|
||||
<div v-if="props.size > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `大小不超过 ${filesize(props.size, { standard: 'jedec' })}` }}
|
||||
</div>
|
||||
<div v-if="props.max > 1" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `数量不超过 ${props.max} 个` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { reactiveOmit, useScrollLock } from '@vueuse/core'
|
||||
import { Maximize, Minimize, X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
@@ -9,29 +9,25 @@ import {
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & {
|
||||
modalId: string
|
||||
class?: HTMLAttributes['class']
|
||||
open?: boolean
|
||||
zIndex?: number
|
||||
maximize?: boolean
|
||||
maximizable?: boolean
|
||||
closable?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
const emits = defineEmits<DialogContentEmits & {
|
||||
toggleMaximize: [val: boolean]
|
||||
animationEnd: []
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
@@ -73,9 +69,12 @@ watch(showOverlay, (val) => {
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
:data-modal-id="props.modalId"
|
||||
: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', {
|
||||
:class="cn('fixed inset-0 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,
|
||||
})"
|
||||
:style="{
|
||||
zIndex: props.zIndex,
|
||||
}"
|
||||
/>
|
||||
</Transition>
|
||||
<DialogContent
|
||||
@@ -83,9 +82,12 @@ watch(showOverlay, (val) => {
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-2000 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-0 data-[state=closed]:slide-out-to-top-1/5 data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-1/5 sm:rounded-lg',
|
||||
'fixed left-1/2 top-1/2 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-0 data-[state=closed]:slide-out-to-top-1/5 data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-1/5 sm:rounded-lg',
|
||||
props.class,
|
||||
)"
|
||||
:style="{
|
||||
zIndex: props.zIndex,
|
||||
}"
|
||||
@animationend="emits('animationEnd')"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogDescription, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
@@ -9,17 +10,12 @@ import {
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogTitle, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
@@ -21,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-lg font-semibold tracking-tight',
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
+152
-103
@@ -1,8 +1,11 @@
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { Component, HTMLAttributes } from 'vue'
|
||||
import { createVNode, isVNode, render } from 'vue'
|
||||
import Modal from './index.vue'
|
||||
|
||||
export interface ModalProps {
|
||||
id?: string
|
||||
modelValue?: boolean
|
||||
zIndex?: number
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: 'info' | 'success' | 'warning' | 'error'
|
||||
@@ -46,125 +49,171 @@ export interface ModalEmits {
|
||||
'cancel': []
|
||||
}
|
||||
|
||||
type alertOptions = Pick<ModalProps, 'title' | 'description' | 'icon' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
onConfirm?: () => void
|
||||
}
|
||||
|
||||
type confirmOptions = Pick<ModalProps, 'title' | 'description' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'beforeClose' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
type BaseOptions = Omit<ModalProps, 'modelValue'> & {
|
||||
content?: Component | VNode | string
|
||||
onOpen?: () => void
|
||||
onOpened?: () => void
|
||||
onClose?: () => void
|
||||
onClosed?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
type alertOptions = Pick<BaseOptions, 'title' | 'description' | 'icon' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass' | 'content' | 'onConfirm'>
|
||||
|
||||
type confirmOptions = Pick<BaseOptions, 'title' | 'description' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'beforeClose' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass' | 'content' | 'onConfirm' | 'onCancel'>
|
||||
|
||||
export function useFaModal() {
|
||||
function info(options: alertOptions) {
|
||||
function create(initialOptions: BaseOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'info',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
const visible = ref(false)
|
||||
const options = reactive({ ...initialOptions })
|
||||
const instance = getCurrentInstance()
|
||||
let vnode: VNode | null = null
|
||||
|
||||
const updateVNode = () => {
|
||||
vnode = createVNode(Modal, Object.assign({
|
||||
'id': instance && instance.uid ? `FaModal-${instance.uid}` : undefined,
|
||||
'modelValue': visible.value,
|
||||
'onUpdate:modelValue': (val: boolean) => {
|
||||
visible.value = val
|
||||
},
|
||||
...options,
|
||||
}), {
|
||||
default: () => {
|
||||
if (typeof options.content === 'string') {
|
||||
return options.content
|
||||
}
|
||||
else if (isVNode(options.content)) {
|
||||
return options.content
|
||||
}
|
||||
else if (options.content) {
|
||||
return h(options.content)
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
// 继承主应用的上下文
|
||||
if (instance && instance.appContext) {
|
||||
vnode.appContext = instance.appContext
|
||||
}
|
||||
render(vnode, container)
|
||||
}
|
||||
|
||||
// 监听 visible 和 options 变化,自动重新渲染
|
||||
watch([visible, options], () => {
|
||||
updateVNode()
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
app.mount(container)
|
||||
|
||||
// 挂载到当前实例
|
||||
instance?.proxy?.$el?.appendChild(container)
|
||||
|
||||
// 监听组件卸载,自动清理
|
||||
if (instance) {
|
||||
onUnmounted(() => {
|
||||
if (vnode) {
|
||||
render(null, container)
|
||||
vnode = null
|
||||
}
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const update = (newOptions: BaseOptions) => {
|
||||
Object.assign(options, newOptions)
|
||||
}
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
update,
|
||||
}
|
||||
}
|
||||
function info(options: alertOptions) {
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'info',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function success(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'success',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'success',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function warning(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'warning',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'warning',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function error(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'error',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'error',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function confirm(options: confirmOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
showCancelButton: true,
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
showCancelButton: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open, update } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
return {
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
info,
|
||||
success,
|
||||
warning,
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = withDefaults(
|
||||
defineProps<ModalProps>(),
|
||||
{
|
||||
modelValue: false,
|
||||
zIndex: 2000,
|
||||
loading: false,
|
||||
closable: true,
|
||||
maximize: false,
|
||||
@@ -61,7 +62,7 @@ defineExpose({
|
||||
areaRef: dialogAreaRef,
|
||||
})
|
||||
|
||||
const modalId = useId()
|
||||
const modalId = shallowRef(props.id ?? useId())
|
||||
const isOpen = ref(props.modelValue)
|
||||
const isMaximize = ref(props.maximize)
|
||||
|
||||
@@ -72,9 +73,16 @@ watch(() => props.modelValue, (newValue) => {
|
||||
const hasOpened = ref(false)
|
||||
const isClosed = ref(true)
|
||||
|
||||
watch(() => isOpen.value, (value) => {
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
isClosed.value = false
|
||||
if (value && !hasOpened.value) {
|
||||
if (val && !hasOpened.value) {
|
||||
hasOpened.value = true
|
||||
}
|
||||
}, {
|
||||
@@ -98,7 +106,6 @@ function setTransform() {
|
||||
}
|
||||
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
if (dialogContentRef.value) {
|
||||
@@ -106,10 +113,6 @@ watch(isOpen, (val) => {
|
||||
setTransform()
|
||||
}
|
||||
})
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -177,7 +180,7 @@ function handleFocusOutside(e: Event) {
|
||||
}
|
||||
|
||||
function handleClickOutside(e: Event) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.modalId !== modalId) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.modalId !== modalId.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
@@ -212,6 +215,7 @@ function handleAnimationEnd() {
|
||||
ref="dialogContentRef"
|
||||
:modal-id="modalId"
|
||||
:open="isOpen"
|
||||
:z-index="props.zIndex"
|
||||
:closable="props.closable"
|
||||
:overlay="props.overlay"
|
||||
:overlay-blur="props.overlayBlur"
|
||||
@@ -265,6 +269,10 @@ function handleAnimationEnd() {
|
||||
</DialogDescription>
|
||||
</slot>
|
||||
</DialogHeader>
|
||||
<VisuallyHidden v-else>
|
||||
<DialogTitle />
|
||||
<DialogDescription />
|
||||
</VisuallyHidden>
|
||||
<FaScrollArea v-if="!!slots.default" ref="dialogAreaRef" class="flex-1">
|
||||
<div :class="cn('min-h-40 p-4', props.contentClass)">
|
||||
<slot />
|
||||
@@ -274,7 +282,7 @@ function handleAnimationEnd() {
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<DialogFooter
|
||||
v-if="footer" :class="cn('p-2 gap-y-2', props.footerClass, {
|
||||
v-if="footer" :class="cn('p-3 gap-y-2', props.footerClass, {
|
||||
'md:justify-center': props.center,
|
||||
'border-t': props.border,
|
||||
})"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastRootEmits } from 'reka-ui'
|
||||
import type { ToastProps } from '.'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ToastRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { toastVariants } from '.'
|
||||
|
||||
@@ -10,11 +10,7 @@ const props = defineProps<ToastProps>()
|
||||
|
||||
const emits = defineEmits<ToastRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastActionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ToastAction } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ToastActionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastCloseProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { ToastClose } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ToastCloseProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ToastDescription } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ToastDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ToastTitle } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ToastTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToastViewportProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ToastViewport } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<ToastViewportProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PinInput, PinInputGroup, PinInputInput } from './pin-input'
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from './pin-input'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaPinInput',
|
||||
@@ -26,10 +26,6 @@ watch(modelValue, (val) => {
|
||||
value.value = val?.split('') ?? []
|
||||
})
|
||||
|
||||
watch(value, (val) => {
|
||||
modelValue.value = val.join('')
|
||||
})
|
||||
|
||||
function handleComplete(e: string[]) {
|
||||
modelValue.value = e.join('')
|
||||
emit('complete')
|
||||
@@ -39,9 +35,9 @@ const id = useId()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PinInput :id :model-value="value" placeholder="○" otp @complete="handleComplete" @update:model-value="arrStr => value = arrStr.filter(Boolean)">
|
||||
<PinInput :id v-model="value" placeholder="○" otp @complete="handleComplete">
|
||||
<PinInputGroup>
|
||||
<PinInputInput v-for="(item, index) in length" :key="item" :index="index" />
|
||||
<PinInputSlot v-for="(item, index) in length" :key="item" :index="index" />
|
||||
</PinInputGroup>
|
||||
</PinInput>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PinInputRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(defineProps<PinInputRootProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
@@ -10,10 +10,7 @@ const props = withDefaults(defineProps<PinInputRootProps & { class?: HTMLAttribu
|
||||
})
|
||||
const emits = defineEmits<PinInputRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Primitive, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>()
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive v-bind="forwardedProps" :class="cn('flex items-center', props.class)">
|
||||
<slot />
|
||||
</primitive>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -12,5 +12,5 @@ const forwardedProps = useForwardProps(props)
|
||||
<slot>
|
||||
<Dot />
|
||||
</slot>
|
||||
</primitive>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
+2
-5
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { PinInputInputProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PinInputInput, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<PinInputInputProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as PinInput } from './PinInput.vue'
|
||||
export { default as PinInputGroup } from './PinInputGroup.vue'
|
||||
export { default as PinInputInput } from './PinInputInput.vue'
|
||||
export { default as PinInputSeparator } from './PinInputSeparator.vue'
|
||||
export { default as PinInputSlot } from './PinInputSlot.vue'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
@@ -22,11 +22,7 @@ const props = withDefaults(
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ProgressRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ProgressIndicator,
|
||||
ProgressRoot,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -15,11 +15,7 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -60,11 +60,6 @@ function onWheel(event: WheelEvent) {
|
||||
left: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
else {
|
||||
scrollAreaRef.value?.el?.viewportElement?.scrollBy({
|
||||
top: event.deltaY || event.detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const scrollContainerRef = useTemplateRef('scrollContainerRef')
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaViewport,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import ScrollBar from './ScrollBar.vue'
|
||||
|
||||
@@ -17,11 +17,7 @@ const props = defineProps<ScrollAreaRootProps & {
|
||||
onWheel?: (event: WheelEvent) => void
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, onWheel, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const viewportRef = useTemplateRef('viewportRef')
|
||||
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaScrollbarProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
@@ -18,13 +19,13 @@ const props = defineProps<{
|
||||
disabled?: boolean
|
||||
options: {
|
||||
label: string
|
||||
value: string
|
||||
value: AcceptableValue
|
||||
disabled?: boolean
|
||||
}[]
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const value = defineModel<string>()
|
||||
const value = defineModel<AcceptableValue>()
|
||||
|
||||
const selectedOption = computed({
|
||||
get() {
|
||||
@@ -43,7 +44,7 @@ const selectedOption = computed({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="option in options" :key="option.value" :value="option.value" :disabled="option.disabled">
|
||||
<SelectItem v-for="(option, index) in options" :key="index" :value="option.value" :disabled="option.disabled">
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
|
||||
|
||||
@@ -23,11 +23,7 @@ const props = withDefaults(
|
||||
)
|
||||
const emits = defineEmits<SelectContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectGroupProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { SelectGroup } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
SelectItem,
|
||||
@@ -8,16 +9,11 @@ import {
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollDownButtonProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollUpButtonProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronUp } from 'lucide-vue-next'
|
||||
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { SelectSeparator } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -16,6 +16,7 @@ const props = withDefaults(defineProps<{
|
||||
step?: SliderRootProps['step']
|
||||
orientation?: SliderRootProps['orientation']
|
||||
thumbAlignment?: SliderRootProps['thumbAlignment']
|
||||
tooltip?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
defaultValue: () => [0],
|
||||
@@ -26,6 +27,7 @@ const props = withDefaults(defineProps<{
|
||||
step: 1,
|
||||
orientation: 'horizontal',
|
||||
thumbAlignment: 'contain',
|
||||
tooltip: true,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<number[]>()
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { SliderRootEmits, SliderRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SliderRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const props = defineProps<SliderRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
tooltip?: boolean
|
||||
}>()
|
||||
const emits = defineEmits<SliderRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
@@ -28,7 +27,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<SliderTrack class="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary data-[orientation=vertical]:w-2">
|
||||
<SliderRange class="absolute h-full bg-primary data-[orientation=vertical]:w-full" />
|
||||
</SliderTrack>
|
||||
<FaTooltip v-for="(val, key) in modelValue" :key="key" :text="val.toString()" disable-closing-trigger>
|
||||
<FaTooltip v-for="(val, key) in modelValue" :key="key" :text="val.toString()" disable-closing-trigger :disabled="!props.tooltip">
|
||||
<SliderThumb
|
||||
class="block h-5 w-5 border-2 border-primary rounded-full bg-background ring-offset-background transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring"
|
||||
/>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { SwitchRootEmits, SwitchRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
SwitchRoot,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsContent } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsListProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsList } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsTrigger, useForwardProps } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts" generic="T extends string | number">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Textarea } from './textarea'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaTextarea',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const value = defineModel<T>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Textarea v-model="value" :disabled :class="props.class" />
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea v-model="modelValue" :class="cn('flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Textarea } from './Textarea.vue'
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
@@ -15,11 +15,7 @@ const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttri
|
||||
|
||||
const emits = defineEmits<TooltipContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import zhCN from 'element-plus/es/locale/lang/zh-cn.mjs'
|
||||
import zhCN from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/utils'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useId } from 'reka-ui'
|
||||
import { type HTMLAttributes, provide } from 'vue'
|
||||
import { provide } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user