mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 20:33:17 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f012323e | ||
|
|
b661e0e834 | ||
|
|
5bf2664a04 | ||
|
|
3fded8abac | ||
|
|
d707522b6c | ||
|
|
a177b8c6a6 | ||
|
|
ac9b5d5b3b | ||
|
|
f0582cf721 | ||
|
|
7be9641d46 | ||
|
|
7fb2864072 | ||
|
|
658b51a747 | ||
|
|
b5832c3256 | ||
|
|
ba6eb5480a | ||
|
|
cf73c14561 | ||
|
|
e712a75a7f | ||
|
|
8ae890213e | ||
|
|
7cc4f64274 | ||
|
|
17f0af6542 | ||
|
|
1262fe9e1d | ||
|
|
c9c69464f3 | ||
|
|
7bcdc1b4cd | ||
|
|
69665852a1 | ||
|
|
09807dd115 | ||
|
|
4daeb67e19 | ||
|
|
439d6e9f60 | ||
|
|
47f1ac3513 | ||
|
|
314de32d79 | ||
|
|
50db1c3091 | ||
|
|
c63ac44413 | ||
|
|
8e2de47ee6 | ||
|
|
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 | ||
|
|
35af83a82a | ||
|
|
128ac5ee46 | ||
|
|
fa537b7969 | ||
|
|
e2cd48469c | ||
|
|
bfe6607f04 | ||
|
|
59e683c2e3 | ||
|
|
8d59733973 | ||
|
|
0ac47384aa | ||
|
|
1a0eb3989c | ||
|
|
6fc89426c9 | ||
|
|
28cc84ff0e | ||
|
|
094ee89c6c | ||
|
|
7ee93b0ccb | ||
|
|
e3aa58e892 | ||
|
|
6febfea146 | ||
|
|
3859c26f42 | ||
|
|
245eda34cb | ||
|
|
2bf683d75d | ||
|
|
e553fefffb | ||
|
|
5160da3085 | ||
|
|
4ef21b7c7f | ||
|
|
202fc12e99 | ||
|
|
098d39b6bf | ||
|
|
53f7485754 | ||
|
|
b5981dd6ac | ||
|
|
ce810aa633 | ||
|
|
eabb43ecab | ||
|
|
8bfa0f0c27 | ||
|
|
05261b37bb | ||
|
|
b974139eeb | ||
|
|
867852e501 | ||
|
|
2cadef4ad1 | ||
|
|
3b889f611f | ||
|
|
9f37438669 | ||
|
|
b7c627c556 | ||
|
|
7d2279f38d | ||
|
|
eb306b18bc | ||
|
|
8d4e226c17 | ||
|
|
1edf486160 | ||
|
|
e2d058abdd | ||
|
|
9b5003fad8 | ||
|
|
cbef1bb52e |
@@ -1,87 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
upload-archive:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release
|
||||
id: last_release
|
||||
uses: joutvhu/get-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
|
||||
- name: Create Archive
|
||||
uses: thedoctor0/zip-release@main
|
||||
with:
|
||||
type: zip
|
||||
filename: fantastic-admin.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
exclusions: '/.git/* /.github/*'
|
||||
|
||||
- name: Upload Archive To Release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
release_id: ${{ steps.last_release.outputs.id }}
|
||||
draft: false
|
||||
file: fantastic-admin.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
|
||||
upload-archive-example:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: example
|
||||
|
||||
- name: Get Release
|
||||
id: last_release
|
||||
uses: joutvhu/get-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
|
||||
- name: Create Archive
|
||||
uses: thedoctor0/zip-release@main
|
||||
with:
|
||||
type: zip
|
||||
filename: fantastic-admin.example.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
exclusions: '/.git/* /.github/* /public/.github/* /public/.nojekyll'
|
||||
|
||||
- name: Upload Archive To Release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
release_id: ${{ steps.last_release.outputs.id }}
|
||||
draft: false
|
||||
file: fantastic-admin.example.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
@@ -1,23 +0,0 @@
|
||||
name: repo-sync
|
||||
|
||||
on:
|
||||
# 每天 00:05 自动同步
|
||||
schedule:
|
||||
- cron: '5 0 * * *'
|
||||
# 手动触发部署
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-to-gitee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync to Gitee
|
||||
uses: wearerequired/git-mirror-action@master
|
||||
env:
|
||||
# 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
|
||||
with:
|
||||
# 注意替换为你的 GitHub 源仓库地址
|
||||
source-repo: git@github.com:fantastic-admin/basic.git
|
||||
# 注意替换为你的 Gitee 目标仓库地址
|
||||
destination-repo: git@gitee.com:fantastic-admin/basic.git
|
||||
Vendored
+9
-8
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"eslint.useFlatConfig": true,
|
||||
"npm.packageManager": "pnpm",
|
||||
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -7,12 +8,7 @@
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"postcss",
|
||||
"scss",
|
||||
"vue"
|
||||
],
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
@@ -25,5 +21,10 @@
|
||||
"jsonc",
|
||||
"yaml"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"postcss",
|
||||
"scss",
|
||||
"vue"
|
||||
]
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
[中文](./README.md) | **English**
|
||||
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" alt="Fantastic-admin" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Fantastic-admin</h1>
|
||||
|
||||
<p align="center">An <b>out-of-the-box</b> Vue3 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" alt="" /></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" alt="" /></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" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview3.png" alt="" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview4.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview5.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview6.png" alt="" /></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" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview3.png" alt="" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview4.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview5.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview6.png" alt="" /></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" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview3.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview4.png" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1,10 +1,12 @@
|
||||
**中文** | [English](./README.EN.md)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" />
|
||||
<img src="https://fantastic-admin.hurui.me/logo.svg" width="200" height="200" alt="Fantastic-admin" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Fantastic-admin</h1>
|
||||
|
||||
<p align="center">一款<b>开箱即用</b>的 Vue3 中后台管理系统框架</p>
|
||||
<p align="center">一款<b>开箱即用</b>的 Vue3 管理系统框架</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fantastic-admin.hurui.me" target="_blank">官网</a>
|
||||
@@ -13,8 +15,8 @@
|
||||
<p>
|
||||
|
||||
<p align="center">
|
||||
<a href="###"><img src="https://img.shields.io/github/license/fantastic-admin/basic?label=%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE&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=%E5%BD%93%E5%89%8D%E7%89%88%E6%9C%AC&style=flat-square" /></a>
|
||||
<a href="###"><img src="https://img.shields.io/github/license/fantastic-admin/basic?label=%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE&style=flat-square" alt="" /></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=%E5%BD%93%E5%89%8D%E7%89%88%E6%9C%AC&style=flat-square" alt="" /></a>
|
||||
</p>
|
||||
|
||||
## 特点
|
||||
@@ -37,8 +39,8 @@
|
||||
|
||||
- `main` Vue3 版本框架源码分支,不含示例代码,可直接用于实际开发
|
||||
- `example` Vue3 版本演示源码分支,同线上演示站,包含大量示例,可用于参考学习
|
||||
- `vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发(停止维护)
|
||||
- `vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习(停止维护)
|
||||
- ~~`vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发~~(停止维护)
|
||||
- ~~`vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习~~(停止维护)
|
||||
|
||||
## 预览
|
||||
|
||||
@@ -46,25 +48,27 @@
|
||||
|
||||
<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>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview1.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview3.png" alt="" /></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>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview4.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview5.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview6.png" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 支持
|
||||
|
||||
如果觉得 Fantastic-admin 这个框架不错,或者已经在使用了,希望你可以在 **Github** 或者 **Gitee(码云)** 帮我点个 ⭐ ,这将对我是极大的鼓励。
|
||||
如果觉得 Fantastic-admin 这个框架不错,或者已经在使用了,希望你可以在 **Github** / **Gitee** / **GitCode** 帮我点个 ⭐ ,这将对我是极大的鼓励。
|
||||
|
||||
[](https://github.com/fantastic-admin/basic)
|
||||
|
||||
[](https://gitee.com/fantastic-admin/basic)
|
||||
|
||||
[](https://atomgit.com/fantastic-admin/basic)
|
||||
|
||||
<details>
|
||||
<summary>Github Stars 曲线</summary>
|
||||
|
||||
@@ -94,18 +98,18 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
一款干啥都快人一步的 Vue 中后台管理系统框架
|
||||
一款干啥都快人一步的 Vue 后台管理系统框架
|
||||
</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>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview1.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview3.png" alt="" /></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>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview4.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview5.png" alt="" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview6.png" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -121,9 +125,9 @@
|
||||
</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>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview1.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview2.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview3.png" alt="" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview4.png" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
+1
-1
@@ -6,11 +6,11 @@ export default antfu(
|
||||
ignores: [
|
||||
'public',
|
||||
'dist*',
|
||||
'src/ui/shadcn/ui',
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'e18e/prefer-static-regex': 'off',
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'curly': ['error', 'all'],
|
||||
'ts/no-unused-expressions': ['error', {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="stylesheet" href="/browser_upgrade/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover"/>
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Cache-control" content="no-cache">
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@
|
||||
height: 100%;
|
||||
color: var(--rainbow-color);
|
||||
user-select: none;
|
||||
background-color: snow;
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
.loading-container *::before,
|
||||
|
||||
+75
-72
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "5.3.0",
|
||||
"version": "5.10.1",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,7 +13,7 @@
|
||||
"serve:test": "http-server ./dist-test -o",
|
||||
"svgo": "svgo -f src/assets/icons",
|
||||
"new": "plop",
|
||||
"generate:icons": "esno ./scripts/generate.icons.ts",
|
||||
"generate:icons": "tsx ./scripts/generate.icons.ts",
|
||||
"lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint",
|
||||
"lint:tsc": "vue-tsc -b",
|
||||
"lint:eslint": "eslint . --cache --fix",
|
||||
@@ -24,95 +25,97 @@
|
||||
"release": "bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vee-validate/zod": "^4.15.0",
|
||||
"@vueuse/components": "^13.1.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"axios": "^1.9.0",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/components": "^14.2.1",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"@vueuse/integrations": "^14.2.1",
|
||||
"axios": "^1.14.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.20",
|
||||
"defu": "^6.1.4",
|
||||
"disable-devtool": "^0.3.8",
|
||||
"element-plus": "^2.9.9",
|
||||
"eruda": "^3.4.1",
|
||||
"es-toolkit": "^1.36.0",
|
||||
"hotkeys-js": "^3.13.9",
|
||||
"lucide-vue-next": "^0.503.0",
|
||||
"disable-devtool": "^0.3.9",
|
||||
"element-plus": "^2.13.6",
|
||||
"eruda": "^3.4.3",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"filesize": "^11.0.15",
|
||||
"hotkeys-js": "^4.0.2",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"pinia": "^3.0.2",
|
||||
"qs": "^6.14.0",
|
||||
"reka-ui": "^2.2.0",
|
||||
"path-to-regexp": "^8.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"qs": "^6.15.0",
|
||||
"reka-ui": "^2.9.2",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"ua-parser-js": "^2.0.9",
|
||||
"vconsole": "^3.15.1",
|
||||
"vee-validate": "^4.15.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-sonner": "^1.3.2",
|
||||
"zod": "^3.24.3"
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "4.3.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@iconify/json": "^2.2.333",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@stylistic/stylelint-config": "^2.0.0",
|
||||
"@antfu/eslint-config": "^7.7.3",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"@faker-js/faker": "^10.4.0",
|
||||
"@iconify/json": "^2.2.456",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@stylistic/stylelint-config": "^5.0.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@unocss/eslint-plugin": "^66.0.0",
|
||||
"@unocss/preset-legacy-compat": "^66.0.0",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"@types/qs": "^6.15.0",
|
||||
"@unocss/eslint-plugin": "^66.6.7",
|
||||
"@unocss/preset-legacy-compat": "^66.6.7",
|
||||
"@unocss/reset": "^66.6.7",
|
||||
"@vitejs/plugin-legacy": "^8.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"boxen": "^8.0.1",
|
||||
"bumpp": "^10.1.0",
|
||||
"cz-git": "^1.11.1",
|
||||
"eslint": "^9.25.1",
|
||||
"esno": "^4.8.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"bumpp": "^11.0.1",
|
||||
"cz-git": "^1.12.0",
|
||||
"eslint": "^10.1.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"http-server": "^14.1.1",
|
||||
"inquirer": "^12.6.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"lint-staged": "^16.4.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"picocolors": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"postcss": "^8.5.3",
|
||||
"plop": "^4.0.5",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"sass-embedded": "^1.87.0",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"stylelint": "^17.6.0",
|
||||
"stylelint-config-recess-order": "^7.7.0",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-scss": "^6.11.1",
|
||||
"svgo": "^3.3.2",
|
||||
"taze": "^19.0.4",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^66.0.0",
|
||||
"unocss-preset-animations": "^1.2.1",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-turbo-console": "^2.1.3",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vite": "^6.2.7",
|
||||
"vite-plugin-app-loading": "^0.3.1",
|
||||
"vite-plugin-archiver": "^0.1.2",
|
||||
"stylelint-scss": "^7.0.0",
|
||||
"svgo": "^4.0.1",
|
||||
"taze": "^19.10.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.2",
|
||||
"unocss": "^66.6.7",
|
||||
"unocss-preset-animations": "^1.3.0",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-turbo-console": "^2.3.0",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.3",
|
||||
"vite-plugin-app-loading": "^0.4.0",
|
||||
"vite-plugin-archiver": "^0.2.0",
|
||||
"vite-plugin-banner": "^0.8.1",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite-plugin-compression2": "^2.5.3",
|
||||
"vite-plugin-env-parse": "^1.0.15",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-pages": "^0.33.0",
|
||||
"vite-plugin-fake-server": "^2.2.3",
|
||||
"vite-plugin-pages": "^0.33.3",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vite-plugin-vue-meta-layouts": "^0.5.1",
|
||||
"vue-tsc": "^2.2.10"
|
||||
"vite-plugin-vue-devtools": "^8.1.1",
|
||||
"vite-plugin-vue-meta-layouts": "^0.6.1",
|
||||
"vue-tsc": "^3.2.6"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const use{{ properCase name }}Store = defineStore(
|
||||
export const use{{ properCase name }}Store = defineStore(
|
||||
// 唯一ID
|
||||
'{{ camelCase name }}',
|
||||
() => {
|
||||
@@ -9,5 +9,3 @@ const use{{ properCase name }}Store = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default use{{ properCase name }}Store
|
||||
|
||||
Generated
+10429
-4468
File diff suppressed because it is too large
Load Diff
+52
-19
@@ -1,8 +1,8 @@
|
||||
import { exec } from 'node:child_process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import * as p from '@clack/prompts'
|
||||
import { lookupCollection, lookupCollections } from '@iconify/json'
|
||||
import { checkbox, confirm } from '@inquirer/prompts'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
// 拿到全部图标集的原始数据
|
||||
@@ -11,32 +11,48 @@ const raw = await lookupCollections()
|
||||
let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8')
|
||||
lastChoose = JSON.parse(lastChoose)
|
||||
|
||||
// 取出可使用的图标集数据用于 inquirer 选择,并按名称排序
|
||||
// 取出可使用的图标集数据用于选择,并按名称排序
|
||||
const collections = Object.entries(raw).map(([id, item]) => ({
|
||||
...item,
|
||||
id,
|
||||
})).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
p.intro('图标集生成工具')
|
||||
|
||||
/**
|
||||
* 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用
|
||||
* (1) src/iconify/index.json 记录用户 inquirer 的交互信息
|
||||
* (1) src/iconify/index.json 记录用户交互信息
|
||||
* (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名
|
||||
* (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用
|
||||
*/
|
||||
const answers = {
|
||||
collections: await checkbox({
|
||||
message: '请选择需要生成的图标集',
|
||||
choices: collections.map(item => ({
|
||||
name: `${item.name} (${item.total} icons)`,
|
||||
value: item.id,
|
||||
checked: lastChoose.collections.includes(item.id),
|
||||
})),
|
||||
}),
|
||||
isOfflineUse: await confirm({
|
||||
message: '是否需要离线使用',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
const answers = await p.group(
|
||||
{
|
||||
collections: () =>
|
||||
p.multiselect({
|
||||
message: '请选择需要生成的图标集',
|
||||
options: collections.map(item => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
hint: `${item.total} 个图标`,
|
||||
})),
|
||||
initialValues: lastChoose.collections,
|
||||
}),
|
||||
isOfflineUse: () =>
|
||||
p.confirm({
|
||||
message: '是否需要离线使用',
|
||||
initialValue: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
p.cancel('操作已取消')
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const spinner = p.spinner()
|
||||
spinner.start('正在生成图标集...')
|
||||
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/index.json'),
|
||||
@@ -72,4 +88,21 @@ await fs.writeJSON(
|
||||
collectionsMeta,
|
||||
)
|
||||
|
||||
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
|
||||
// 使用 spawn 运行 eslint
|
||||
const eslint = spawn('eslint', ['src/iconify/data.json', 'src/iconify/index.json', '--cache', '--fix'], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
})
|
||||
|
||||
eslint.on('error', (err) => {
|
||||
p.log.error(`ESLint 执行失败: ${err.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
eslint.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
p.log.error(`ESLint 执行失败,退出码: ${code}`)
|
||||
process.exit(code)
|
||||
}
|
||||
spinner.stop('图标集生成完成!')
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { ua } from '@/utils/ua'
|
||||
import Provider from './ui/provider/index.vue'
|
||||
|
||||
|
||||
+67
-28
@@ -1,8 +1,19 @@
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
// 请求重试配置
|
||||
const MAX_RETRY_COUNT = 3 // 最大重试次数
|
||||
const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)
|
||||
|
||||
// 扩展 AxiosRequestConfig 类型
|
||||
declare module 'axios' {
|
||||
export interface AxiosRequestConfig {
|
||||
retry?: boolean
|
||||
retryCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY) ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 1000 * 60,
|
||||
@@ -29,32 +40,12 @@ api.interceptors.request.use(
|
||||
},
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: {} }
|
||||
* 规则是当 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)
|
||||
}
|
||||
}
|
||||
else {
|
||||
useUserStore().requestLogout()
|
||||
}
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 401) {
|
||||
useUserStore().requestLogout()
|
||||
throw error
|
||||
}
|
||||
// 处理错误信息的函数
|
||||
function handleError(error: any) {
|
||||
if (error.status === 401) {
|
||||
useUserStore().requestLogout()
|
||||
}
|
||||
else {
|
||||
let message = error.message
|
||||
if (message === 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
@@ -68,7 +59,55 @@ api.interceptors.response.use(
|
||||
toast.error('Error', {
|
||||
description: message,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: {} }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
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 {
|
||||
return Promise.reject(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)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.0 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200.781" height="200" class="icon" viewBox="0 0 1028 1024"><path d="M989.867 234.667H499.2c-17.067 0-34.133-21.334-34.133-42.667 0-25.6 12.8-42.667 34.133-42.667h490.667c17.066 0 34.133 17.067 34.133 42.667 0 21.333-12.8 42.667-34.133 42.667zm-473.6 128h465.066c25.6 0 46.934 21.333 46.934 42.666 0 25.6-21.334 42.667-46.934 42.667H516.267c-25.6 0-46.934-17.067-46.934-42.667s21.334-42.666 46.934-42.666zm0 298.666c-25.6 0-46.934-21.333-46.934-42.666 0-25.6 21.334-42.667 46.934-42.667h465.066c25.6 0 46.934 17.067 46.934 42.667s-21.334 42.666-46.934 42.666H516.267zm4.266 128H972.8c29.867 0 51.2 17.067 51.2 42.667s-21.333 42.667-51.2 42.667H520.533c-29.866 0-51.2-17.067-51.2-42.667s21.334-42.667 51.2-42.667zm-192 25.6c-17.066 17.067-46.933 17.067-64 0L12.8 541.867c-17.067-17.067-17.067-51.2 0-68.267l251.733-273.067c17.067-17.066 46.934-17.066 64 0s17.067 51.2 0 68.267L106.667 507.733l221.866 238.934c17.067 21.333 17.067 51.2 0 68.266z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200.781" height="200" class="icon" viewBox="0 0 1028 1024"><path d="M989.867 234.667H499.2c-17.067 0-34.133-21.334-34.133-42.667 0-25.6 12.8-42.667 34.133-42.667h490.667c17.066 0 34.133 17.067 34.133 42.667 0 21.333-12.8 42.667-34.133 42.667m-473.6 128h465.066c25.6 0 46.934 21.333 46.934 42.666 0 25.6-21.334 42.667-46.934 42.667H516.267c-25.6 0-46.934-17.067-46.934-42.667s21.334-42.666 46.934-42.666m0 298.666c-25.6 0-46.934-21.333-46.934-42.666 0-25.6 21.334-42.667 46.934-42.667h465.066c25.6 0 46.934 17.067 46.934 42.667s-21.334 42.666-46.934 42.666zm4.266 128H972.8c29.867 0 51.2 17.067 51.2 42.667s-21.333 42.667-51.2 42.667H520.533c-29.866 0-51.2-17.067-51.2-42.667s21.334-42.667 51.2-42.667m-192 25.6c-17.066 17.067-46.933 17.067-64 0L12.8 541.867c-17.067-17.067-17.067-51.2 0-68.267l251.733-273.067c17.067-17.066 46.934-17.066 64 0s17.067 51.2 0 68.267L106.667 507.733l221.866 238.934c17.067 21.333 17.067 51.2 0 68.266"/></svg>
|
||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 986 B |
@@ -1,5 +1,7 @@
|
||||
/* 页面布局 CSS 变量 */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* 头部高度 */
|
||||
--g-header-height: 60px;
|
||||
|
||||
@@ -16,44 +18,51 @@
|
||||
|
||||
/* 工具栏高度 */
|
||||
--g-toolbar-height: 50px;
|
||||
|
||||
/* 滚动条颜色 */
|
||||
--scrollbar-color: 240 5.9% 90%;
|
||||
|
||||
&.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--scrollbar-color: 240 3.7% 15.9%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 明暗模式 CSS 变量 */
|
||||
/* stylelint-disable-next-line no-duplicate-selectors */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&::view-transition-old(root),
|
||||
&::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
::view-transition-old(root) {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark {
|
||||
&::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
&::view-transition-old(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
scrollbar-color: hsl(var(--scrollbar-color)) transparent;
|
||||
scrollbar-width: thin;
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.disable-color-scheme-transition-duration *,
|
||||
.disable-color-scheme-transition-duration *::before,
|
||||
.disable-color-scheme-transition-duration *::after {
|
||||
transition-duration: 0ms !important;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -64,9 +73,6 @@ body {
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--background));
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import Profile from './profile.vue'
|
||||
@@ -41,7 +39,7 @@ const isProfileShow = ref(false)
|
||||
],
|
||||
]" class="flex-center"
|
||||
>
|
||||
<template #label>
|
||||
<template #header>
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs text-secondary-foreground/50 font-light">
|
||||
当前登录账号
|
||||
@@ -67,7 +65,7 @@ const isProfileShow = ref(false)
|
||||
<FaAvatar :src="userStore.avatar" :class="cn('size-6', { 'size-full': onlyAvatar })">
|
||||
<FaIcon name="i-carbon:user-avatar-filled" class="size-6 text-secondary-foreground/50" />
|
||||
</FaAvatar>
|
||||
<div v-if="settingsStore.mode === 'pc' && !onlyAvatar" class="min-w-0 flex-center-between flex-1 gap-2">
|
||||
<div v-if="!onlyAvatar" class="min-w-0 flex-center-between flex-1 gap-2">
|
||||
<div class="flex-1 truncate text-start">
|
||||
{{ userStore.account }}
|
||||
</div>
|
||||
@@ -75,7 +73,7 @@ const isProfileShow = ref(false)
|
||||
</div>
|
||||
</FaButton>
|
||||
</FaDropdown>
|
||||
<FaModal v-model="isProfileShow" align-center :header="false" :footer="false" :close-on-click-overlay="false" :close-on-press-escape="false" class="h-500px min-w-600px overflow-hidden" content-class="min-h-full p-0 flex">
|
||||
<FaModal v-model="isProfileShow" align-center :header="false" :footer="false" :close-on-click-overlay="false" :close-on-press-escape="false" class="h-500px max-w-xl overflow-hidden" content-class="min-h-full p-0 flex">
|
||||
<Profile />
|
||||
</FaModal>
|
||||
</template>
|
||||
|
||||
@@ -16,8 +16,8 @@ const tabs = ref([
|
||||
|
||||
<template>
|
||||
<div class="min-h-full w-full">
|
||||
<div class="fixed inset-s-0 bottom-0 top-0 h-full w-40 border-e">
|
||||
<div v-for="(tab, index) in tabs" :key="index" class="cursor-pointer px-4 py-3 transition-background-color space-y-2 hover-bg-accent/50" :class="{ 'bg-accent hover-bg-accent!': active === index }" @click="active = index">
|
||||
<div class="fixed right-0 top-0 z-1 flex flex-row overflow-auto border-b border-e bg-background md:(inset-s-0 bottom-0 h-full w-40 flex-col)">
|
||||
<div v-for="(tab, index) in tabs" :key="index" class="flex-shrink-0 cursor-pointer px-4 py-3 transition-background-color space-y-2 hover-bg-accent/50" :class="{ 'bg-accent hover-bg-accent!': active === index }" @click="active = index">
|
||||
<div class="text-base text-accent-foreground leading-tight">
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ const tabs = ref([
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-40 min-h-full flex-col-center p-10">
|
||||
<div class="min-h-full flex-col-center p-10 pt-20 md:(ms-40 pt-10)">
|
||||
<div v-if="active === 0">
|
||||
请开发者自行扩展
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
import * as z from 'zod'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'EditPasswordForm',
|
||||
@@ -56,7 +55,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入原密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请输入原密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
@@ -66,7 +65,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, errors }" name="newPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
@@ -76,7 +75,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, errors }" name="checkPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请确认新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请确认新密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginForm',
|
||||
@@ -84,7 +83,7 @@ function testAccount(account: string) {
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
@@ -94,7 +93,7 @@ function testAccount(account: string) {
|
||||
<FormField v-slot="{ componentField, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'RegisterForm',
|
||||
@@ -23,7 +23,6 @@ const form = useForm({
|
||||
validationSchema: toTypedSchema(
|
||||
z.object({
|
||||
account: z.string().min(1, '请输入用户名'),
|
||||
captcha: z.string().min(6, '请输入验证码'),
|
||||
password: z.string().min(1, '请输入密码').min(6, '密码长度为6到18位').max(18, '密码长度为6到18位'),
|
||||
checkPassword: z.string().min(1, '请再次输入密码'),
|
||||
}).refine(data => data.password === data.checkPassword, {
|
||||
@@ -33,7 +32,6 @@ const form = useForm({
|
||||
),
|
||||
initialValues: {
|
||||
account: props.account ?? '',
|
||||
captcha: '',
|
||||
password: '',
|
||||
checkPassword: '',
|
||||
},
|
||||
@@ -58,7 +56,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
@@ -68,7 +66,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, value, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<FaPasswordStrength :password="value" class="mt-2" />
|
||||
@@ -81,7 +79,7 @@ const onSubmit = form.handleSubmit((values) => {
|
||||
<FormField v-slot="{ componentField, errors }" name="checkPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请再次输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请再次输入密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPasswordForm',
|
||||
@@ -63,7 +63,7 @@ function handleSendCaptcha() {
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
@@ -88,7 +88,7 @@ function handleSendCaptcha() {
|
||||
<FormField v-slot="{ componentField, errors }" name="newPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length > 0 && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
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) {
|
||||
ElMessage.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
|
||||
const onExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.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,63 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ImagePreview',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
src: string
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
}>(),
|
||||
{
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
)
|
||||
|
||||
const realWidth = computed(() => {
|
||||
return typeof props.width === 'string' ? props.width : `${props.width}px`
|
||||
})
|
||||
|
||||
const realHeight = computed(() => {
|
||||
return typeof props.height === 'string' ? props.height : `${props.height}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElImage :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<FaIcon name="i-ph:image-broken-duotone" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-image {
|
||||
background-color: var(--el-fill-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 30px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
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) {
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.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 { ElMessage } from 'element-plus'
|
||||
|
||||
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) {
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.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>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import settingsDefault from '@/settings.default'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { diffTwoObj } from '@/utils/object'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { toast } from 'vue-sonner'
|
||||
import settingsDefault from '@/settings.default'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { diffTwoObj } from '@/utils/object'
|
||||
|
||||
defineOptions({
|
||||
name: 'AppSetting',
|
||||
@@ -58,7 +56,7 @@ function handleCopy() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块" :footer="isSupported" content-class="sm:min-w-md">
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块" :footer="isSupported" :destroy-on-close="false" content-class="sm:min-w-md">
|
||||
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
|
||||
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
defineOptions({
|
||||
@@ -21,7 +19,7 @@ const { switchTo } = useMenu()
|
||||
<component :is="useSlots('header-start')" />
|
||||
<Logo class="title" />
|
||||
<component :is="useSlots('header-after-logo')" />
|
||||
<FaScrollArea :scrollbar="false" mask horizontal gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1">
|
||||
<FaScrollArea :scrollbar="false" mask horizontal gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1 overscroll-contain">
|
||||
<!-- 顶部模式 -->
|
||||
<div class="menu h-full flex of-hidden transition-all">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
@@ -31,7 +29,7 @@ const { switchTo } = useMenu()
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-3 text-[var(--g-header-menu-color)] transition-all hover-(bg-[var(--g-header-menu-hover-bg)] text-[var(--g-header-menu-hover-color)])" :class="{
|
||||
v-if="item.children && item.children.length !== 0" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-3 text-[var(--g-header-menu-color)] transition-colors hover-(bg-[var(--g-header-menu-hover-bg)] text-[var(--g-header-menu-hover-color)])" :class="{
|
||||
'text-[var(--g-header-menu-active-color)]! bg-[var(--g-header-menu-active-bg)]!': index === menuStore.actived,
|
||||
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import imgLogo from '@/assets/images/logo.svg'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'Logo',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
defineOptions({
|
||||
@@ -40,7 +38,7 @@ onUnmounted(() => {
|
||||
<component :is="useSlots('main-sidebar-top')" />
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<component :is="useSlots('main-sidebar-after-logo')" />
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-main-sidebar-bg)" class="menu">
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-main-sidebar-bg)" class="menu flex-1 overscroll-contain">
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="w-full flex flex-col of-hidden py-1 transition-all -mt-2">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
@@ -50,7 +48,7 @@ onUnmounted(() => {
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg py-4 text-[var(--g-main-sidebar-menu-color)] transition-all hover-(bg-[var(--g-main-sidebar-menu-hover-bg)] text-[var(--g-main-sidebar-menu-hover-color)]) px-2!" :class="{
|
||||
v-if="item.children && item.children.length !== 0" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg py-4 text-[var(--g-main-sidebar-menu-color)] transition-colors hover-(bg-[var(--g-main-sidebar-menu-hover-bg)] text-[var(--g-main-sidebar-menu-hover-color)]) px-2!" :class="{
|
||||
'text-[var(--g-main-sidebar-menu-active-color)]! bg-[var(--g-main-sidebar-menu-active-bg)]!': index === menuStore.actived,
|
||||
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
|
||||
>
|
||||
@@ -92,8 +90,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex: 1;
|
||||
|
||||
:deep(.menu-item) {
|
||||
.menu-item-container {
|
||||
padding-block: 8px;
|
||||
|
||||
@@ -20,6 +20,16 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
// 用于缓存对象到 ID 的映射
|
||||
const idMap = new WeakMap<object, string>()
|
||||
// 获取对象的唯一 ID,如果已存在则返回缓存的 ID,否则生成新的
|
||||
function getUseId(obj: object): string {
|
||||
if (!idMap.has(obj)) {
|
||||
idMap.set(obj, useId())
|
||||
}
|
||||
return idMap.get(obj)!
|
||||
}
|
||||
|
||||
const activeIndex = ref<MenuInjection['activeIndex']>(props.value)
|
||||
const items = ref<MenuInjection['items']>({})
|
||||
const subMenus = ref<MenuInjection['subMenus']>({})
|
||||
@@ -32,8 +42,8 @@ const isMenuPopup = computed<MenuInjection['isMenuPopup']>(() => {
|
||||
// 解析传入的 menu 数据,并保存到 items 和 subMenus 对象中
|
||||
function initItems(menu: MenuProps['menu'], parentPaths: string[] = []) {
|
||||
menu.forEach((item) => {
|
||||
const index = item.path ?? JSON.stringify(item)
|
||||
if (item.children) {
|
||||
const index = item.path ?? getUseId(item)
|
||||
if (item.children?.some(item => item.meta?.menu !== false)) {
|
||||
const indexPath = [...parentPaths, index]
|
||||
subMenus.value[index] = {
|
||||
index,
|
||||
@@ -144,6 +154,7 @@ watch(() => props.collapse, (value) => {
|
||||
|
||||
provide(rootMenuInjectionKey, reactive({
|
||||
props,
|
||||
getUseId,
|
||||
items,
|
||||
subMenus,
|
||||
activeIndex,
|
||||
@@ -159,15 +170,15 @@ provide(rootMenuInjectionKey, reactive({
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('h-full w-full flex flex-col of-hidden transition-all-300', {
|
||||
:class="cn('h-full w-full flex flex-col of-hidden', {
|
||||
'flex-row w-auto': isMenuPopup && props.mode === 'horizontal',
|
||||
'py-1': props.mode === 'vertical',
|
||||
})"
|
||||
>
|
||||
<template v-for="item in menu" :key="item.path ?? JSON.stringify(item)">
|
||||
<template v-for="item in menu" :key="item.path ?? getUseId(item)">
|
||||
<template v-if="item.meta?.menu !== false">
|
||||
<SubMenu v-if="item.children?.length" :menu="item" :unique-key="[item.path ?? JSON.stringify(item)]" />
|
||||
<Item v-else :item="item" :unique-key="[item.path ?? JSON.stringify(item)]" @click="handleMenuItemClick(item.path ?? JSON.stringify(item))" />
|
||||
<SubMenu v-if="item.children?.length" :menu="item" :unique-key="[item.path ?? getUseId(item)]" />
|
||||
<Item v-else :item="item" :unique-key="[item.path ?? getUseId(item)]" @click="handleMenuItemClick(item.path ?? getUseId(item))" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="itemRef" :class="cn('menu-item relative transition-all', {
|
||||
ref="itemRef" :class="cn('menu-item relative', {
|
||||
'active': isItemActive,
|
||||
'py-1 px-2': (rootMenu.isMenuPopup && rootMenu.props.mode === 'vertical') || (rootMenu.isMenuPopup && level !== 0 && rootMenu.props.mode === 'horizontal') || !rootMenu.isMenuPopup,
|
||||
'px-1 py-2': rootMenu.isMenuPopup && level === 0 && rootMenu.props.mode === 'horizontal',
|
||||
@@ -52,7 +52,7 @@ defineExpose({
|
||||
target: item.meta?.link ? '_blank' : '_self',
|
||||
class: 'no-underline',
|
||||
}),
|
||||
}" :class="cn('group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-4 py-3 text-[var(--g-sub-sidebar-menu-color)] transition-all hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])', {
|
||||
}" :class="cn('group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-4 py-3 text-[var(--g-sub-sidebar-menu-color)] transition-colors hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])', {
|
||||
'text-[var(--g-sub-sidebar-menu-active-color)]! bg-[var(--g-sub-sidebar-menu-active-bg)]!': isItemActive,
|
||||
'px-3': rootMenu.isMenuPopup && level === 0,
|
||||
})" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" v-on="{
|
||||
|
||||
+141
-122
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { SubMenuProps } from './types'
|
||||
import { cn } from '@/utils'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { cn } from '@/utils'
|
||||
import Item from './item.vue'
|
||||
import { rootMenuInjectionKey } from './types'
|
||||
|
||||
@@ -16,108 +16,113 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const index = props.menu.path ?? JSON.stringify(props.menu)
|
||||
const itemRef = useTemplateRef('itemRef')
|
||||
const subMenuRef = useTemplateRef('subMenuRef')
|
||||
const rootMenu = inject(rootMenuInjectionKey)!
|
||||
|
||||
const opened = computed(() => {
|
||||
return rootMenu.openedMenus.includes(props.uniqueKey.at(-1)!)
|
||||
})
|
||||
const index = props.menu.path ?? rootMenu.getUseId(props.menu)
|
||||
|
||||
const opened = computed(() => rootMenu.openedMenus.includes(props.uniqueKey.at(-1)!))
|
||||
|
||||
const transitionEvent = computed(() => {
|
||||
return rootMenu.isMenuPopup
|
||||
? {
|
||||
enter(el: HTMLElement) {
|
||||
if (el.offsetHeight > window.innerHeight) {
|
||||
el.style.height = `${window.innerHeight}px`
|
||||
}
|
||||
},
|
||||
afterEnter: () => {},
|
||||
beforeLeave: (el: HTMLElement) => {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
leave: (el: HTMLElement) => {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
: CSS.supports('height', 'calc-size(auto, size)')
|
||||
? {}
|
||||
: {
|
||||
enter(el: HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
el.dataset.height = el.offsetHeight.toString()
|
||||
el.style.maxHeight = '0'
|
||||
void el.offsetHeight
|
||||
el.style.maxHeight = `${el.dataset.height}px`
|
||||
el.style.overflow = 'hidden'
|
||||
})
|
||||
},
|
||||
afterEnter(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
enterCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
beforeLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
leave(el: HTMLElement) {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
leaveCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
if (rootMenu.isMenuPopup) {
|
||||
return {
|
||||
enter(el: HTMLElement) {
|
||||
if (el.offsetHeight > window.innerHeight) {
|
||||
el.style.height = `${window.innerHeight}px`
|
||||
}
|
||||
},
|
||||
afterEnter: () => {},
|
||||
beforeLeave: (el: HTMLElement) => {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
leave: (el: HTMLElement) => {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
}
|
||||
if (CSS.supports('height', 'calc-size(auto, size)')) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
enter(el: HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
el.dataset.height = el.offsetHeight.toString()
|
||||
el.style.maxHeight = '0'
|
||||
void el.offsetHeight
|
||||
el.style.maxHeight = `${el.dataset.height}px`
|
||||
el.style.overflow = 'hidden'
|
||||
})
|
||||
},
|
||||
afterEnter(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
enterCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
beforeLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
leave(el: HTMLElement) {
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
afterLeave(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
leaveCancelled(el: HTMLElement) {
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = ''
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const transitionClass = computed(() => {
|
||||
return rootMenu.isMenuPopup
|
||||
? {
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: 'opacity-0 translate-x-4',
|
||||
enterToClass: 'opacity-100',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0',
|
||||
}
|
||||
: {
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
enterToClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveToClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
}
|
||||
if (rootMenu.isMenuPopup) {
|
||||
return {
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: 'opacity-0 translate-x-4',
|
||||
enterToClass: 'opacity-100',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0',
|
||||
}
|
||||
}
|
||||
return {
|
||||
enterActiveClass: 'ease-in-out duration-300',
|
||||
enterFromClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
enterToClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveActiveClass: 'ease-in-out duration-300',
|
||||
leaveFromClass: 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||
leaveToClass: cn('opacity-0 translate-y-4 scale-95 blur-4', CSS.supports('height', 'calc-size(auto, size)') && 'h-0'),
|
||||
}
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
return props.menu.children?.some((item: any) => item.meta?.menu !== false) ?? false
|
||||
})
|
||||
const hasChildren = computed(() => props.menu.children?.some((item: any) => item.meta?.menu !== false) ?? false)
|
||||
|
||||
function handleClick() {
|
||||
if (rootMenu.isMenuPopup && hasChildren.value) {
|
||||
if (
|
||||
(rootMenu.isMenuPopup && hasChildren.value)
|
||||
|| props.menu.meta?.link
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (hasChildren.value) {
|
||||
rootMenu.handleSubMenuClick(index, props.uniqueKey)
|
||||
}
|
||||
else {
|
||||
rootMenu.handleMenuItemClick(index)
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (hasChildren.value) {
|
||||
rootMenu.handleSubMenuClick(index, props.uniqueKey)
|
||||
}
|
||||
else {
|
||||
rootMenu.handleMenuItemClick(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let timeout: (() => void) | undefined
|
||||
@@ -132,32 +137,44 @@ function handleMouseenter() {
|
||||
if (hasChildren.value) {
|
||||
rootMenu.openMenu(index, props.uniqueKey)
|
||||
nextTick(() => {
|
||||
const el = itemRef.value?.ref
|
||||
const subMenuEl = subMenuRef.value?.$el
|
||||
if (!el || !subMenuEl) {
|
||||
return
|
||||
}
|
||||
let top = 0
|
||||
let left = 0
|
||||
if (rootMenu.props.mode === 'vertical' || props.level !== 0) {
|
||||
top = el.getBoundingClientRect().top + el.scrollTop
|
||||
left = el.getBoundingClientRect().left + el.getBoundingClientRect().width
|
||||
if (top + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
top = Math.max(0, window.innerHeight - subMenuEl.offsetHeight)
|
||||
requestAnimationFrame(() => {
|
||||
const el = itemRef.value?.ref
|
||||
const subMenuEl = subMenuRef.value?.$el
|
||||
if (!el || !subMenuEl) {
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
top = el.getBoundingClientRect().top + el.getBoundingClientRect().height
|
||||
left = el.getBoundingClientRect().left
|
||||
if (top + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
subMenuEl.style.height = `${window.innerHeight - top}px`
|
||||
const rect = el.getBoundingClientRect()
|
||||
const { top, left, width, height } = rect
|
||||
let menuTop = 0
|
||||
let menuLeft = 0
|
||||
if (rootMenu.props.mode === 'vertical' || props.level !== 0) {
|
||||
menuTop = top + el.scrollTop
|
||||
menuLeft = left + width
|
||||
// 处理边界情况
|
||||
if (menuTop + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
menuTop = Math.max(0, window.innerHeight - subMenuEl.offsetHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (left + subMenuEl.offsetWidth > document.documentElement.clientWidth) {
|
||||
left = el.getBoundingClientRect().left - el.getBoundingClientRect().width
|
||||
}
|
||||
subMenuEl.style.top = `${top}px`
|
||||
subMenuEl.style.insetInlineStart = `${left}px`
|
||||
else {
|
||||
menuTop = top + height
|
||||
menuLeft = left
|
||||
// 处理边界情况
|
||||
if (menuTop + subMenuEl.offsetHeight > window.innerHeight) {
|
||||
subMenuEl.style.height = `${window.innerHeight - menuTop}px`
|
||||
}
|
||||
}
|
||||
// 处理边界情况
|
||||
if (menuLeft + subMenuEl.offsetWidth > document.documentElement.clientWidth) {
|
||||
menuLeft = left - width
|
||||
}
|
||||
// 设置样式
|
||||
Object.assign(subMenuEl.style, {
|
||||
top: `${menuTop}px`,
|
||||
insetInlineStart: `${menuLeft}px`,
|
||||
willChange: 'transform',
|
||||
transform: 'translateZ(0)',
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
@@ -174,14 +191,16 @@ function handleMouseleave() {
|
||||
rootMenu.mouseInMenu = []
|
||||
timeout?.()
|
||||
;({ stop: timeout } = useTimeoutFn(() => {
|
||||
if (rootMenu.mouseInMenu.length === 0) {
|
||||
rootMenu.closeMenu(props.uniqueKey)
|
||||
}
|
||||
else {
|
||||
if (hasChildren.value) {
|
||||
!rootMenu.mouseInMenu.includes(props.uniqueKey.at(-1)!) && rootMenu.closeMenu(props.uniqueKey.at(-1)!)
|
||||
requestAnimationFrame(() => {
|
||||
if (rootMenu.mouseInMenu.length === 0) {
|
||||
rootMenu.closeMenu(props.uniqueKey)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (hasChildren.value) {
|
||||
!rootMenu.mouseInMenu.includes(props.uniqueKey.at(-1)!) && rootMenu.closeMenu(props.uniqueKey.at(-1)!)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 300))
|
||||
}
|
||||
</script>
|
||||
@@ -191,15 +210,15 @@ function handleMouseleave() {
|
||||
<Teleport v-if="hasChildren" to="body" :disabled="!rootMenu.isMenuPopup">
|
||||
<Transition v-bind="transitionClass" v-on="transitionEvent">
|
||||
<FaScrollArea
|
||||
v-if="opened" ref="subMenuRef" :scrollbar="false" :mask="rootMenu.isMenuPopup" :class="cn('sub-menu static h-[calc-size(auto,size)] rounded-lg', {
|
||||
v-if="opened" ref="subMenuRef" :scrollbar="false" :mask="rootMenu.isMenuPopup" :class="cn('sub-menu static h-[calc-size(auto,size)] rounded-lg will-change-transform', {
|
||||
'bg-[var(--g-sub-sidebar-bg)]': rootMenu.isMenuPopup,
|
||||
'border shadow-xl fixed z-3000 w-[200px]': rootMenu.isMenuPopup,
|
||||
'mx-1': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
|
||||
'py-1': rootMenu.isMenuPopup,
|
||||
'py-1 overscroll-contain': rootMenu.isMenuPopup,
|
||||
})"
|
||||
>
|
||||
<template v-for="item in menu.children" :key="item.path ?? JSON.stringify(item)">
|
||||
<SubMenu v-if="item.meta?.menu !== false" :unique-key="[...uniqueKey, item.path ?? JSON.stringify(item)]" :menu="item" :level="level + 1" />
|
||||
<template v-for="item in menu.children" :key="item.path ?? rootMenu.getUseId(item)">
|
||||
<SubMenu v-if="item.meta?.menu !== false" :unique-key="[...uniqueKey, item.path ?? rootMenu.getUseId(item)]" :menu="item" :level="level + 1" />
|
||||
</template>
|
||||
</FaScrollArea>
|
||||
</Transition>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface MenuProps {
|
||||
|
||||
export interface MenuInjection {
|
||||
props: MenuProps
|
||||
getUseId: (obj: object) => string
|
||||
items: Record<string, MenuItem>
|
||||
subMenus: Record<string, MenuItem>
|
||||
activeIndex: MenuProps['value']
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
import Menu from '../Menu/index.vue'
|
||||
|
||||
@@ -56,7 +54,7 @@ watch(() => menuStore.actived, (val, oldVal) => {
|
||||
}"
|
||||
/>
|
||||
<component :is="useSlots('sub-sidebar-after-logo')" />
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-sub-sidebar-bg)" class="flex-1">
|
||||
<FaScrollArea :scrollbar="false" mask gradient-color="var(--g-sub-sidebar-bg)" class="flex-1 overscroll-contain">
|
||||
<TransitionGroup :name="transitionName">
|
||||
<template v-for="(mainItem, mainIndex) in menuStore.allMenus" :key="mainIndex">
|
||||
<div v-show="mainIndex === menuStore.actived">
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { Tabbar } from '#/global'
|
||||
import { useSlots } from '@/slots'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useTabbarStore from '@/store/modules/tabbar'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
name: 'Tabbar',
|
||||
@@ -30,10 +28,20 @@ const tabRef = useTemplateRef<HTMLElement[]>('tabRef')
|
||||
|
||||
watch(() => route, (val) => {
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
tabbarStore.add(val).then(() => {
|
||||
tabbarStore.add(val)
|
||||
nextTick(() => {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
|
||||
if (index !== -1) {
|
||||
tabRef.value && tabsRef.value?.scrollTo(tabRef.value[index].offsetLeft - tabsRef.value.ref?.$el.clientWidth * 0.4)
|
||||
const tabEl = tabRef.value?.find(item => Number.parseInt(item.dataset.index!) === index)
|
||||
const containerEl = tabsRef.value?.ref?.$el
|
||||
if (tabEl && containerEl) {
|
||||
const tabLeft = tabEl.offsetLeft
|
||||
const tabWidth = tabEl.offsetWidth
|
||||
const containerWidth = containerEl.clientWidth
|
||||
// 计算滚动位置,使标签页居中
|
||||
const scrollLeft = tabLeft - (containerWidth - tabWidth) / 2
|
||||
tabsRef.value?.scrollTo(scrollLeft)
|
||||
}
|
||||
tabbarScrollTip()
|
||||
}
|
||||
})
|
||||
@@ -75,16 +83,19 @@ function contextMenuItems(routeItem: Tabbar.recordRaw) {
|
||||
[
|
||||
{
|
||||
label: '关闭其他标签页',
|
||||
icon: 'i-mdi:close',
|
||||
disabled: !tabbar.checkCloseOtherSide(routeItem.tabId),
|
||||
handle: () => tabbar.closeOtherSide(routeItem.tabId),
|
||||
},
|
||||
{
|
||||
label: '关闭左侧标签页',
|
||||
icon: 'i-mdi:arrow-expand-left',
|
||||
disabled: !tabbar.checkCloseLeftSide(routeItem.tabId),
|
||||
handle: () => tabbar.closeLeftSide(routeItem.tabId),
|
||||
},
|
||||
{
|
||||
label: '关闭右侧标签页',
|
||||
icon: 'i-mdi:arrow-expand-right',
|
||||
disabled: !tabbar.checkCloseRightSide(routeItem.tabId),
|
||||
handle: () => tabbar.closeRightSide(routeItem.tabId),
|
||||
},
|
||||
@@ -92,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) {
|
||||
@@ -127,12 +166,19 @@ 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
|
||||
}
|
||||
// 切换到最后一个标签页
|
||||
case 'alt+0':
|
||||
router.push(tabbarStore.list[tabbarStore.list.length - 1].fullPath)
|
||||
{
|
||||
const last = tabbarStore.list.at(-1)
|
||||
if (last) {
|
||||
router.push(last.fullPath)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -147,7 +193,7 @@ onUnmounted(() => {
|
||||
<div class="tabbar">
|
||||
<component :is="useSlots('tabbar-start')" />
|
||||
<div class="tabbar-container">
|
||||
<FaScrollArea ref="tabsRef" horizontal :scrollbar="false" mask gradient-color="var(--g-tabbar-bg)" class="tabs">
|
||||
<FaScrollArea ref="tabsRef" :scrollbar="false" mask horizontal gradient-color="var(--g-tabbar-bg)" class="tabs">
|
||||
<TransitionGroup ref="tabContainerRef" name="tabbar" tag="div" class="tab-container">
|
||||
<div
|
||||
v-for="(element, index) in tabbarStore.list" :key="element.tabId"
|
||||
@@ -168,8 +214,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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { compile } from 'path-to-regexp'
|
||||
import Breadcrumb from '../../../Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '../../../Breadcrumb/item.vue'
|
||||
@@ -42,7 +41,7 @@ function pathCompile(path: string) {
|
||||
<template>
|
||||
<Breadcrumb v-if="settingsStore.mode === 'pc' && settingsStore.settings.app.routeBaseOn !== 'filesystem'" class="breadcrumb whitespace-nowrap px-2">
|
||||
<TransitionGroup name="breadcrumb">
|
||||
<BreadcrumbItem v-for="(item, index) in breadcrumbList" :key="`${index}_${item.path}_${item.title}`" :to="index < breadcrumbList.length - 1 && item.path !== '' ? pathCompile(item.path) : ''">
|
||||
<BreadcrumbItem v-for="(item, index) in breadcrumbList" :key="`${index}_${item.path}_${item.title}`" :to="Number(index) < breadcrumbList.length - 1 && item.path !== '' ? pathCompile(item.path) : ''">
|
||||
{{ item.title }}
|
||||
</BreadcrumbItem>
|
||||
</TransitionGroup>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'ColorScheme',
|
||||
})
|
||||
@@ -8,28 +6,40 @@ defineOptions({
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
function toggleColorScheme(event: MouseEvent) {
|
||||
const { startViewTransition } = useViewTransition(() => {
|
||||
if (!document.startViewTransition || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
settingsStore.currentColorScheme && settingsStore.setColorScheme(settingsStore.currentColorScheme === 'dark' ? 'light' : 'dark')
|
||||
return
|
||||
}
|
||||
const target = event.target as HTMLElement
|
||||
const { left, top, width, height } = target.getBoundingClientRect()
|
||||
const x = left + width / 2
|
||||
const y = top + height / 2
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)
|
||||
const ratioX = (100 * x) / innerWidth
|
||||
const ratioY = (100 * y) / innerHeight
|
||||
const referR = Math.hypot(innerWidth, innerHeight) / Math.SQRT2
|
||||
const ratioR = (100 * endRadius) / referR
|
||||
const transition = document.startViewTransition(async () => {
|
||||
settingsStore.currentColorScheme && settingsStore.setColorScheme(settingsStore.currentColorScheme === 'dark' ? 'light' : 'dark')
|
||||
await nextTick()
|
||||
})
|
||||
startViewTransition()?.ready.then(() => {
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)
|
||||
transition.ready.then(() => {
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
`circle(0% at ${ratioX}% ${ratioY}%)`,
|
||||
`circle(${ratioR}% at ${ratioX}% ${ratioY}%)`,
|
||||
]
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: settingsStore.settings.app.colorScheme !== 'dark' ? clipPath : clipPath.reverse(),
|
||||
clipPath: settingsStore.currentColorScheme === 'light' ? clipPath : clipPath.toReversed(),
|
||||
},
|
||||
{
|
||||
duration: 300,
|
||||
easing: 'ease-out',
|
||||
pseudoElement: settingsStore.settings.app.colorScheme !== 'dark' ? '::view-transition-new(root)' : '::view-transition-old(root)',
|
||||
duration: 500,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'both',
|
||||
pseudoElement: settingsStore.currentColorScheme === 'light' ? '::view-transition-new(root)' : '::view-transition-old(root)',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Menu } from '@/types/global'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
import { useFocus } from '@vueuse/core'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'Search',
|
||||
name: 'NavSearchModal',
|
||||
})
|
||||
|
||||
const isShow = defineModel<boolean>({
|
||||
@@ -33,7 +30,7 @@ const searchInput = ref('')
|
||||
const searchInputRef = useTemplateRef('searchInputRef')
|
||||
const { focused: searchInputFocused } = useFocus(searchInputRef)
|
||||
const sourceList = ref<listTypes[]>([])
|
||||
const actived = ref(-1)
|
||||
const actived = ref(0)
|
||||
|
||||
const searchResultRef = useTemplateRef('searchResultRef')
|
||||
const searchResultItemRef = useTemplateRef<HTMLElement[]>('searchResultItemRef')
|
||||
@@ -72,7 +69,7 @@ const resultList = computed(() => {
|
||||
watch(() => isShow.value, (val) => {
|
||||
if (val) {
|
||||
searchInput.value = ''
|
||||
actived.value = -1
|
||||
actived.value = 0
|
||||
// 当搜索显示的时候绑定上、下、回车快捷键,隐藏的时候再解绑。另外当 input 处于 focus 状态时,采用 vue 来绑定键盘事件
|
||||
hotkeys('up', keyUp)
|
||||
hotkeys('down', keyDown)
|
||||
@@ -92,7 +89,7 @@ watch(() => isShow.value, (val) => {
|
||||
}
|
||||
})
|
||||
watch(() => resultList.value, () => {
|
||||
actived.value = -1
|
||||
actived.value = 0
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
@@ -161,15 +158,13 @@ function keyDown() {
|
||||
}
|
||||
}
|
||||
function keyEnter() {
|
||||
if (actived.value !== -1) {
|
||||
searchResultItemRef.value?.find(item => Number.parseInt(item.dataset.index!) === actived.value)?.click()
|
||||
}
|
||||
searchResultItemRef.value?.find(item => Number.parseInt(item.dataset.index!) === actived.value)?.click()
|
||||
}
|
||||
function handleScroll() {
|
||||
if (searchResultRef.value?.areaRef?.ref?.el?.viewportElement) {
|
||||
const contentDom = searchResultRef.value.areaRef.ref.el.viewportElement
|
||||
if (searchResultRef.value?.ref?.el?.viewportElement) {
|
||||
const contentDom = searchResultRef.value.ref.el.viewportElement
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
if (resultList.value.length > 0) {
|
||||
scrollTo = contentDom.scrollTop
|
||||
const activedOffsetTop = searchResultItemRef.value?.find(item => Number.parseInt(item.dataset.index!) === actived.value)?.offsetTop ?? 0
|
||||
const activedClientHeight = searchResultItemRef.value?.find(item => Number.parseInt(item.dataset.index!) === actived.value)?.clientHeight ?? 0
|
||||
@@ -200,7 +195,7 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaModal ref="searchResultRef" v-model="isShow" border :footer="settingsStore.mode === 'pc'" :closable="false" class="w-full lg-max-w-2xl" content-class="flex flex-col p-0 min-h-auto" header-class="p-0" footer-class="p-0" @opened="searchInputFocused = true">
|
||||
<FaModal v-model="isShow" border :footer="settingsStore.mode === 'pc'" :closable="false" class="w-full lg-max-w-2xl" content-class="flex flex-col p-0 min-h-auto" header-class="p-0" footer-class="p-0" @opened="searchInputFocused = true">
|
||||
<template #header>
|
||||
<div class="h-12 flex flex-shrink-0 items-center">
|
||||
<div class="h-full w-14 flex-center">
|
||||
@@ -239,7 +234,7 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<FaScrollArea ref="searchResultRef">
|
||||
<template v-if="resultList.length > 0">
|
||||
<div v-for="(item, index) in resultList" ref="searchResultItemRef" :key="item.path" class="p-4" :data-index="index" @click="pageJump(item.path, item.link)" @mouseover="actived = index">
|
||||
<a class="flex cursor-pointer items-center border rounded-lg" :class="{ '-mt-4': index !== 0, 'bg-accent': index === actived }">
|
||||
@@ -271,6 +266,6 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
</FaModal>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -8,7 +8,7 @@ defineOptions({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar-container flex items-center justify-between">
|
||||
<div class="toolbar-container h-[var(--g-toolbar-height)] flex items-center justify-between bg-[var(--g-toolbar-bg)]">
|
||||
<div class="left-side h-full flex items-center of-hidden pe-16 ps-2">
|
||||
<LeftSide />
|
||||
</div>
|
||||
@@ -19,13 +19,7 @@ defineOptions({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-container {
|
||||
height: var(--g-toolbar-height);
|
||||
background-color: var(--g-toolbar-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.left-side {
|
||||
mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
}
|
||||
.left-side {
|
||||
mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Breadcrumb from './Breadcrumb/index.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import ColorScheme from './ColorScheme/index.vue'
|
||||
import Fullscreen from './Fullscreen/index.vue'
|
||||
import NavSearch from './NavSearch/index.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import Tabbar from './Tabbar/index.vue'
|
||||
import Toolbar from './Toolbar/index.vue'
|
||||
|
||||
@@ -20,11 +20,10 @@ const enableToolbar = computed(() => {
|
||||
|
||||
const scrollTop = ref(0)
|
||||
const scrollOnHide = ref(false)
|
||||
const topbarHeight = computed(() => {
|
||||
const tabbarHeight = settingsStore.settings.tabbar.enable ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-tabbar-height')) : 0
|
||||
const toolbarHeight = enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
|
||||
return tabbarHeight + toolbarHeight
|
||||
})
|
||||
|
||||
const topbarRef = useTemplateRef('topbarRef')
|
||||
const { height: topbarHeight } = useElementSize(topbarRef)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
@@ -42,7 +41,7 @@ watch(scrollTop, (val, oldVal) => {
|
||||
<template>
|
||||
<FaSmartFixedBlock position="top" :class="{ 'absolute!': settingsStore.settings.topbar.mode === 'static' }">
|
||||
<div
|
||||
class="topbar-container transition-[transform,box-shadow]-300" :class="{
|
||||
ref="topbarRef" class="topbar-container transition-[transform,box-shadow]-300" :class="{
|
||||
[`topbar-${settingsStore.settings.topbar.mode}`]: true,
|
||||
mask: scrollTop,
|
||||
hide: scrollOnHide,
|
||||
|
||||
+30
-47
@@ -1,8 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from '@/slots'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import Header from './components/Header/index.vue'
|
||||
@@ -22,51 +19,36 @@ const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
// 头部当前实际高度
|
||||
const headerActualHeight = computed(() => {
|
||||
let actualHeight = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-header-height'))
|
||||
if (['single', 'side'].includes(settingsStore.settings.menu.mode) || settingsStore.mode === 'mobile') {
|
||||
actualHeight = 0
|
||||
}
|
||||
return actualHeight
|
||||
// 头部是否隐藏
|
||||
const isHeaderHide = computed(() => {
|
||||
return ['single', 'side'].includes(settingsStore.settings.menu.mode) || settingsStore.mode === 'mobile'
|
||||
})
|
||||
|
||||
// 侧边栏主导航当前实际宽度
|
||||
const mainSidebarActualWidth = computed(() => {
|
||||
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
|
||||
if (settingsStore.settings.menu.mode === 'single' || (settingsStore.settings.menu.mode === 'head' && settingsStore.mode !== 'mobile')) {
|
||||
actualWidth = 0
|
||||
}
|
||||
return actualWidth
|
||||
// 侧边栏主导航是否隐藏
|
||||
const isMainSidebarHide = computed(() => {
|
||||
return settingsStore.settings.menu.mode === 'single'
|
||||
|| (settingsStore.settings.menu.mode === 'head' && settingsStore.mode !== 'mobile')
|
||||
})
|
||||
|
||||
// 侧边栏次导航当前实际宽度
|
||||
const subSidebarActualWidth = computed(() => {
|
||||
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
|
||||
if (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile') {
|
||||
actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-collapse-width'))
|
||||
}
|
||||
if (menuStore.sidebarMenus.every(item => item.meta?.menu === false)) {
|
||||
actualWidth = 0
|
||||
}
|
||||
return actualWidth
|
||||
// 侧边栏次导航是否隐藏
|
||||
const isSubSidebarHide = computed(() => {
|
||||
return menuStore.sidebarMenus.every(item => item.meta?.menu === false)
|
||||
})
|
||||
|
||||
// 顶栏当前实际高度
|
||||
const topbarActualHeight = computed(() => {
|
||||
let actualHeight = 0
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
actualHeight += Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-tabbar-height'))
|
||||
}
|
||||
if (settingsStore.settings.toolbar.enable && Object.keys(settingsStore.settings.toolbar).some((key) => {
|
||||
if (settingsStore.settings.app.routeBaseOn === 'filesystem' && key === 'breadcrumb') {
|
||||
return false
|
||||
}
|
||||
return settingsStore.settings.toolbar[key as keyof typeof settingsStore.settings.toolbar]
|
||||
})) {
|
||||
actualHeight += Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-toolbar-height'))
|
||||
}
|
||||
return actualHeight
|
||||
// 标签栏是否隐藏
|
||||
const isTabbarHide = computed(() => {
|
||||
return !settingsStore.settings.tabbar.enable
|
||||
})
|
||||
|
||||
// 工具栏是否隐藏
|
||||
const isToolbarHide = computed(() => {
|
||||
return !settingsStore.settings.toolbar.enable
|
||||
|| !Object.keys(settingsStore.settings.toolbar).some((key) => {
|
||||
if (settingsStore.settings.app.routeBaseOn === 'filesystem' && key === 'breadcrumb') {
|
||||
return false
|
||||
}
|
||||
return settingsStore.settings.toolbar[key as keyof typeof settingsStore.settings.toolbar]
|
||||
})
|
||||
})
|
||||
|
||||
const isLink = computed(() => !!routeInfo.meta.link)
|
||||
@@ -96,10 +78,11 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
<template>
|
||||
<div
|
||||
class="layout" :style="{
|
||||
'--g-header-actual-height': `${headerActualHeight}px`,
|
||||
'--g-main-sidebar-actual-width': `${mainSidebarActualWidth}px`,
|
||||
'--g-sub-sidebar-actual-width': `${subSidebarActualWidth}px`,
|
||||
'--g-topbar-actual-height': `${topbarActualHeight}px`,
|
||||
'--g-header-actual-height': isHeaderHide ? '0px' : 'var(--g-header-height)',
|
||||
'--g-main-sidebar-actual-width': isMainSidebarHide ? '0px' : 'var(--g-main-sidebar-width)',
|
||||
'--g-sub-sidebar-actual-width': isSubSidebarHide ? '0px' : (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile' ? 'var(--g-sub-sidebar-collapse-width)' : 'var(--g-sub-sidebar-width)'),
|
||||
'--g-tabbar-actual-height': isTabbarHide ? '0px' : 'var(--g-tabbar-height)',
|
||||
'--g-toolbar-actual-height': isToolbarHide ? '0px' : 'var(--g-toolbar-height)',
|
||||
}"
|
||||
>
|
||||
<div id="app-main">
|
||||
@@ -214,7 +197,7 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING
|
||||
position: relative;
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
margin: var(--g-topbar-actual-height) 0 0;
|
||||
margin: calc(var(--g-tabbar-actual-height) + var(--g-toolbar-actual-height)) 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,22 +1,20 @@
|
||||
// 加载 iconify 图标
|
||||
import { downloadAndInstall } from '@/iconify'
|
||||
|
||||
import icons from '@/iconify/index.json'
|
||||
// 自定义指令
|
||||
import directive from '@/utils/directive'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import router from './router'
|
||||
import pinia from './store'
|
||||
import uiProvider from './ui/provider'
|
||||
|
||||
import '@/utils/systemCopyright'
|
||||
|
||||
// 加载 svg 图标
|
||||
import 'virtual:svg-icons-register'
|
||||
import 'virtual:uno.css'
|
||||
// UnoCSS
|
||||
import '@unocss/reset/tailwind-compat.css'
|
||||
import 'virtual:uno.css'
|
||||
// 全局样式
|
||||
import '@/assets/styles/globals.css'
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export default defineFakeRoute([
|
||||
{
|
||||
path: '/multilevel_menu_example',
|
||||
component: 'Layout',
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
@@ -36,7 +35,6 @@ export default defineFakeRoute([
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
@@ -52,7 +50,6 @@ export default defineFakeRoute([
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
||||
import type { NavigationFailure, RouteLocationRaw, Router } from 'vue-router'
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useTabbarStore from '@/store/modules/tabbar'
|
||||
|
||||
function getId(router: Router) {
|
||||
return router.currentRoute.value.fullPath
|
||||
@@ -57,8 +55,32 @@ function extendReplace(router: Router) {
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface Router {
|
||||
/**
|
||||
* 本方法为框架扩展语法,等同于 `push` 方法,并且同时会关闭当前标签页
|
||||
*/
|
||||
close: (to: RouteLocationRaw) => Promise<NavigationFailure | void | undefined>
|
||||
}
|
||||
}
|
||||
|
||||
function extendClose(router: Router) {
|
||||
router.close = function (to: RouteLocationRaw) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function setupExtensions(router: Router) {
|
||||
extendPush(router)
|
||||
extendGo(router)
|
||||
extendReplace(router)
|
||||
extendClose(router)
|
||||
}
|
||||
|
||||
+64
-71
@@ -1,15 +1,10 @@
|
||||
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||
import { asyncRoutes, asyncRoutesByFilesystem } from './routes'
|
||||
import '@/assets/styles/nprogress.css'
|
||||
|
||||
function setupRoutes(router: Router) {
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const routeStore = useRouteStore()
|
||||
@@ -22,27 +17,19 @@ function setupRoutes(router: Router) {
|
||||
settingsStore.settings.menu.mode !== 'single' && menuStore.setActived(to.path)
|
||||
// 如果已登录状态下,进入登录页会强制跳转到主页
|
||||
if (to.name === 'login') {
|
||||
next({
|
||||
return {
|
||||
path: settingsStore.settings.home.fullPath,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 如果未开启主页,但进入的是主页,则会进入侧边栏导航第一个模块
|
||||
else if (!settingsStore.settings.home.enable && to.fullPath === settingsStore.settings.home.fullPath) {
|
||||
if (menuStore.sidebarMenus.length > 0) {
|
||||
next({
|
||||
return {
|
||||
path: menuStore.sidebarMenusFirstDeepestPath,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 如果侧边栏导航第一个模块均无法命中,则还是进入主页
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
// 正常访问页面
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -89,24 +76,35 @@ function setupRoutes(router: Router) {
|
||||
userStore.logout()
|
||||
}
|
||||
// 动态路由生成并注册后,重新进入当前路由
|
||||
next({
|
||||
return {
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (to.name !== 'login') {
|
||||
next({
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath !== settingsStore.settings.home.fullPath ? to.fullPath : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 当父级路由未配置重定向时,自动重定向到有访问权限的子路由
|
||||
function setupRedirectAuthChildrenRoute(router: Router) {
|
||||
router.beforeEach((to) => {
|
||||
const { auth } = useAuth()
|
||||
const currentRoute = router.getRoutes().find(route => route.path === (to.matched.at(-1)?.path ?? ''))
|
||||
if (!currentRoute?.redirect) {
|
||||
const findAuthRoute = currentRoute?.children?.find(route => route.meta?.menu !== false && auth(route.meta?.auth ?? ''))
|
||||
if (findAuthRoute) {
|
||||
return findAuthRoute
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -115,12 +113,11 @@ function setupRoutes(router: Router) {
|
||||
// 进度条
|
||||
function setupProgress(router: Router) {
|
||||
const { isLoading } = useNProgress()
|
||||
router.beforeEach((_to, _from, next) => {
|
||||
router.beforeEach(() => {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (settingsStore.settings.app.enableProgress) {
|
||||
isLoading.value = true
|
||||
}
|
||||
next()
|
||||
})
|
||||
router.afterEach(() => {
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -145,54 +142,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] 该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +200,7 @@ function setupOther(router: Router) {
|
||||
|
||||
export default function setupGuards(router: Router) {
|
||||
setupRoutes(router)
|
||||
setupRedirectAuthChildrenRoute(router)
|
||||
setupProgress(router)
|
||||
setupTitle(router)
|
||||
setupKeepAlive(router)
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { loadingFadeOut } from 'virtual:app-loading'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import pinia from '@/store'
|
||||
import setupExtensions from './extensions'
|
||||
import setupGuards from './guards'
|
||||
// 路由相关数据
|
||||
|
||||
@@ -7,7 +7,6 @@ function Layout() {
|
||||
const routes: RouteRecordRaw = {
|
||||
path: '/multilevel_menu_example',
|
||||
component: Layout,
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
@@ -25,7 +24,6 @@ const routes: RouteRecordRaw = {
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
@@ -41,7 +39,6 @@ const routes: RouteRecordRaw = {
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Route } from '#/global'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import generatedRoutes from 'virtual:generated-pages'
|
||||
import { setupLayouts } from 'virtual:meta-layouts'
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
@@ -74,9 +73,9 @@ const constantRoutesByFilesystem = generatedRoutes.filter((item) => {
|
||||
return item.meta?.enabled !== false && item.meta?.constant === true
|
||||
})
|
||||
|
||||
const asyncRoutesByFilesystem = setupLayouts(generatedRoutes.filter((item) => {
|
||||
const asyncRoutesByFilesystem = [...setupLayouts(generatedRoutes.filter((item) => {
|
||||
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false
|
||||
}))
|
||||
}))]
|
||||
|
||||
export {
|
||||
asyncRoutes,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { RecursiveRequired, Settings } from '#/global'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import settingsDefault from '@/settings.default'
|
||||
import { merge } from '@/utils/object'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
|
||||
const globalSettings: Settings.all = {
|
||||
// 请在此处编写或粘贴配置代码
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import { pascalCase } from 'scule'
|
||||
|
||||
type Slots =
|
||||
'header-start' | 'header-after-logo' | 'header-after-menu' | 'header-end' |
|
||||
'main-sidebar-top' | 'main-sidebar-after-logo' | 'main-sidebar-after-menu' | 'main-sidebar-bottom' |
|
||||
'sub-sidebar-top' | 'sub-sidebar-after-logo' | 'sub-sidebar-after-menu' | 'sub-sidebar-bottom' |
|
||||
'tabbar-start' | 'tabbar-end' |
|
||||
'toolbar-start' | 'toolbar-end' |
|
||||
'free-position'
|
||||
type Slots
|
||||
= 'header-start' | 'header-after-logo' | 'header-after-menu' | 'header-end'
|
||||
| 'main-sidebar-top' | 'main-sidebar-after-logo' | 'main-sidebar-after-menu' | 'main-sidebar-bottom'
|
||||
| 'sub-sidebar-top' | 'sub-sidebar-after-logo' | 'sub-sidebar-after-menu' | 'sub-sidebar-bottom'
|
||||
| 'tabbar-start' | 'tabbar-end'
|
||||
| 'toolbar-start' | 'toolbar-end'
|
||||
| 'free-position'
|
||||
|
||||
function tryLoadComponent(name: Slots) {
|
||||
const componentMap = import.meta.glob('./*/index.vue', { eager: true })
|
||||
|
||||
Executable → Regular
@@ -1,4 +1,4 @@
|
||||
const useKeepAliveStore = defineStore(
|
||||
export const useKeepAliveStore = defineStore(
|
||||
// 唯一ID
|
||||
'keepAlive',
|
||||
() => {
|
||||
@@ -38,5 +38,3 @@ const useKeepAliveStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useKeepAliveStore
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import type { Menu, Route } from '#/global'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import apiApp from '@/api/modules/app'
|
||||
import menu from '@/menu'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import useRouteStore from './route'
|
||||
import useSettingsStore from './settings'
|
||||
import useUserStore from './user'
|
||||
|
||||
const useMenuStore = defineStore(
|
||||
export const useMenuStore = defineStore(
|
||||
// 唯一ID
|
||||
'menu',
|
||||
() => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
const filesystemMenusRaw = ref<Menu.recordMainRaw[]>([])
|
||||
@@ -78,10 +74,7 @@ const useMenuStore = defineStore(
|
||||
else {
|
||||
returnMenus = filesystemMenusRaw.value
|
||||
}
|
||||
// 如果权限功能开启,则需要对导航数据进行筛选过滤
|
||||
if (settingsStore.settings.app.enablePermission) {
|
||||
returnMenus = filterAsyncMenus(returnMenus, userStore.permissions)
|
||||
}
|
||||
returnMenus = filterAsyncMenus(returnMenus)
|
||||
return returnMenus
|
||||
})
|
||||
// 次导航数据
|
||||
@@ -100,7 +93,7 @@ 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))
|
||||
@@ -155,35 +148,15 @@ const useMenuStore = defineStore(
|
||||
return defaultOpenedPaths
|
||||
}
|
||||
|
||||
// 判断是否有权限
|
||||
function hasPermission(permissions: string[], menu: Menu.recordMainRaw | Menu.recordRaw) {
|
||||
let isAuth = false
|
||||
if (menu.meta?.auth) {
|
||||
isAuth = permissions.some((auth) => {
|
||||
if (typeof menu.meta?.auth === 'string') {
|
||||
return menu.meta.auth !== '' ? menu.meta.auth === auth : true
|
||||
}
|
||||
else if (typeof menu.meta?.auth === 'object') {
|
||||
return menu.meta.auth.length > 0 ? menu.meta.auth.includes(auth) : true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
isAuth = true
|
||||
}
|
||||
return isAuth
|
||||
}
|
||||
const auth = useAuth()
|
||||
// 根据权限过滤导航
|
||||
function filterAsyncMenus<T extends Menu.recordMainRaw[] | Menu.recordRaw[]>(menus: T, permissions: string[]): T {
|
||||
function filterAsyncMenus<T extends Menu.recordMainRaw[] | Menu.recordRaw[]>(menus: T): T {
|
||||
const res: any = []
|
||||
menus.forEach((menu) => {
|
||||
if (hasPermission(permissions, menu)) {
|
||||
if (auth.auth(menu.meta?.auth ?? '')) {
|
||||
const tmpMenu = cloneDeep(menu)
|
||||
if (tmpMenu.children && tmpMenu.children.length > 0) {
|
||||
tmpMenu.children = filterAsyncMenus(tmpMenu.children, permissions) as Menu.recordRaw[]
|
||||
tmpMenu.children = filterAsyncMenus(tmpMenu.children) as Menu.recordRaw[]
|
||||
tmpMenu.children.length > 0 && res.push(tmpMenu)
|
||||
}
|
||||
else {
|
||||
@@ -242,5 +215,3 @@ const useMenuStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useMenuStore
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Route } from '#/global'
|
||||
import type { RouteRecordRaw, RouterMatcher } from 'vue-router'
|
||||
import apiApp from '@/api/modules/app'
|
||||
import { systemRoutes as systemRoutesRaw } from '@/router/routes'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import { createRouterMatcher } from 'vue-router'
|
||||
import useSettingsStore from './settings'
|
||||
import apiApp from '@/api/modules/app'
|
||||
import { systemRoutes as systemRoutesRaw } from '@/router/routes'
|
||||
|
||||
const useRouteStore = defineStore(
|
||||
export const useRouteStore = defineStore(
|
||||
// 唯一ID
|
||||
'route',
|
||||
() => {
|
||||
@@ -131,7 +130,7 @@ const useRouteStore = defineStore(
|
||||
})
|
||||
routesMatcher.value = createRouterMatcher(routes, {})
|
||||
isGenerate.value = true
|
||||
}).catch(() => {})
|
||||
})
|
||||
}
|
||||
// 生成路由(文件系统生成)
|
||||
function generateRoutesAtFilesystem(asyncRoutes: RouteRecordRaw[]) {
|
||||
@@ -169,5 +168,3 @@ const useRouteStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useRouteStore
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Settings } from '#/global'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
import settingsDefault from '@/settings'
|
||||
import { merge } from '@/utils/object'
|
||||
import { cloneDeep } from 'es-toolkit'
|
||||
|
||||
const useSettingsStore = defineStore(
|
||||
export const useSettingsStore = defineStore(
|
||||
// 唯一ID
|
||||
'settings',
|
||||
() => {
|
||||
@@ -12,6 +12,12 @@ const useSettingsStore = defineStore(
|
||||
|
||||
const prefersColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
watch(() => settings.value.app.colorScheme, (val) => {
|
||||
document.documentElement.classList.add('disable-color-scheme-transition-duration')
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.remove('disable-color-scheme-transition-duration')
|
||||
})
|
||||
})
|
||||
if (val === '') {
|
||||
prefersColorScheme.addEventListener('change', updateTheme)
|
||||
}
|
||||
@@ -172,5 +178,3 @@ const useSettingsStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useSettingsStore
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Tabbar } from '#/global'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import useKeepAliveStore from './keepAlive'
|
||||
|
||||
const useTabbarStore = defineStore(
|
||||
export const useTabbarStore = defineStore(
|
||||
// 唯一ID
|
||||
'tabbar',
|
||||
() => {
|
||||
@@ -12,7 +11,7 @@ const useTabbarStore = defineStore(
|
||||
const leaveIndex = ref(-1)
|
||||
|
||||
// 添加标签页
|
||||
async function add(route: RouteLocationNormalized) {
|
||||
function add(route: RouteLocationNormalized) {
|
||||
const names: string[] = []
|
||||
route.matched.forEach((v, i) => {
|
||||
if (i > 0) {
|
||||
@@ -20,7 +19,7 @@ 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) => {
|
||||
@@ -159,5 +158,3 @@ const useTabbarStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useTabbarStore
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import apiUser from '@/api/modules/user'
|
||||
import router from '@/router'
|
||||
import useMenuStore from './menu'
|
||||
import useRouteStore from './route'
|
||||
import useSettingsStore from './settings'
|
||||
import useTabbarStore from './tabbar'
|
||||
|
||||
const useUserStore = defineStore(
|
||||
export const useUserStore = defineStore(
|
||||
// 唯一ID
|
||||
'user',
|
||||
() => {
|
||||
@@ -109,5 +105,3 @@ const useUserStore = defineStore(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useUserStore
|
||||
|
||||
+87
-80
@@ -6,89 +6,96 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
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 mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('../utils/composables/useAuth')['default']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useGlobalProperties: typeof import('../utils/composables/useGlobalProperties')['default']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useMainPage: typeof import('../utils/composables/useMainPage')['default']
|
||||
const useMenu: typeof import('../utils/composables/useMenu')['default']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTabbar: typeof import('../utils/composables/useTabbar')['default']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useViewTransition: typeof import('../utils/composables/useViewTransition')['default']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const defineStore: typeof import('pinia').defineStore
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
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
|
||||
const mapStores: typeof import('pinia').mapStores
|
||||
const mapWritableState: typeof import('pinia').mapWritableState
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const storeToRefs: typeof import('pinia').storeToRefs
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useAuth: typeof import('../utils/composables/useAuth').default
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useGlobalProperties: typeof import('../utils/composables/useGlobalProperties').default
|
||||
const useId: typeof import('vue').useId
|
||||
const useKeepAliveStore: typeof import('../store/modules/keepAlive').useKeepAliveStore
|
||||
const useLink: typeof import('vue-router').useLink
|
||||
const useMainPage: typeof import('../utils/composables/useMainPage').default
|
||||
const useMenu: typeof import('../utils/composables/useMenu').default
|
||||
const useMenuStore: typeof import('../store/modules/menu').useMenuStore
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouteStore: typeof import('../store/modules/route').useRouteStore
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSettingsStore: typeof import('../store/modules/settings').useSettingsStore
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTabbar: typeof import('../utils/composables/useTabbar').default
|
||||
const useTabbarStore: typeof import('../store/modules/tabbar').useTabbarStore
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const useUserStore: typeof import('../store/modules/user').useUserStore
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
Vendored
+61
-5
@@ -1,8 +1,12 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
import { GlobalComponents } from 'vue'
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -13,15 +17,21 @@ declare module 'vue' {
|
||||
FaAvatar: typeof import('./../ui/components/FaAvatar/index.vue')['default']
|
||||
FaBackToTop: typeof import('./../ui/components/FaBackToTop/index.vue')['default']
|
||||
FaButton: typeof import('./../ui/components/FaButton/index.vue')['default']
|
||||
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']
|
||||
@@ -32,6 +42,7 @@ declare module 'vue' {
|
||||
FaPasswordStrength: typeof import('./../ui/components/FaPasswordStrength/index.vue')['default']
|
||||
FaPinInput: typeof import('./../ui/components/FaPinInput/index.vue')['default']
|
||||
FaPopover: typeof import('./../ui/components/FaPopover/index.vue')['default']
|
||||
FaProgress: typeof import('./../ui/components/FaProgress/index.vue')['default']
|
||||
FaScrollArea: typeof import('./../ui/components/FaScrollArea/index.vue')['default']
|
||||
FaSearchBar: typeof import('./../ui/components/FaSearchBar/index.vue')['default']
|
||||
FaSelect: typeof import('./../ui/components/FaSelect/index.vue')['default']
|
||||
@@ -40,13 +51,58 @@ 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']
|
||||
ImagePreview: typeof import('./../components/ImagePreview/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']
|
||||
}
|
||||
}
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const AccountButton: typeof import('./../components/AccountButton/index.vue')['default']
|
||||
const FaAuth: typeof import('./../ui/components/FaAuth/index.vue')['default']
|
||||
const FaAvatar: typeof import('./../ui/components/FaAvatar/index.vue')['default']
|
||||
const FaBackToTop: typeof import('./../ui/components/FaBackToTop/index.vue')['default']
|
||||
const FaButton: typeof import('./../ui/components/FaButton/index.vue')['default']
|
||||
const FaButtonGroup: typeof import('./../ui/components/FaButtonGroup/index.vue')['default']
|
||||
const FaCard: typeof import('./../ui/components/FaCard/index.vue')['default']
|
||||
const FaCheckbox: typeof import('./../ui/components/FaCheckbox/index.vue')['default']
|
||||
const FaCollapsible: typeof import('./../ui/components/FaCollapsible/index.vue')['default']
|
||||
const FaContextMenu: typeof import('./../ui/components/FaContextMenu/index.vue')['default']
|
||||
const FaCopyright: typeof import('./../ui/components/FaCopyright/index.vue')['default']
|
||||
const FaDivider: typeof import('./../ui/components/FaDivider/index.vue')['default']
|
||||
const FaDrawer: typeof import('./../ui/components/FaDrawer/index.vue')['default']
|
||||
const FaDropdown: typeof import('./../ui/components/FaDropdown/index.vue')['default']
|
||||
const FaFileUpload: typeof import('./../ui/components/FaFileUpload/index.vue')['default']
|
||||
const FaFixedActionBar: typeof import('./../ui/components/FaFixedActionBar/index.vue')['default']
|
||||
const FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
|
||||
const FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
|
||||
const FaImagePreview: typeof import('./../ui/components/FaImagePreview/index.vue')['default']
|
||||
const FaImageUpload: typeof import('./../ui/components/FaImageUpload/index.vue')['default']
|
||||
const FaInput: typeof import('./../ui/components/FaInput/index.vue')['default']
|
||||
const FaKbd: typeof import('./../ui/components/FaKbd/index.vue')['default']
|
||||
const FaModal: typeof import('./../ui/components/FaModal/index.vue')['default']
|
||||
const FaNotAllowed: typeof import('./../ui/components/FaNotAllowed/index.vue')['default']
|
||||
const FaNotification: typeof import('./../ui/components/FaNotification/index.vue')['default']
|
||||
const FaPageHeader: typeof import('./../ui/components/FaPageHeader/index.vue')['default']
|
||||
const FaPageMain: typeof import('./../ui/components/FaPageMain/index.vue')['default']
|
||||
const FaPasswordStrength: typeof import('./../ui/components/FaPasswordStrength/index.vue')['default']
|
||||
const FaPinInput: typeof import('./../ui/components/FaPinInput/index.vue')['default']
|
||||
const FaPopover: typeof import('./../ui/components/FaPopover/index.vue')['default']
|
||||
const FaProgress: typeof import('./../ui/components/FaProgress/index.vue')['default']
|
||||
const FaScrollArea: typeof import('./../ui/components/FaScrollArea/index.vue')['default']
|
||||
const FaSearchBar: typeof import('./../ui/components/FaSearchBar/index.vue')['default']
|
||||
const FaSelect: typeof import('./../ui/components/FaSelect/index.vue')['default']
|
||||
const FaSlider: typeof import('./../ui/components/FaSlider/index.vue')['default']
|
||||
const FaSmartFixedBlock: typeof import('./../ui/components/FaSmartFixedBlock/index.vue')['default']
|
||||
const FaSwitch: typeof import('./../ui/components/FaSwitch/index.vue')['default']
|
||||
const FaSystemInfo: typeof import('./../ui/components/FaSystemInfo/index.vue')['default']
|
||||
const FaTabs: typeof import('./../ui/components/FaTabs/index.vue')['default']
|
||||
const FaTextarea: typeof import('./../ui/components/FaTextarea/index.vue')['default']
|
||||
const FaToast: typeof import('./../ui/components/FaToast/index.vue')['default']
|
||||
const FaTooltip: typeof import('./../ui/components/FaTooltip/index.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
Vendored
+10
-10
@@ -1,13 +1,5 @@
|
||||
interface ImportMetaEnv {
|
||||
// Auto generate by env-parse
|
||||
/**
|
||||
* 应用配置面板
|
||||
*/
|
||||
readonly VITE_APP_SETTING: boolean
|
||||
/**
|
||||
* 页面标题
|
||||
*/
|
||||
readonly VITE_APP_TITLE: string
|
||||
/**
|
||||
* 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
*/
|
||||
@@ -21,11 +13,19 @@ interface ImportMetaEnv {
|
||||
*/
|
||||
readonly VITE_APP_DISABLE_DEVTOOL: boolean
|
||||
/**
|
||||
* 是否开启代理
|
||||
* 应用配置面板
|
||||
*/
|
||||
readonly VITE_OPEN_PROXY: boolean
|
||||
readonly VITE_APP_SETTING: boolean
|
||||
/**
|
||||
* 页面标题
|
||||
*/
|
||||
readonly VITE_APP_TITLE: string
|
||||
/**
|
||||
* 是否开启开发者工具
|
||||
*/
|
||||
readonly VITE_OPEN_DEVTOOLS: boolean
|
||||
/**
|
||||
* 是否开启代理
|
||||
*/
|
||||
readonly VITE_OPEN_PROXY: boolean
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { AvatarVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { AvatarRoot } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
import { avatarVariant } from '.'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
import { buttonVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaButtonGroup',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
vertical?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('inline-flex gap-[1px] items-stretch', {
|
||||
'horizontal-group flex-row': !props.vertical,
|
||||
'vertical-group flex-col': props.vertical,
|
||||
})"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.horizontal-group {
|
||||
:deep(> button) {
|
||||
&:first-child:not(:last-child) {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child, :last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-group {
|
||||
:deep(> button) {
|
||||
&:first-child:not(:last-child) {
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child, :last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { 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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaCopyright',
|
||||
})
|
||||
@@ -14,9 +12,9 @@ const settingsStore = useSettingsStore()
|
||||
<FaIcon name="i-ri:copyright-line" class="size-5" />
|
||||
<span v-if="settingsStore.settings.copyright.dates" class="px-1">{{ settingsStore.settings.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.settings.copyright.company">
|
||||
<a v-if="settingsStore.settings.copyright.website" :href="settingsStore.settings.copyright.website" target="_blank" rel="noopener" class="px-1 text-center no-underline transition hover-text-secondary-foreground">{{ settingsStore.settings.copyright.company }}</a>
|
||||
<a v-if="settingsStore.settings.copyright.website" :href="settingsStore.settings.copyright.website" target="_blank" rel="noopener" class="px-1 text-center no-underline transition-colors hover-text-secondary-foreground">{{ settingsStore.settings.copyright.company }}</a>
|
||||
<span v-else class="px-1">{{ settingsStore.settings.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.settings.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="px-1 text-center no-underline transition hover-text-secondary-foreground">{{ settingsStore.settings.copyright.beian }}</a>
|
||||
<a v-if="settingsStore.settings.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="px-1 text-center no-underline transition-colors hover-text-secondary-foreground">{{ settingsStore.settings.copyright.beian }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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
|
||||
openAutoFocus?: 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 as object], () => {
|
||||
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,33 +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
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
defineProps<DrawerProps>(),
|
||||
{
|
||||
modelValue: false,
|
||||
zIndex: 2000,
|
||||
side: 'right',
|
||||
loading: false,
|
||||
closable: true,
|
||||
@@ -59,47 +37,110 @@ const props = withDefaults(
|
||||
footer: true,
|
||||
closeOnClickOverlay: true,
|
||||
closeOnPressEscape: true,
|
||||
destroyOnClose: true,
|
||||
openAutoFocus: false,
|
||||
},
|
||||
)
|
||||
|
||||
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) => {
|
||||
isOpen.value = newValue
|
||||
})
|
||||
|
||||
function updateOpen(value: boolean) {
|
||||
isOpen.value = value
|
||||
emits('update:modelValue', value)
|
||||
if (value) {
|
||||
const hasOpened = ref(false)
|
||||
const isClosed = ref(true)
|
||||
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
isClosed.value = false
|
||||
if (val && !hasOpened.value) {
|
||||
hasOpened.value = true
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const forceMount = computed(() => !props.destroyOnClose && hasOpened.value)
|
||||
|
||||
async function updateOpen(value: boolean) {
|
||||
if (value) {
|
||||
isOpen.value = value
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
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 handleOpenAutoFocus(e: Event) {
|
||||
if (!props.openAutoFocus) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusOutside(e: Event) {
|
||||
@@ -108,7 +149,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()
|
||||
}
|
||||
@@ -127,6 +168,7 @@ function handleAnimationEnd() {
|
||||
}
|
||||
else {
|
||||
emits('closed')
|
||||
isClosed.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -134,13 +176,18 @@ 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)"
|
||||
:class="cn('w-full flex flex-col gap-0 p-0', props.contentClass, {
|
||||
hidden: isClosed,
|
||||
})"
|
||||
:side="props.side"
|
||||
@open-auto-focus="handleFocusOutside"
|
||||
:force-mount="forceMount"
|
||||
@open-auto-focus="handleOpenAutoFocus"
|
||||
@close-auto-focus="handleFocusOutside"
|
||||
@focus-outside="handleFocusOutside"
|
||||
@pointer-down-outside="handleClickOutside"
|
||||
@@ -153,6 +200,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 }}
|
||||
@@ -162,18 +213,18 @@ function handleAnimationEnd() {
|
||||
</SheetDescription>
|
||||
</slot>
|
||||
</SheetHeader>
|
||||
<div class="m-0 flex-1 of-y-hidden">
|
||||
<FaScrollArea class="h-full">
|
||||
<div class="p-4">
|
||||
<slot />
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
<VisuallyHidden v-else>
|
||||
<SheetTitle />
|
||||
<SheetDescription />
|
||||
</VisuallyHidden>
|
||||
<div v-if="!!slots.default" class="relative m-0 flex-1 overflow-y-auto 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>
|
||||
<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,8 +2,7 @@
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { SheetVariants } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { reactiveOmit, useScrollLock } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
@@ -11,16 +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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
@@ -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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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 { cn } from '@/utils'
|
||||
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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user