mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-01 00:05:17 +00:00
@@ -1,12 +1,11 @@
|
|||||||
dist
|
node_modules/
|
||||||
node_modules
|
dist/
|
||||||
public
|
public/
|
||||||
.husky
|
.vscode/
|
||||||
.vscode
|
src/types/import/
|
||||||
.idea
|
.auto-import.json
|
||||||
*.sh
|
|
||||||
*.md
|
|
||||||
|
|
||||||
src/assets
|
|
||||||
stats.html
|
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
.stylelintcache
|
||||||
|
.eslintcache
|
||||||
|
stats.html
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
# 在单参数箭头函数中始终添加括号
|
# 单参数箭头函数始终添加括号
|
||||||
arrowParens: "always"
|
arrowParens: "always"
|
||||||
# JSX 多行元素的闭合标签另起一行
|
# JSX 多行元素的闭合标签另起一行
|
||||||
bracketSameLine: false
|
bracketSameLine: false
|
||||||
# 对象字面量中的括号之间添加空格
|
# 对象字面量括号之间添加空格 { foo: bar }
|
||||||
bracketSpacing: true
|
bracketSpacing: true
|
||||||
# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码)
|
# 自动格式化嵌入的代码(Markdown/HTML 内的代码块)
|
||||||
embeddedLanguageFormatting: "auto"
|
embeddedLanguageFormatting: "auto"
|
||||||
# 忽略 HTML 空白敏感度,将空白视为非重要内容
|
# HTML 空白敏感度,CSS 模式保留 display 相关空白
|
||||||
htmlWhitespaceSensitivity: "ignore"
|
htmlWhitespaceSensitivity: "css"
|
||||||
# 不插入 @prettier 的 pragma 注释
|
# 不插入 @prettier 的 pragma 注释
|
||||||
insertPragma: false
|
insertPragma: false
|
||||||
# 在 JSX 中使用双引号
|
# JSX 中使用双引号
|
||||||
jsxSingleQuote: false
|
jsxSingleQuote: false
|
||||||
# 每行代码的最大长度限制为 100 字符
|
# 每行最大长度
|
||||||
printWidth: 100
|
printWidth: 100
|
||||||
# 在 Markdown 中保留原有的换行格式
|
# Markdown 换行保留原格式
|
||||||
proseWrap: "preserve"
|
proseWrap: "preserve"
|
||||||
# 仅在必要时添加对象属性的引号
|
# 对象属性仅在必要时添加引号
|
||||||
quoteProps: "as-needed"
|
quoteProps: "as-needed"
|
||||||
# 不要求文件开头插入 @prettier 的 pragma 注释
|
# 不要求文件开头有 @prettier 的 pragma 注释
|
||||||
requirePragma: false
|
requirePragma: false
|
||||||
# 在语句末尾添加分号
|
# 语句末尾添加分号
|
||||||
semi: true
|
semi: true
|
||||||
# 使用双引号而不是单引号
|
# 使用双引号
|
||||||
singleQuote: false
|
singleQuote: false
|
||||||
# 缩进使用 2 个空格
|
# 缩进 2 个空格
|
||||||
tabWidth: 2
|
tabWidth: 2
|
||||||
# 在多行元素的末尾添加逗号(ES5 支持的对象、数组等)
|
# 多行尾随逗号(ES5 支持的对象、数组等)
|
||||||
trailingComma: "es5"
|
trailingComma: "es5"
|
||||||
# 使用空格而不是制表符缩进
|
# 使用空格而非制表符缩进
|
||||||
useTabs: false
|
useTabs: false
|
||||||
# Vue 文件中的 <script> 和 <style> 不增加额外的缩进
|
# Vue 文件 <script> 和 <style> 不额外缩进
|
||||||
vueIndentScriptAndStyle: false
|
vueIndentScriptAndStyle: false
|
||||||
# 根据系统自动检测换行符
|
# 换行符自动检测(LF / CRLF)
|
||||||
endOfLine: "auto"
|
endOfLine: "auto"
|
||||||
# 对 HTML 文件应用特定格式化规则
|
# HTML 文件使用专用解析器
|
||||||
overrides:
|
overrides:
|
||||||
- files: "*.html"
|
- files: "*.html"
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// 继承推荐规范配置
|
|
||||||
extends: [
|
extends: [
|
||||||
"stylelint-config-standard",
|
"stylelint-config-standard",
|
||||||
"stylelint-config-recommended",
|
|
||||||
"stylelint-config-recommended-scss",
|
"stylelint-config-recommended-scss",
|
||||||
"stylelint-config-recommended-vue/scss",
|
|
||||||
"stylelint-config-html/vue",
|
|
||||||
"stylelint-config-recess-order",
|
"stylelint-config-recess-order",
|
||||||
],
|
],
|
||||||
plugins: [
|
|
||||||
"stylelint-prettier", // 统一代码风格,格式冲突时以 Prettier 规则为准
|
|
||||||
],
|
|
||||||
// 指定不同文件对应的解析器
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["**/*.{vue,html}"],
|
files: ["**/*.{vue,html}"],
|
||||||
@@ -21,57 +13,55 @@ module.exports = {
|
|||||||
files: ["**/*.{css,scss}"],
|
files: ["**/*.{css,scss}"],
|
||||||
customSyntax: "postcss-scss",
|
customSyntax: "postcss-scss",
|
||||||
},
|
},
|
||||||
// Markdown 预览样式由多段合并,存在有意重复的限定选择器
|
|
||||||
{
|
|
||||||
files: ["**/src/styles/core/md.scss"],
|
|
||||||
rules: {
|
|
||||||
"no-duplicate-selectors": null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
// 自定义规则
|
|
||||||
rules: {
|
rules: {
|
||||||
"prettier/prettier": true, // 强制执行 Prettier 格式化规则(需配合 .prettierrc 配置文件)
|
"import-notation": "string",
|
||||||
"declaration-property-value-no-unknown": null, // 允许非常规数值格式 ,如 height: calc(100% - 50)
|
"selector-class-pattern": null,
|
||||||
// Tailwind CSS v4 使用 @reference 引入主题而不注入样式,需放行
|
"custom-property-pattern": null,
|
||||||
"scss/at-rule-no-unknown": [
|
"keyframes-name-pattern": null,
|
||||||
true,
|
"no-descending-specificity": null,
|
||||||
{
|
"no-empty-source": null,
|
||||||
// Tailwind CSS v4:@custom-variant、@theme、@utility、@layer 等
|
"property-no-vendor-prefix": null,
|
||||||
ignoreAtRules: ["reference", "custom-variant", "theme", "utility", "layer"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"import-notation": "string", // 指定导入CSS文件的方式("string"|"url")
|
|
||||||
"selector-class-pattern": null, // 选择器类名命名规则
|
|
||||||
"custom-property-pattern": null, // 自定义属性命名规则
|
|
||||||
"keyframes-name-pattern": null, // 动画帧节点样式命名规则
|
|
||||||
"no-descending-specificity": null, // 允许无降序特异性
|
|
||||||
"no-empty-source": null, // 允许空样式
|
|
||||||
"property-no-vendor-prefix": null, // 允许属性前缀
|
|
||||||
// SCSS @if/@else 链与 Prettier 易冲突,关闭「at-rule 前空行」检查
|
|
||||||
"at-rule-empty-line-before": null,
|
"at-rule-empty-line-before": null,
|
||||||
// 允许 global 、export 、deep伪类
|
// 允许 Vue SFC :deep() / ::deep / :global() / :slotted() 伪选择器
|
||||||
"selector-pseudo-class-no-unknown": [
|
"selector-pseudo-class-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ignorePseudoClasses: ["global", "export", "deep"],
|
ignorePseudoClasses: ["global", "export", "deep", "slotted"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 允许 Vue 3 的 ::deep 伪元素选择器
|
|
||||||
"selector-pseudo-element-no-unknown": [
|
"selector-pseudo-element-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ignorePseudoElements: ["deep"],
|
ignorePseudoElements: ["deep"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 允许未知属性
|
// 允许 SCSS + Tailwind CSS v4 专属 at-rules
|
||||||
"property-no-unknown": [
|
"at-rule-no-unknown": null,
|
||||||
|
"scss/at-rule-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ignoreProperties: [],
|
ignoreAtRules: [
|
||||||
|
"apply",
|
||||||
|
"use",
|
||||||
|
"mixin",
|
||||||
|
"include",
|
||||||
|
"extend",
|
||||||
|
"each",
|
||||||
|
"if",
|
||||||
|
"else",
|
||||||
|
"for",
|
||||||
|
"while",
|
||||||
|
"reference",
|
||||||
|
"tailwind",
|
||||||
|
"theme",
|
||||||
|
"utility",
|
||||||
|
"custom-variant",
|
||||||
|
"layer",
|
||||||
|
"config",
|
||||||
|
"plugin",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 允许未知规则
|
|
||||||
"at-rule-no-unknown": null, // 禁用默认的未知 at-rule 检查,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+57
-57
@@ -26,10 +26,10 @@ main.ts 启动
|
|||||||
|
|
||||||
### 1. 环境准备
|
### 1. 环境准备
|
||||||
|
|
||||||
| 工具 | 版本要求 |
|
| 工具 | 版本要求 |
|
||||||
|------|-----------|
|
| ------- | ------------------------------------------------------ |
|
||||||
| Node.js | ≥ 20.19(见 `package.json` → `engines`) |
|
| Node.js | ≥ 20.19(见 `package.json` → `engines`) |
|
||||||
| pnpm | ≥ 8.8,推荐 **pnpm 9**(与 `packageManager` 字段一致) |
|
| pnpm | ≥ 8.8,推荐 **pnpm 9**(与 `packageManager` 字段一致) |
|
||||||
|
|
||||||
未安装 pnpm 时可执行:`corepack enable && corepack prepare pnpm@9.15.3 --activate`(版本可按项目 `packageManager` 调整)。
|
未安装 pnpm 时可执行:`corepack enable && corepack prepare pnpm@9.15.3 --activate`(版本可按项目 `packageManager` 调整)。
|
||||||
|
|
||||||
@@ -76,45 +76,45 @@ cp .env.example .env
|
|||||||
|
|
||||||
## 常见问题(排查)
|
## 常见问题(排查)
|
||||||
|
|
||||||
| 现象 | 建议 |
|
| 现象 | 建议 |
|
||||||
|------|------|
|
| ------------------------- | --------------------------------------------------------------------------------------- |
|
||||||
| `ECONNREFUSED` / 网络错误 | 后端未启动或 **`VITE_API_BASE_URL`** 端口错误 |
|
| `ECONNREFUSED` / 网络错误 | 后端未启动或 **`VITE_API_BASE_URL`** 端口错误 |
|
||||||
| 接口 401 / 频繁跳转登录 | Token 失效或未登录;清除站点本地存储后重新登录 |
|
| 接口 401 / 频繁跳转登录 | Token 失效或未登录;清除站点本地存储后重新登录 |
|
||||||
| 修改 `.env` 不生效 | 必须 **重启** `pnpm dev` |
|
| 修改 `.env` 不生效 | 必须 **重启** `pnpm dev` |
|
||||||
| 依赖异常、热更新怪异 | 尝试 **`pnpm clean:cache`** 后再 **`pnpm dev`**;仍不行可 **`pnpm dev:force`** |
|
| 依赖异常、热更新怪异 | 尝试 **`pnpm clean:cache`** 后再 **`pnpm dev`**;仍不行可 **`pnpm dev:force`** |
|
||||||
| 类型报错 | 运行 **`pnpm type-check`**;自动生成类型见 `src/types/import/`(勿手改自动生成的 d.ts) |
|
| 类型报错 | 运行 **`pnpm type-check`**;自动生成类型见 `src/types/import/`(勿手改自动生成的 d.ts) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 类别 | 选型 |
|
| 类别 | 选型 |
|
||||||
|------|------|
|
| ------ | ------------------------------------------------------ |
|
||||||
| 框架 | Vue 3(Composition API / `<script setup>`) |
|
| 框架 | Vue 3(Composition API / `<script setup>`) |
|
||||||
| 构建 | Vite 7 |
|
| 构建 | Vite 7 |
|
||||||
| 语言 | TypeScript |
|
| 语言 | TypeScript |
|
||||||
| UI | Element Plus |
|
| UI | Element Plus |
|
||||||
| 路由 | Vue Router 4(Hash;静态路由 + 守卫内动态 `addRoute`) |
|
| 路由 | Vue Router 4(Hash;静态路由 + 守卫内动态 `addRoute`) |
|
||||||
| 状态 | Pinia + pinia-plugin-persistedstate |
|
| 状态 | Pinia + pinia-plugin-persistedstate |
|
||||||
| 样式 | Tailwind CSS、SCSS |
|
| 样式 | Tailwind CSS、SCSS |
|
||||||
| HTTP | Axios |
|
| HTTP | Axios |
|
||||||
| 国际化 | vue-i18n |
|
| 国际化 | vue-i18n |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常用脚本
|
## 常用脚本
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
| --------------------------------------------- | ------------------------------------------------------------- |
|
||||||
| `pnpm dev` | 本地开发(读取 `.env` + `.env.development`) |
|
| `pnpm dev` | 本地开发(读取 `.env` + `.env.development`) |
|
||||||
| `pnpm dev:force` | 强制预打包依赖后启动(缓存异常时) |
|
| `pnpm dev:force` | 强制预打包依赖后启动(缓存异常时) |
|
||||||
| `pnpm build` | `vue-tsc` 类型检查 + 生产构建,产物在 **`dist/`** |
|
| `pnpm build` | `vue-tsc` 类型检查 + 生产构建,产物在 **`dist/`** |
|
||||||
| `pnpm build:dev` / `build:test` / `build:pro` | 按 mode 构建(需对应 env 文件) |
|
| `pnpm build:dev` / `build:test` / `build:pro` | 按 mode 构建(需对应 env 文件) |
|
||||||
| `pnpm preview` | 本地预览构建结果 |
|
| `pnpm preview` | 本地预览构建结果 |
|
||||||
| `pnpm type-check` | 仅 TypeScript 检查 |
|
| `pnpm type-check` | 仅 TypeScript 检查 |
|
||||||
| `pnpm lint` | ESLint + Prettier + Stylelint |
|
| `pnpm lint` | ESLint + Prettier + Stylelint |
|
||||||
| `pnpm clean:dev` | 执行 `scripts/clean-dev.ts`(清理演示等,使用前阅读脚本说明) |
|
| `pnpm clean:dev` | 执行 `scripts/clean-dev.ts`(清理演示等,使用前阅读脚本说明) |
|
||||||
| `pnpm clean:cache` | 清理 Vite 等缓存 |
|
| `pnpm clean:cache` | 清理 Vite 等缓存 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,14 +146,14 @@ src/
|
|||||||
|
|
||||||
## 路径别名(编写代码时)
|
## 路径别名(编写代码时)
|
||||||
|
|
||||||
| 别名 | 指向 |
|
| 别名 | 指向 |
|
||||||
|------|------|
|
| ------------------ | ------------ |
|
||||||
| `@` | `src/` |
|
| `@` | `src/` |
|
||||||
| `@views` | `src/views` |
|
| `@views` | `src/views` |
|
||||||
| `@stores` | `src/store` |
|
| `@stores` | `src/store` |
|
||||||
| `@utils` | `src/utils` |
|
| `@utils` | `src/utils` |
|
||||||
| `@styles` | `src/styles` |
|
| `@styles` | `src/styles` |
|
||||||
| `@imgs` / `@icons` | 图片与 SVG |
|
| `@imgs` / `@icons` | 图片与 SVG |
|
||||||
|
|
||||||
与 **`vite.config.ts`**、`tsconfig.json` 中 `paths` 保持一致。
|
与 **`vite.config.ts`**、`tsconfig.json` 中 `paths` 保持一致。
|
||||||
|
|
||||||
@@ -163,16 +163,16 @@ src/
|
|||||||
|
|
||||||
只有以 **`VITE_`** 开头的变量会注入前端代码。下列与 **`pnpm dev`** 关系最密切:
|
只有以 **`VITE_`** 开头的变量会注入前端代码。下列与 **`pnpm dev`** 关系最密切:
|
||||||
|
|
||||||
| 变量 | 作用 |
|
| 变量 | 作用 |
|
||||||
|------|------|
|
| ---------------------- | ------------------------------------------------ |
|
||||||
| `VITE_PORT` | 开发服务器端口 |
|
| `VITE_PORT` | 开发服务器端口 |
|
||||||
| `VITE_BASE_URL` | 部署基础路径(子目录部署时形如 `/admin/`) |
|
| `VITE_BASE_URL` | 部署基础路径(子目录部署时形如 `/admin/`) |
|
||||||
| `VITE_APP_BASE_API` | 接口路径前缀,与 Vite 代理匹配 |
|
| `VITE_APP_BASE_API` | 接口路径前缀,与 Vite 代理匹配 |
|
||||||
| `VITE_API_URL` | 浏览器侧发出的 API 根前缀(开发时常为 `/`) |
|
| `VITE_API_URL` | 浏览器侧发出的 API 根前缀(开发时常为 `/`) |
|
||||||
| `VITE_API_BASE_URL` | **代理目标**:后端 HTTP 根地址 |
|
| `VITE_API_BASE_URL` | **代理目标**:后端 HTTP 根地址 |
|
||||||
| `VITE_ACCESS_MODE` | `frontend` / `backend` / `mixed`,菜单与路由来源 |
|
| `VITE_ACCESS_MODE` | `frontend` / `backend` / `mixed`,菜单与路由来源 |
|
||||||
| `VITE_APP_WS_ENDPOINT` | WebSocket(如 AI 对话) |
|
| `VITE_APP_WS_ENDPOINT` | WebSocket(如 AI 对话) |
|
||||||
| `VITE_APP_TITLE` | 页面标题(可被后端参数配置覆盖) |
|
| `VITE_APP_TITLE` | 页面标题(可被后端参数配置覆盖) |
|
||||||
|
|
||||||
完整列表以仓库内 **`.env`**、**`.env.development`** 为准;模板说明见 **`.env.example`**。
|
完整列表以仓库内 **`.env`**、**`.env.development`** 为准;模板说明见 **`.env.example`**。
|
||||||
|
|
||||||
@@ -180,12 +180,12 @@ src/
|
|||||||
|
|
||||||
## 路由与菜单(扩展页面时)
|
## 路由与菜单(扩展页面时)
|
||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
| ----------------------------- | ------------------------------------------------- |
|
||||||
| `src/router/staticRoutes.ts` | 静态路由、`dashboardLayoutChildren`、壳层菜单合并 |
|
| `src/router/staticRoutes.ts` | 静态路由、`dashboardLayoutChildren`、壳层菜单合并 |
|
||||||
| `src/router/dynamicRoutes.ts` | 菜单驱动的动态路由 |
|
| `src/router/dynamicRoutes.ts` | 菜单驱动的动态路由 |
|
||||||
| `src/router/beforeEach.ts` | 权限与动态挂载 |
|
| `src/router/beforeEach.ts` | 权限与动态挂载 |
|
||||||
| `src/router/MenuProcessor.ts` | 后端菜单 → 前端路由记录 |
|
| `src/router/MenuProcessor.ts` | 后端菜单 → 前端路由记录 |
|
||||||
|
|
||||||
新增业务页:一般需要 **视图 +(可选)静态或动态路由 + 后端菜单/i18n**,三者路径与 **name** 保持一致。
|
新增业务页:一般需要 **视图 +(可选)静态或动态路由 + 后端菜单/i18n**,三者路径与 **name** 保持一致。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const BANNER = [
|
||||||
|
" ___ _ _ __ _ ",
|
||||||
|
" .' ..] / |_ (_) | ] (_) ",
|
||||||
|
" _| |_ ,--. .--. `| |-',--. _ .--. __ ,--. .--.| | _ .--..--. __ _ .--. ",
|
||||||
|
" '-| |-'`'_\\ : ( (`\\] | | `'_\\ :[ '/ '`\\ [ | `'_\\ : / /'`\\' | [ `.-. .-. | [ | [ `.-. | ",
|
||||||
|
" | | // | |, `'.'. | |,// | |,| \\__/ | | | // | |,| \\__/ | | | | | | | | | | | | | ",
|
||||||
|
" [___] \\'-;__/[\\__) )\\__/\\'-;__/| ;.__/ [___]\\'-;__/ '.__.;__][___||__||__][___][___||__] ",
|
||||||
|
" [__] ",
|
||||||
|
].join("\n");
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { PluginOption, ResolvedConfig } from "vite";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { BANNER } from "./banner";
|
||||||
|
|
||||||
|
const { green, blue, bold, dim, cyan, yellow, magenta } = pc;
|
||||||
|
|
||||||
|
const W = 96;
|
||||||
|
const ANSI_RE = new RegExp(String.fromCharCode(27) + "\\[\\d+(;\\d+)*m", "g");
|
||||||
|
const strip = (s: string) => s.replace(ANSI_RE, "");
|
||||||
|
const BANNER_LINES = BANNER.split("\n");
|
||||||
|
const row = (content: string) => {
|
||||||
|
const pad = Math.max(0, W - strip(content).length - 1);
|
||||||
|
return ` ${dim("│")} ${content}${" ".repeat(pad)}${dim("│")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function printBanner(env: Record<string, string>, mode: string) {
|
||||||
|
const line = dim("─".repeat(W));
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
` ${dim("┌")}${line}${dim("┐")}`,
|
||||||
|
row(""),
|
||||||
|
row(`${bold(cyan("fastapiadmin"))} ${bold(magenta(`v${env.VITE_VERSION || "3.0.0"}`))}`),
|
||||||
|
row(""),
|
||||||
|
...BANNER_LINES.map((l) => row(green(l))),
|
||||||
|
row(""),
|
||||||
|
row(`${dim("mode")} ${yellow(env.VITE_APP_ENV || mode)}`),
|
||||||
|
row(`${dim("api")} ${blue(env.VITE_API_BASE_URL || "")}`),
|
||||||
|
row(`${dim("port")} ${cyan(env.VITE_PORT || "")}`),
|
||||||
|
row(""),
|
||||||
|
` ${dim("└")}${line}${dim("┘")}`,
|
||||||
|
"",
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function vitePluginStart(): PluginOption {
|
||||||
|
return {
|
||||||
|
name: "vite:start-banner",
|
||||||
|
enforce: "pre",
|
||||||
|
|
||||||
|
configResolved(resolvedConfig: ResolvedConfig) {
|
||||||
|
const env = resolvedConfig.env as Record<string, string>;
|
||||||
|
printBanner(env, resolvedConfig.mode);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+36
-157
@@ -1,208 +1,87 @@
|
|||||||
// ESLint 配置文件
|
// ESLint 配置文件
|
||||||
import fs from "fs"
|
import fs from "fs";
|
||||||
import path, { dirname } from "path"
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
import pluginJs from "@eslint/js"
|
// 从 ESLint 插件中导入推荐配置
|
||||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
|
import pluginJs from "@eslint/js";
|
||||||
import pluginVue from "eslint-plugin-vue"
|
import pluginVue from "eslint-plugin-vue";
|
||||||
import globals from "globals"
|
import globals from "globals";
|
||||||
import tseslint from "typescript-eslint"
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
// 使用 import.meta.url 获取当前模块的路径
|
||||||
const __dirname = dirname(__filename)
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
|
||||||
const autoImportConfig = JSON.parse(
|
const autoImportConfig = JSON.parse(
|
||||||
fs.readFileSync(path.resolve(__dirname, ".auto-import.json"), "utf-8")
|
fs.readFileSync(path.resolve(__dirname, ".auto-import.json"), "utf-8")
|
||||||
)
|
);
|
||||||
|
|
||||||
// Element Plus 组件全局配置
|
|
||||||
const elementPlusComponents = {
|
|
||||||
ElInput: "readonly",
|
|
||||||
ElSelect: "readonly",
|
|
||||||
ElSwitch: "readonly",
|
|
||||||
ElCascader: "readonly",
|
|
||||||
ElInputNumber: "readonly",
|
|
||||||
ElTimePicker: "readonly",
|
|
||||||
ElTimeSelect: "readonly",
|
|
||||||
ElDatePicker: "readonly",
|
|
||||||
ElTreeSelect: "readonly",
|
|
||||||
ElText: "readonly",
|
|
||||||
ElRadioGroup: "readonly",
|
|
||||||
ElCheckboxGroup: "readonly",
|
|
||||||
ElOption: "readonly",
|
|
||||||
ElRadio: "readonly",
|
|
||||||
ElCheckbox: "readonly",
|
|
||||||
ElInputTag: "readonly",
|
|
||||||
ElForm: "readonly",
|
|
||||||
ElFormItem: "readonly",
|
|
||||||
ElTable: "readonly",
|
|
||||||
ElTableColumn: "readonly",
|
|
||||||
ElButton: "readonly",
|
|
||||||
ElDialog: "readonly",
|
|
||||||
ElPagination: "readonly",
|
|
||||||
ElMessage: "readonly",
|
|
||||||
ElMessageBox: "readonly",
|
|
||||||
ElNotification: "readonly",
|
|
||||||
ElTree: "readonly",
|
|
||||||
ElDropdown: "readonly",
|
|
||||||
ElDropdownMenu: "readonly",
|
|
||||||
ElDropdownItem: "readonly",
|
|
||||||
ElAvatar: "readonly",
|
|
||||||
ElBadge: "readonly",
|
|
||||||
ElCard: "readonly",
|
|
||||||
ElCol: "readonly",
|
|
||||||
ElRow: "readonly",
|
|
||||||
ElContainer: "readonly",
|
|
||||||
ElHeader: "readonly",
|
|
||||||
ElAside: "readonly",
|
|
||||||
ElMain: "readonly",
|
|
||||||
ElFooter: "readonly",
|
|
||||||
ElLink: "readonly",
|
|
||||||
ElDivider: "readonly",
|
|
||||||
ElImage: "readonly",
|
|
||||||
ElProgress: "readonly",
|
|
||||||
ElSkeleton: "readonly",
|
|
||||||
ElSlider: "readonly",
|
|
||||||
ElSwitch: "readonly",
|
|
||||||
ElTag: "readonly",
|
|
||||||
ElTooltip: "readonly",
|
|
||||||
ElPopover: "readonly",
|
|
||||||
ElPopconfirm: "readonly",
|
|
||||||
ElDrawer: "readonly",
|
|
||||||
ElAlert: "readonly",
|
|
||||||
ElLoading: "readonly",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// 忽略文件配置
|
// 忽略文件(flat config 中 ignores 需在最前面)
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
"node_modules",
|
"**/node_modules/**",
|
||||||
"dist",
|
"**/dist/**",
|
||||||
"public",
|
"public/**",
|
||||||
".vscode/**",
|
".vscode/**",
|
||||||
"src/assets/**",
|
"src/assets/**",
|
||||||
"src/utils/console.ts",
|
"src/utils/console.ts",
|
||||||
"**/*.min.*",
|
".auto-import.json",
|
||||||
"**/auto-imports.d.ts",
|
|
||||||
"**/components.d.ts",
|
|
||||||
"**/types/**/*.d.ts",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 全局语言环境
|
||||||
// 基础配置
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs,cjs,ts,tsx,vue}"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.es2022,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 基础推荐配置
|
||||||
pluginJs.configs.recommended,
|
pluginJs.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
...pluginVue.configs["flat/essential"],
|
...pluginVue.configs["flat/essential"],
|
||||||
|
// 项目自定义规则
|
||||||
// 全局配置
|
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,mjs,cjs,ts,tsx,vue}"],
|
files: ["**/*.{js,mjs,cjs,ts,tsx,vue}"],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...autoImportConfig.globals,
|
...autoImportConfig.globals,
|
||||||
|
// TypeScript 全局命名空间(global.d.ts 中声明的类型)
|
||||||
Api: "readonly",
|
Api: "readonly",
|
||||||
...elementPlusComponents,
|
|
||||||
// 全局类型定义
|
|
||||||
PageQuery: "readonly",
|
|
||||||
PageResult: "readonly",
|
|
||||||
UserListItem: "readonly",
|
|
||||||
UserSearchParams: "readonly",
|
|
||||||
RoleListItem: "readonly",
|
|
||||||
RoleSearchParams: "readonly",
|
|
||||||
OptionType: "readonly",
|
OptionType: "readonly",
|
||||||
ApiResponse: "readonly",
|
ApiResponse: "readonly",
|
||||||
ExcelResult: "readonly",
|
|
||||||
CommonType: "readonly",
|
|
||||||
updatorType: "readonly",
|
|
||||||
creatorType: "readonly",
|
|
||||||
AppSettings: "readonly",
|
|
||||||
__APP_INFO__: "readonly",
|
|
||||||
UploadFilePath: "readonly",
|
UploadFilePath: "readonly",
|
||||||
|
// unplugin-vue-components 自动注册的组件引用
|
||||||
|
FaSearchBar: "readonly",
|
||||||
|
FaSearchBarWithAudit: "readonly",
|
||||||
|
FaForm: "readonly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// 代码风格
|
|
||||||
quotes: ["error", "single"],
|
|
||||||
semi: ["error", "never"],
|
|
||||||
"no-var": "error",
|
"no-var": "error",
|
||||||
"prefer-const": "error",
|
|
||||||
"object-shorthand": "error",
|
|
||||||
|
|
||||||
// 最佳实践
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"eqeqeq": "off",
|
|
||||||
"no-multi-spaces": "error",
|
|
||||||
"no-multiple-empty-lines": ["warn", { max: 1 }],
|
"no-multiple-empty-lines": ["warn", { max: 1 }],
|
||||||
"no-unexpected-multiline": "error",
|
"no-unexpected-multiline": "error",
|
||||||
|
|
||||||
// TypeScript 规则
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
|
||||||
|
|
||||||
// Vue 规则
|
|
||||||
"vue/multi-word-component-names": "off",
|
"vue/multi-word-component-names": "off",
|
||||||
"vue/no-v-html": "off",
|
// Prettier 负责格式化,ESLint 只关注代码质量
|
||||||
"vue/require-default-prop": "off",
|
|
||||||
"vue/require-explicit-emits": "error",
|
|
||||||
"vue/no-unused-vars": "error",
|
|
||||||
"vue/no-mutating-props": "off",
|
|
||||||
"vue/valid-v-for": "warn",
|
|
||||||
"vue/no-template-shadow": "warn",
|
|
||||||
"vue/return-in-computed-property": "warn",
|
|
||||||
"vue/block-order": ["error", { order: ["template", "script", "style"] }],
|
|
||||||
"vue/html-self-closing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
html: { void: "always", normal: "never", component: "always" },
|
|
||||||
svg: "always",
|
|
||||||
math: "always",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"vue/component-name-in-template-casing": ["error", "PascalCase"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Vue 文件:使用 TypeScript parser
|
||||||
// Pin TypeScript project root so @typescript-eslint/parser resolves tsconfig consistently.
|
|
||||||
{
|
|
||||||
files: ["**/*.{ts,tsx,mts,cts}"],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Vue 文件特定配置
|
|
||||||
{
|
{
|
||||||
files: ["**/*.vue"],
|
files: ["**/*.vue"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: { parser: tseslint.parser },
|
||||||
parser: tseslint.parser,
|
},
|
||||||
tsconfigRootDir: __dirname,
|
},
|
||||||
},
|
// .d.ts 声明文件:放宽规则
|
||||||
|
{
|
||||||
|
files: ["**/*.d.ts"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// prettier 配置
|
|
||||||
eslintPluginPrettierRecommended,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,13 +26,10 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"type-check": "vue-tsc --noEmit",
|
"type-check": "vue-tsc --noEmit",
|
||||||
"lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:stylelint",
|
"lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:stylelint",
|
||||||
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"",
|
|
||||||
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
|
||||||
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.cjs",
|
|
||||||
"lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix",
|
"lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix",
|
||||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
"lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix",
|
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --cache --fix",
|
||||||
"fix": "eslint --fix",
|
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.cjs",
|
||||||
"commit": "git-cz",
|
"commit": "git-cz",
|
||||||
"clean:dev": "tsx scripts/clean-dev.ts"
|
"clean:dev": "tsx scripts/clean-dev.ts"
|
||||||
},
|
},
|
||||||
@@ -79,7 +76,7 @@
|
|||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.1",
|
"@vue-flow/core": "^1.48.1",
|
||||||
"@vue-flow/minimap": "^1.5.4",
|
"@vue-flow/minimap": "^1.5.4",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@wangeditor-next/editor": "^5.7.0",
|
"@wangeditor-next/editor": "^5.7.0",
|
||||||
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@@ -92,7 +89,7 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"dompurify": "^3.4.3",
|
"dompurify": "^3.4.3",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.14.0",
|
"element-plus": "~2.13.7",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
@@ -104,6 +101,7 @@
|
|||||||
"ohash": "^2.0.11",
|
"ohash": "^2.0.11",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"path-to-regexp": "^8.4.2",
|
"path-to-regexp": "^8.4.2",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"qrcode.vue": "^3.9.0",
|
"qrcode.vue": "^3.9.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/ai/chat";
|
const API_PATH = "/ai/chat";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/application/portal";
|
const API_PATH = "/application/portal";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/example/demo";
|
const API_PATH = "/example/demo";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/example/demo01";
|
const API_PATH = "/example/demo01";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/generator/gencode";
|
const API_PATH = "/generator/gencode";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/monitor/cache";
|
const API_PATH = "/monitor/cache";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/monitor/online";
|
const API_PATH = "/monitor/online";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/monitor/resource";
|
const API_PATH = "/monitor/resource";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/monitor/server";
|
const API_PATH = "/monitor/server";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/auth";
|
const API_PATH = "/system/auth";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/dept";
|
const API_PATH = "/system/dept";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/dict";
|
const API_PATH = "/system/dict";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/log";
|
const API_PATH = "/system/log";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/menu";
|
const API_PATH = "/system/menu";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/notice";
|
const API_PATH = "/system/notice";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request, NO_AUTH_FLAG } from "@utils";
|
||||||
import { NO_AUTH_FLAG } from "@utils/http";
|
|
||||||
|
|
||||||
const API_PATH = "/system/param";
|
const API_PATH = "/system/param";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/position";
|
const API_PATH = "/system/position";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/role";
|
const API_PATH = "/system/role";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/system/tenant";
|
const API_PATH = "/system/tenant";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
import { MenuTable, MenuForm } from "@/api/module_system/menu";
|
import { MenuTable, MenuForm } from "@/api/module_system/menu";
|
||||||
|
|
||||||
const API_PATH = "/system/user";
|
const API_PATH = "/system/user";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/task/cronjob/job";
|
const API_PATH = "/task/cronjob/job";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
const API_PATH = "/task/cronjob/node";
|
const API_PATH = "/task/cronjob/node";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
/** 对应后端 `plugin.module_task.workflow.definition` */
|
/** 对应后端 `plugin.module_task.workflow.definition` */
|
||||||
const API_PATH = "/task/workflow/definition";
|
const API_PATH = "/task/workflow/definition";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "@utils/http";
|
import { request } from "@utils";
|
||||||
|
|
||||||
/** 对应后端 `plugin.module_task.workflow.node_type` */
|
/** 对应后端 `plugin.module_task.workflow.node_type` */
|
||||||
const API_PATH = "/task/workflow/node-type";
|
const API_PATH = "/task/workflow/node-type";
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from "vue";
|
import { onMounted, ref, computed } from "vue";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
const settingStore = useSettingsStore();
|
const settingStore = useSettingsStore();
|
||||||
const { isDark } = storeToRefs(settingStore);
|
const { isDark } = storeToRefs(settingStore);
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
height: "11rem",
|
height: "11rem",
|
||||||
titleColor: "white",
|
titleColor: "white",
|
||||||
subtitleColor: "white",
|
subtitleColor: "white",
|
||||||
boxStyle: "!bg-theme/60",
|
boxStyle: "bg-theme/60!",
|
||||||
decoration: true,
|
decoration: true,
|
||||||
buttonConfig: () => ({
|
buttonConfig: () => ({
|
||||||
show: true,
|
show: true,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<div class="pb-5 text-lg font-medium">评论 {{ comments.length }}</div>
|
<div class="pb-5 text-lg font-medium">评论 {{ comments.length }}</div>
|
||||||
<CommentItem
|
<FaCommentItem
|
||||||
v-for="comment in comments.slice().reverse()"
|
v-for="comment in comments.slice().reverse()"
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
@@ -42,8 +42,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import CommentItem from "./widget/CommentItem.vue";
|
|
||||||
import { commentList, Comment } from "@/mock/temp/commentDetail";
|
import { commentList, Comment } from "@/mock/temp/commentDetail";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const comments = commentList;
|
const comments = commentList;
|
||||||
|
|
||||||
const newComment = ref<Partial<Comment>>({
|
const newComment = ref<Partial<Comment>>({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="chartRef"
|
ref="chartRef"
|
||||||
class="absolute bottom-0 left-0 right-0 mx-auto"
|
class="absolute bottom-0 left-0 right-0 mx-auto"
|
||||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
:class="isMiniChart ? 'absolute! top-5! right-5! bottom-auto! left-auto! h-15! w-4/10!' : ''"
|
||||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex gap-4 text-xs text-g-600">
|
<div class="mt-2 flex gap-4 text-xs text-g-600">
|
||||||
<div v-if="currentValue" class="flex-cc">
|
<div v-if="currentValue" class="flex-cc">
|
||||||
<div class="size-2 bg-theme/100 rounded mr-2"></div>
|
<div class="size-2 bg-theme rounded mr-2"></div>
|
||||||
{{ currentValue }}
|
{{ currentValue }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="previousValue" class="flex-cc">
|
<div v-if="previousValue" class="flex-cc">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="chartRef"
|
ref="chartRef"
|
||||||
class="absolute bottom-0 left-0 right-0 box-border w-full"
|
class="absolute bottom-0 left-0 right-0 box-border w-full"
|
||||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
:class="isMiniChart ? 'absolute! top-5! right-5! bottom-auto! left-auto! h-15! w-4/10!' : ''"
|
||||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
||||||
import { getCssVar, hexToRgba } from "@utils/ui";
|
import { getCssVar, hexToRgba } from "@utils";
|
||||||
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
||||||
|
|
||||||
defineOptions({ name: "FaLineChartCard" });
|
defineOptions({ name: "FaLineChartCard" });
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
<ElScrollbar :style="{ height: maxHeight }">
|
<ElScrollbar :style="{ height: maxHeight }">
|
||||||
<ElTimeline class="!pl-0.5">
|
<ElTimeline class="pl-0.5!">
|
||||||
<ElTimelineItem
|
<ElTimelineItem
|
||||||
v-for="item in list"
|
v-for="item in list"
|
||||||
:key="item.time"
|
:key="item.time"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
||||||
import { getCssVar } from "@utils/ui";
|
import { getCssVar } from "@utils";
|
||||||
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
||||||
import type { BarChartProps, BarDataItem } from "@/types/component/chart";
|
import type { BarChartProps, BarDataItem } from "@/types/component/chart";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
||||||
import { getCssVar } from "@utils/ui";
|
import { getCssVar } from "@utils";
|
||||||
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
||||||
import type { BarChartProps, BarDataItem } from "@/types/component/chart";
|
import type { BarChartProps, BarDataItem } from "@/types/component/chart";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
import { graphic, type EChartsOption } from "@/plugins/echarts";
|
||||||
import { getCssVar, hexToRgba } from "@utils/ui";
|
import { getCssVar, hexToRgba } from "@utils";
|
||||||
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
||||||
import type { LineChartProps, LineDataItem } from "@/types/component/chart";
|
import type { LineChartProps, LineDataItem } from "@/types/component/chart";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { echarts } from "@/plugins/echarts";
|
import { echarts } from "@/plugins/echarts";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
import chinaMapJson from "@/mock/json/chinaMap.json";
|
import chinaMapJson from "@/mock/json/chinaMap.json";
|
||||||
import type { MapChartProps } from "@/types/component/chart";
|
import type { MapChartProps } from "@/types/component/chart";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EChartsOption } from "@/plugins/echarts";
|
import type { EChartsOption } from "@/plugins/echarts";
|
||||||
import { getCssVar } from "@utils/ui";
|
import { getCssVar } from "@utils";
|
||||||
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
|
||||||
import type { ScatterChartProps } from "@/types/component/chart";
|
import type { ScatterChartProps } from "@/types/component/chart";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ElDropdown v-if="hasAnyAuthItem">
|
<ElDropdown v-if="hasAnyAuthItem">
|
||||||
<FaIconButton icon="ri:more-2-fill" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
|
<FaIconButton icon="ri:more-2-fill" class="size-8! bg-g-200 dark:bg-g-300/45 text-sm" />
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<template v-for="item in list" :key="item.key">
|
<template v-for="item in list" :key="item.key">
|
||||||
|
|||||||
@@ -2,7 +2,106 @@
|
|||||||
<!-- 支持常用表单组件、自定义组件、插槽、校验、隐藏表单项 -->
|
<!-- 支持常用表单组件、自定义组件、插槽、校验、隐藏表单项 -->
|
||||||
<!-- 写法同 ElementPlus 官方文档组件,把属性写在 props 里面就可以了 -->
|
<!-- 写法同 ElementPlus 官方文档组件,把属性写在 props 里面就可以了 -->
|
||||||
<template>
|
<template>
|
||||||
<section class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
|
<ElScrollbar v-if="scrollbar" :max-height="maxHeight" :view-style="{ overflowX: 'hidden' }">
|
||||||
|
<section class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="modelValue"
|
||||||
|
:label-position="labelPosition"
|
||||||
|
v-bind="{ ...$attrs }"
|
||||||
|
>
|
||||||
|
<ElRow class="flex flex-wrap" :gutter="gutter">
|
||||||
|
<ElCol
|
||||||
|
v-for="item in visibleFormItems"
|
||||||
|
:key="item.key"
|
||||||
|
:xs="getColSpan(item.span, 'xs')"
|
||||||
|
:sm="getColSpan(item.span, 'sm')"
|
||||||
|
:md="getColSpan(item.span, 'md')"
|
||||||
|
:lg="getColSpan(item.span, 'lg')"
|
||||||
|
:xl="getColSpan(item.span, 'xl')"
|
||||||
|
>
|
||||||
|
<ElFormItem
|
||||||
|
:prop="item.key"
|
||||||
|
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
|
||||||
|
>
|
||||||
|
<template #label v-if="item.label">
|
||||||
|
<component v-if="typeof item.label !== 'string'" :is="item.label" />
|
||||||
|
<span v-else>{{ item.label }}</span>
|
||||||
|
</template>
|
||||||
|
<slot :name="item.key" :item="item" :modelValue="modelValue">
|
||||||
|
<component
|
||||||
|
:is="getComponent(item)"
|
||||||
|
:model-value="getFieldValue(item.key)"
|
||||||
|
@update:model-value="setFieldValue(item.key, $event)"
|
||||||
|
v-bind="getProps(item)"
|
||||||
|
>
|
||||||
|
<!-- 下拉选择 -->
|
||||||
|
<template v-if="item.type === 'select' && getProps(item)?.options">
|
||||||
|
<ElOption
|
||||||
|
v-for="option in getProps(item).options"
|
||||||
|
v-bind="option"
|
||||||
|
:key="option.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 复选框组 -->
|
||||||
|
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
|
||||||
|
<ElCheckbox
|
||||||
|
v-for="option in getProps(item).options"
|
||||||
|
v-bind="option"
|
||||||
|
:key="option.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 单选框组 -->
|
||||||
|
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
|
||||||
|
<ElRadio
|
||||||
|
v-for="option in getProps(item).options"
|
||||||
|
v-bind="option"
|
||||||
|
:key="option.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 动态插槽支持 -->
|
||||||
|
<template
|
||||||
|
v-for="(slotFn, slotName) in getSlots(item)"
|
||||||
|
:key="slotName"
|
||||||
|
#[slotName]
|
||||||
|
>
|
||||||
|
<component :is="slotFn" />
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
</slot>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="max-w-full flex-1">
|
||||||
|
<div
|
||||||
|
class="mb-3 flex-c flex-wrap justify-end md:flex-row md:items-stretch md:gap-2"
|
||||||
|
:style="actionButtonsStyle"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 md:justify-center">
|
||||||
|
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
|
||||||
|
{{ t("table.form.reset") }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="showSubmit"
|
||||||
|
type="primary"
|
||||||
|
class="submit-button"
|
||||||
|
@click="handleSubmit"
|
||||||
|
v-ripple
|
||||||
|
:disabled="disabledSubmit"
|
||||||
|
>
|
||||||
|
{{ t("table.form.submit") }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElForm>
|
||||||
|
</section>
|
||||||
|
</ElScrollbar>
|
||||||
|
<!-- 不使用滚动条时直接渲染 -->
|
||||||
|
<section v-else class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="modelValue"
|
:model="modelValue"
|
||||||
@@ -34,7 +133,6 @@
|
|||||||
@update:model-value="setFieldValue(item.key, $event)"
|
@update:model-value="setFieldValue(item.key, $event)"
|
||||||
v-bind="getProps(item)"
|
v-bind="getProps(item)"
|
||||||
>
|
>
|
||||||
<!-- 下拉选择 -->
|
|
||||||
<template v-if="item.type === 'select' && getProps(item)?.options">
|
<template v-if="item.type === 'select' && getProps(item)?.options">
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="option in getProps(item).options"
|
v-for="option in getProps(item).options"
|
||||||
@@ -42,8 +140,6 @@
|
|||||||
:key="option.value"
|
:key="option.value"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 复选框组 -->
|
|
||||||
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
|
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
|
||||||
<ElCheckbox
|
<ElCheckbox
|
||||||
v-for="option in getProps(item).options"
|
v-for="option in getProps(item).options"
|
||||||
@@ -51,8 +147,6 @@
|
|||||||
:key="option.value"
|
:key="option.value"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 单选框组 -->
|
|
||||||
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
|
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
|
||||||
<ElRadio
|
<ElRadio
|
||||||
v-for="option in getProps(item).options"
|
v-for="option in getProps(item).options"
|
||||||
@@ -60,8 +154,6 @@
|
|||||||
:key="option.value"
|
:key="option.value"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 动态插槽支持 -->
|
|
||||||
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
|
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
|
||||||
<component :is="slotFn" />
|
<component :is="slotFn" />
|
||||||
</template>
|
</template>
|
||||||
@@ -108,7 +200,7 @@
|
|||||||
import { useWindowSize } from "@vueuse/core";
|
import { useWindowSize } from "@vueuse/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { toRaw, type Component } from "vue";
|
import { toRaw, type Component } from "vue";
|
||||||
import DatePicker from "@/components/DatePicker/index.vue";
|
import FaDatePicker from "@/components/forms/fa-search-bar/FaDatePicker.vue";
|
||||||
import {
|
import {
|
||||||
ElCascader,
|
ElCascader,
|
||||||
ElCheckbox,
|
ElCheckbox,
|
||||||
@@ -126,7 +218,7 @@ import {
|
|||||||
ElTreeSelect,
|
ElTreeSelect,
|
||||||
type FormInstance,
|
type FormInstance,
|
||||||
} from "element-plus";
|
} from "element-plus";
|
||||||
import { calculateResponsiveSpan, type ResponsiveBreakpoint } from "@utils/form";
|
import { calculateResponsiveSpan, type ResponsiveBreakpoint } from "@utils";
|
||||||
|
|
||||||
defineOptions({ name: "FaForm" });
|
defineOptions({ name: "FaForm" });
|
||||||
|
|
||||||
@@ -139,10 +231,10 @@ const componentMap = {
|
|||||||
checkbox: ElCheckbox, // 复选框
|
checkbox: ElCheckbox, // 复选框
|
||||||
checkboxgroup: ElCheckboxGroup, // 复选框组
|
checkboxgroup: ElCheckboxGroup, // 复选框组
|
||||||
radiogroup: ElRadioGroup, // 单选框组
|
radiogroup: ElRadioGroup, // 单选框组
|
||||||
date: DatePicker, // 日期选择器
|
date: FaDatePicker, // 日期选择器
|
||||||
daterange: DatePicker, // 日期范围选择器
|
daterange: FaDatePicker, // 日期范围选择器
|
||||||
datetime: DatePicker, // 日期时间选择器
|
datetime: FaDatePicker, // 日期时间选择器
|
||||||
datetimerange: DatePicker, // 日期时间范围选择器
|
datetimerange: FaDatePicker, // 日期时间范围选择器
|
||||||
rate: ElRate, // 评分
|
rate: ElRate, // 评分
|
||||||
slider: ElSlider, // 滑块
|
slider: ElSlider, // 滑块
|
||||||
cascader: ElCascader, // 级联选择器
|
cascader: ElCascader, // 级联选择器
|
||||||
@@ -206,6 +298,10 @@ interface FormProps {
|
|||||||
disabledSubmit?: boolean;
|
disabledSubmit?: boolean;
|
||||||
/** 提交时是否清洗空值 */
|
/** 提交时是否清洗空值 */
|
||||||
sanitizeOutput?: Partial<SanitizeOutputOptions>;
|
sanitizeOutput?: Partial<SanitizeOutputOptions>;
|
||||||
|
/** 是否需要内置 ElScrollbar 包裹 */
|
||||||
|
scrollbar?: boolean;
|
||||||
|
/** ElScrollbar 最大高度 */
|
||||||
|
maxHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SanitizeOutputOptions {
|
interface SanitizeOutputOptions {
|
||||||
@@ -234,6 +330,8 @@ const props = withDefaults(defineProps<FormProps>(), {
|
|||||||
showSubmit: true,
|
showSubmit: true,
|
||||||
disabledSubmit: false,
|
disabledSubmit: false,
|
||||||
sanitizeOutput: () => ({}),
|
sanitizeOutput: () => ({}),
|
||||||
|
scrollbar: false,
|
||||||
|
maxHeight: "75vh",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface FormEmits {
|
interface FormEmits {
|
||||||
@@ -505,6 +603,12 @@ const handleSubmit = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
ref: formInstance,
|
ref: formInstance,
|
||||||
validate: (...args: any[]) => formInstance.value?.validate(...args),
|
validate: (...args: any[]) => formInstance.value?.validate(...args),
|
||||||
|
/** 代理 ElForm.resetFields */
|
||||||
|
resetFields: (...args: any[]) => formInstance.value?.resetFields(...args),
|
||||||
|
/** 代理 ElForm.clearValidate */
|
||||||
|
clearValidate: (...args: any[]) => formInstance.value?.clearValidate(...args),
|
||||||
|
/** 代理 ElForm.validateField */
|
||||||
|
validateField: (...args: any[]) => formInstance.value?.validateField(...args),
|
||||||
reset: handleReset,
|
reset: handleReset,
|
||||||
// 允许外部在不触发提交事件时主动获取清洗后的输出。
|
// 允许外部在不触发提交事件时主动获取清洗后的输出。
|
||||||
getOutput: getSanitizedOutput,
|
getOutput: getSanitizedOutput,
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="!$slots.created_id" #created_id>
|
<template v-if="!$slots.created_id" #created_id>
|
||||||
<div class="w-full min-w-0">
|
<div class="w-full min-w-0">
|
||||||
<UserTableSelect
|
<FaUserTableSelect
|
||||||
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
|
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
|
||||||
@update:model-value="(v) => patchField('created_id', v)"
|
@update:model-value="(v: number | undefined) => patchField('created_id', v)"
|
||||||
@confirm-click="emitImmediateSearch"
|
@confirm-click="emitImmediateSearch"
|
||||||
@clear-click="emitImmediateSearch"
|
@clear-click="emitImmediateSearch"
|
||||||
/>
|
/>
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="!$slots.updated_id" #updated_id>
|
<template v-if="!$slots.updated_id" #updated_id>
|
||||||
<div class="w-full min-w-0">
|
<div class="w-full min-w-0">
|
||||||
<UserTableSelect
|
<FaUserTableSelect
|
||||||
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
|
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
|
||||||
@update:model-value="(v) => patchField('updated_id', v)"
|
@update:model-value="(v: number | undefined) => patchField('updated_id', v)"
|
||||||
@confirm-click="emitImmediateSearch"
|
@confirm-click="emitImmediateSearch"
|
||||||
@clear-click="emitImmediateSearch"
|
@clear-click="emitImmediateSearch"
|
||||||
/>
|
/>
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck
|
||||||
import { computed, ref, useAttrs } from "vue";
|
import { computed, ref, useAttrs } from "vue";
|
||||||
import FaSearchBar from "./index.vue";
|
import FaSearchBar from "./index.vue";
|
||||||
import type { SearchFormItem } from "./index.vue";
|
import type { SearchFormItem } from "./index.vue";
|
||||||
@@ -42,7 +44,6 @@ import {
|
|||||||
getAuditSearchFormItems,
|
getAuditSearchFormItems,
|
||||||
type GetAuditSearchFormItemsOptions,
|
type GetAuditSearchFormItemsOptions,
|
||||||
} from "./auditSearchFormItems";
|
} from "./auditSearchFormItems";
|
||||||
import UserTableSelect from "@views/module_system/user/components/UserTableSelect.vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaSearchBarWithAudit", inheritAttrs: false });
|
defineOptions({ name: "FaSearchBarWithAudit", inheritAttrs: false });
|
||||||
|
|
||||||
@@ -65,13 +66,10 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const modelValue = defineModel<Record<string, any>>({ default: () => ({}) });
|
const modelValue = defineModel<Record<string, any>>({ default: () => ({}) });
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
|
// @ts-expect-error 循环类型引用问题
|
||||||
const innerRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
const innerRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||||||
|
|
||||||
const forwardedAttrs = computed(() => attrs as Record<string, unknown>);
|
const forwardedAttrs = computed(() => attrs as Record<string, unknown>);
|
||||||
|
|
||||||
const auditItems = computed(() => getAuditSearchFormItems(props.auditItemOptions));
|
const auditItems = computed(() => getAuditSearchFormItems(props.auditItemOptions));
|
||||||
|
|
||||||
const mergedItems = computed(() => {
|
const mergedItems = computed(() => {
|
||||||
if (!props.includeAudit) return props.items;
|
if (!props.includeAudit) return props.items;
|
||||||
return [...props.items, ...auditItems.value];
|
return [...props.items, ...auditItems.value];
|
||||||
@@ -85,10 +83,11 @@ function emitImmediateSearch() {
|
|||||||
emit("search", { ...modelValue.value });
|
emit("search", { ...modelValue.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error 循环类型引用问题
|
||||||
defineExpose({
|
defineExpose({
|
||||||
validate: (...args: any[]) => innerRef.value?.validate?.(...args),
|
validate: (...args: any[]) => innerRef.value?.validate?.(...args),
|
||||||
reset: () => innerRef.value?.reset?.(),
|
reset: () => innerRef.value?.reset?.(),
|
||||||
getOutput: () => innerRef.value?.getOutput?.(),
|
getOutput: () => innerRef.value?.getOutput?.(),
|
||||||
ref: computed(() => innerRef.value?.ref),
|
ref: computed(() => innerRef.value?.ref) as unknown as any,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ import { ArrowUpBold, ArrowDownBold, Refresh, Search } from "@element-plus/icons
|
|||||||
import { useWindowSize } from "@vueuse/core";
|
import { useWindowSize } from "@vueuse/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { toRaw, type Component } from "vue";
|
import { toRaw, type Component } from "vue";
|
||||||
import DatePicker from "@/components/DatePicker/index.vue";
|
import FaDatePicker from "@/components/forms/fa-search-bar/FaDatePicker.vue";
|
||||||
import {
|
import {
|
||||||
ElCascader,
|
ElCascader,
|
||||||
ElCheckbox,
|
ElCheckbox,
|
||||||
@@ -144,10 +144,10 @@ const componentMap = {
|
|||||||
checkbox: ElCheckbox, // 复选框
|
checkbox: ElCheckbox, // 复选框
|
||||||
checkboxgroup: ElCheckboxGroup, // 复选框组
|
checkboxgroup: ElCheckboxGroup, // 复选框组
|
||||||
radiogroup: ElRadioGroup, // 单选框组
|
radiogroup: ElRadioGroup, // 单选框组
|
||||||
date: DatePicker, // 日期选择器
|
date: FaDatePicker, // 日期选择器
|
||||||
daterange: DatePicker, // 日期范围选择器
|
daterange: FaDatePicker, // 日期范围选择器
|
||||||
datetime: DatePicker, // 日期时间选择器
|
datetime: FaDatePicker, // 日期时间选择器
|
||||||
datetimerange: DatePicker, // 日期时间范围选择器
|
datetimerange: FaDatePicker, // 日期时间范围选择器
|
||||||
rate: ElRate, // 评分
|
rate: ElRate, // 评分
|
||||||
slider: ElSlider, // 滑块
|
slider: ElSlider, // 滑块
|
||||||
cascader: ElCascader, // 级联选择器
|
cascader: ElCascader, // 级联选择器
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
import "@wangeditor-next/editor/dist/css/style.css";
|
import "@wangeditor-next/editor/dist/css/style.css";
|
||||||
import { onBeforeUnmount, onMounted, shallowRef, computed } from "vue";
|
import { onBeforeUnmount, onMounted, shallowRef, computed } from "vue";
|
||||||
import { Editor, Toolbar } from "@wangeditor-next/editor-for-vue";
|
import { Editor, Toolbar } from "@wangeditor-next/editor-for-vue";
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
import { useUserStore } from "@stores";
|
||||||
import { EmojiText } from "@utils/ui";
|
import { request, EmojiText } from "@utils";
|
||||||
import { IDomEditor, IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor";
|
import { IDomEditor, IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor";
|
||||||
import request from "@utils/http";
|
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "FaWangEditor" });
|
defineOptions({ name: "FaWangEditor" });
|
||||||
|
|
||||||
@@ -258,5 +258,5 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "./style";
|
@use "@styles/fa-wang-editor";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- 面包屑导航 -->
|
<!-- 面包屑导航 -->
|
||||||
<template>
|
<template>
|
||||||
<nav class="ml-2.5 max-lg:!hidden" aria-label="breadcrumb">
|
<nav class="ml-2.5 max-lg:hidden!" aria-label="breadcrumb">
|
||||||
<ul class="flex-c h-full">
|
<ul class="flex-c h-full">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in breadcrumbItems"
|
v-for="(item, index) in breadcrumbItems"
|
||||||
|
|||||||
@@ -6,10 +6,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="text-base font-medium">Art Bot</span>
|
<span class="text-base font-medium">Art Bot</span>
|
||||||
<div class="mt-1.5 flex-c gap-1">
|
<div class="mt-1.5 flex-c gap-1">
|
||||||
<div
|
<div class="h-2 w-2 rounded-full" :class="isOnline ? 'bg-success' : 'bg-danger'"></div>
|
||||||
class="h-2 w-2 rounded-full"
|
|
||||||
:class="isOnline ? 'bg-success/100' : 'bg-danger/100'"
|
|
||||||
></div>
|
|
||||||
<span class="text-xs text-g-600">{{ isOnline ? "在线" : "离线" }}</span>
|
<span class="text-xs text-g-600">{{ isOnline ? "在线" : "离线" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +19,7 @@
|
|||||||
<div class="flex h-[calc(100%-70px)] flex-col">
|
<div class="flex h-[calc(100%-70px)] flex-col">
|
||||||
<!-- 聊天消息区域 -->
|
<!-- 聊天消息区域 -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 overflow-y-auto border-t-d px-4 py-7.5 [&::-webkit-scrollbar]:!w-1"
|
class="flex-1 overflow-y-auto border-t-d px-4 py-7.5 [&::-webkit-scrollbar]:w-1!"
|
||||||
ref="messageContainer"
|
ref="messageContainer"
|
||||||
>
|
>
|
||||||
<template v-for="(message, index) in messages" :key="index">
|
<template v-for="(message, index) in messages" :key="index">
|
||||||
@@ -91,7 +88,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Picture, Paperclip, Close } from "@element-plus/icons-vue";
|
import { Picture, Paperclip, Close } from "@element-plus/icons-vue";
|
||||||
import { mittBus } from "@utils/sys";
|
import { mittBus } from "@utils";
|
||||||
import meAvatar from "@imgs/avatar/avatar5.webp";
|
import meAvatar from "@imgs/avatar/avatar5.webp";
|
||||||
import aiAvatar from "@imgs/avatar/avatar10.webp";
|
import aiAvatar from "@imgs/avatar/avatar10.webp";
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="application in enabledApplications"
|
v-for="application in enabledApplications"
|
||||||
:key="application.name"
|
:key="application.name"
|
||||||
class="mr-3 c-p flex-c gap-3 rounded-lg p-2 hover:bg-g-200/70 dark:hover:bg-g-200/90 hover:[&_.app-icon]:!bg-transparent"
|
class="mr-3 c-p flex-c gap-3 rounded-lg p-2 hover:bg-g-200/70 dark:hover:bg-g-200/90 hover:[&_.app-icon]:bg-transparent!"
|
||||||
@click="handleApplicationClick(application)"
|
@click="handleApplicationClick(application)"
|
||||||
>
|
>
|
||||||
<div class="app-icon size-12 flex-cc rounded-lg bg-g-200/80 dark:bg-g-300/30">
|
<div class="app-icon size-12 flex-cc rounded-lg bg-g-200/80 dark:bg-g-300/30">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
>
|
>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<div class="mb-5 flex items-start gap-4">
|
<div class="mb-5 flex items-start gap-4">
|
||||||
<div class="flex-cc size-14 rounded-xl bg-gradient-to-br from-purple-400 to-indigo-500">
|
<div class="flex-cc size-14 rounded-xl bg-linear-to-br from-purple-400 to-indigo-500">
|
||||||
<FaSvgIcon icon="ri:loader-line" class="text-2xl text-white" />
|
<FaSvgIcon icon="ri:loader-line" class="text-2xl text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -84,29 +84,29 @@
|
|||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<ElTag type="primary" class="!rounded-md !border-0 flex-shrink-0">快捷键</ElTag>
|
<ElTag type="primary" class="rounded-md! border-0! shrink-0">快捷键</ElTag>
|
||||||
<span class="text-sm text-g-600">
|
<span class="text-sm text-g-600">
|
||||||
<ElTag size="small" class="!mr-1">Ctrl</ElTag>
|
<ElTag size="small" class="mr-1!">Ctrl</ElTag>
|
||||||
+
|
+
|
||||||
<ElTag size="small" class="!mx-1">Shift</ElTag>
|
<ElTag size="small" class="mx-1!">Shift</ElTag>
|
||||||
+
|
+
|
||||||
<ElTag size="small" class="!ml-1">P</ElTag>
|
<ElTag size="small" class="ml-1!">P</ElTag>
|
||||||
<span class="mx-2 text-g-400">/</span>
|
<span class="mx-2 text-g-400">/</span>
|
||||||
<ElTag size="small" class="!mr-1">⌘</ElTag>
|
<ElTag size="small" class="mr-1!">⌘</ElTag>
|
||||||
+
|
+
|
||||||
<ElTag size="small" class="!mx-1">Shift</ElTag>
|
<ElTag size="small" class="mx-1!">Shift</ElTag>
|
||||||
+
|
+
|
||||||
<ElTag size="small" class="!ml-1">P</ElTag>
|
<ElTag size="small" class="ml-1!">P</ElTag>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<ElTag type="success" class="!rounded-md !border-0 flex-shrink-0">节日素材</ElTag>
|
<ElTag type="success" class="rounded-md! border-0! shrink-0">节日素材</ElTag>
|
||||||
<span class="text-sm text-g-600">
|
<span class="text-sm text-g-600">
|
||||||
礼花素材与当前节日配置一致,自动匹配春节、元宵、中秋等传统节日主题
|
礼花素材与当前节日配置一致,自动匹配春节、元宵、中秋等传统节日主题
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<ElTag type="warning" class="!rounded-md !border-0 flex-shrink-0">触发方式</ElTag>
|
<ElTag type="warning" class="rounded-md! border-0! shrink-0">触发方式</ElTag>
|
||||||
<span class="text-sm text-g-600">
|
<span class="text-sm text-g-600">
|
||||||
页面加载时自动检测节日并连发,也可通过快捷键手动触发一次
|
页面加载时自动检测节日并连发,也可通过快捷键手动触发一次
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<canvas
|
<canvas
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
class="fixed top-0 left-0 z-[9999] w-full h-full pointer-events-none"
|
class="fixed top-0 left-0 z-9999 w-full h-full pointer-events-none"
|
||||||
></canvas>
|
></canvas>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
import { mittBus } from "@utils/sys";
|
import { mittBus } from "@utils";
|
||||||
import type { Handler } from "mitt";
|
import type { Handler } from "mitt";
|
||||||
import { festivalConfigList } from "@/config/modules/festival";
|
import { festivalConfigList } from "@/config/modules/festival";
|
||||||
import { buildBuiltinSolarFestivals } from "@/config/modules/festival.builtin";
|
import { buildBuiltinSolarFestivals } from "@/config/modules/festival.builtin";
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<div
|
<div
|
||||||
class="h-4.5 flex-cc rounded border border-g-300 dark:!bg-g-200/50 !bg-box px-1.5 text-g-500"
|
class="h-4.5 flex-cc rounded border border-g-300 dark:bg-g-200/50! bg-box! px-1.5 text-g-500"
|
||||||
>
|
>
|
||||||
<FaSvgIcon icon="fluent:arrow-enter-left-20-filled" />
|
<FaSvgIcon icon="fluent:arrow-enter-left-20-filled" />
|
||||||
</div>
|
</div>
|
||||||
@@ -29,13 +29,13 @@
|
|||||||
<ElScrollbar class="mt-5" max-height="370px" ref="searchResultScrollbar" always>
|
<ElScrollbar class="mt-5" max-height="370px" ref="searchResultScrollbar" always>
|
||||||
<div class="result w-full" v-show="searchResult.length">
|
<div class="result w-full" v-show="searchResult.length">
|
||||||
<div
|
<div
|
||||||
class="box !mt-0 c-p text-base leading-none"
|
class="box mt-0! c-p text-base leading-none"
|
||||||
v-for="(item, index) in searchResult"
|
v-for="(item, index) in searchResult"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mt-2 h-12 flex-cb rounded-custom-sm bg-g-200/80 px-4 text-sm text-g-700"
|
class="mt-2 h-12 flex-cb rounded-custom-sm bg-g-200/80 px-4 text-sm text-g-700"
|
||||||
:class="isHighlighted(index) ? 'highlighted !bg-theme/70 !text-white' : ''"
|
:class="isHighlighted(index) ? 'highlighted bg-theme/70! text-white!' : ''"
|
||||||
@click="searchGoPage(item)"
|
@click="searchGoPage(item)"
|
||||||
@mouseenter="highlightOnHover(index)"
|
@mouseenter="highlightOnHover(index)"
|
||||||
>
|
>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:class="
|
:class="
|
||||||
historyHIndex === index
|
historyHIndex === index
|
||||||
? 'highlighted !bg-theme/70 !text-white [&_.selected-icon]:!text-white'
|
? 'highlighted bg-theme/70! text-white! [&_.selected-icon]:text-white!'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
@click="searchGoPage(item)"
|
@click="searchGoPage(item)"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.switchKeydown") }}</span>
|
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.switchKeydown") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-c">
|
<div class="flex-c">
|
||||||
<i class="keyboard !w-8 flex-cc"><p class="text-[10px] font-medium">ESC</p></i>
|
<i class="keyboard w-8! flex-cc"><p class="text-[10px] font-medium">ESC</p></i>
|
||||||
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.exitKeydown") }}</span>
|
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.exitKeydown") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,13 +94,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
|
||||||
import { AppRouteRecord } from "@/types/router";
|
import { AppRouteRecord } from "@/types/router";
|
||||||
import { Search } from "@element-plus/icons-vue";
|
import { Search } from "@element-plus/icons-vue";
|
||||||
import { mittBus } from "@utils/sys";
|
import { mittBus, formatMenuTitle, handleMenuJump } from "@utils";
|
||||||
import { useMenuStore } from "@stores/modules/menu.store";
|
import { useUserStore, useMenuStore } from "@stores";
|
||||||
import { formatMenuTitle } from "@utils/navigation";
|
|
||||||
import { handleMenuJump } from "@utils/navigation";
|
|
||||||
import { type ScrollbarInstance } from "element-plus";
|
import { type ScrollbarInstance } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "FaGlobalSearch" });
|
defineOptions({ name: "FaGlobalSearch" });
|
||||||
|
|||||||
Executable → Regular
+12
-17
@@ -1,10 +1,10 @@
|
|||||||
<!-- 顶部栏 -->
|
<!-- 顶部栏 -->
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full bg-[var(--default-bg-color)]"
|
class="w-full bg-(--default-bg-color)"
|
||||||
:class="[
|
:class="[
|
||||||
tabStyle === 'tab-card' || tabStyle === 'tab-google' || tabStyle === 'tab-default'
|
tabStyle === 'tab-card' || tabStyle === 'tab-google' || tabStyle === 'tab-default'
|
||||||
? 'max-sm:mb-3 !bg-box'
|
? 'max-sm:mb-3 bg-box!'
|
||||||
: '',
|
: '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
class="relative box-border flex-b h-15 leading-15 select-none"
|
class="relative box-border flex-b h-15 leading-15 select-none"
|
||||||
:class="[
|
:class="[
|
||||||
tabStyle === 'tab-card' || tabStyle === 'tab-google' || tabStyle === 'tab-default'
|
tabStyle === 'tab-card' || tabStyle === 'tab-google' || tabStyle === 'tab-default'
|
||||||
? 'border-b border-[var(--fa-card-border)]'
|
? 'border-b border-(--fa-card-border)'
|
||||||
: '',
|
: '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<FaLogo
|
<FaLogo
|
||||||
v-if="showAppLogo"
|
v-if="showAppLogo"
|
||||||
class="!hidden pl-3.5 overflow-hidden align-[-0.15em] fill-current"
|
class="hidden! pl-3.5 overflow-hidden align-[-0.15em] fill-current"
|
||||||
:src="headerLogoSrc"
|
:src="headerLogoSrc"
|
||||||
@click="toHome"
|
@click="toHome"
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<FaIconButton
|
<FaIconButton
|
||||||
v-if="shouldShowRefreshButton"
|
v-if="shouldShowRefreshButton"
|
||||||
icon="ri:refresh-line"
|
icon="ri:refresh-line"
|
||||||
class="!ml-3 refresh-btn max-sm:!hidden"
|
class="ml-3! refresh-btn max-sm:hidden!"
|
||||||
:style="{ marginLeft: !isLeftMenu ? '10px' : '0' }"
|
:style="{ marginLeft: !isLeftMenu ? '10px' : '0' }"
|
||||||
@click="reload"
|
@click="reload"
|
||||||
/>
|
/>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<!-- 搜索 -->
|
<!-- 搜索 -->
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowGlobalSearch"
|
v-if="shouldShowGlobalSearch"
|
||||||
class="flex-cb w-40 h-9 px-2.5 c-p border border-g-400 rounded-custom-sm max-md:!hidden tad-300 hover:-translate-y-0.5 hover:shadow-md"
|
class="flex-cb w-40 h-9 px-2.5 c-p border border-g-400 rounded-custom-sm max-md:hidden! tad-300 hover:-translate-y-0.5 hover:shadow-md"
|
||||||
@click="openSearchDialog"
|
@click="openSearchDialog"
|
||||||
>
|
>
|
||||||
<div class="flex-c">
|
<div class="flex-c">
|
||||||
@@ -87,12 +87,12 @@
|
|||||||
v-if="shouldShowFullscreen"
|
v-if="shouldShowFullscreen"
|
||||||
:icon="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-fill'"
|
:icon="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-fill'"
|
||||||
:class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn', 'ml-3']"
|
:class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn', 'ml-3']"
|
||||||
class="max-md:!hidden"
|
class="max-md:hidden!"
|
||||||
@click="toggleFullScreen"
|
@click="toggleFullScreen"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 组件尺寸 default/large/small(沿用旧版持久化开关 showSizeSelect) -->
|
<!-- 组件尺寸 default/large/small(沿用旧版持久化开关 showSizeSelect) -->
|
||||||
<div v-if="shouldShowSizeSelect" class="flex-cc ml-1 max-md:!hidden">
|
<div v-if="shouldShowSizeSelect" class="flex-cc ml-1 max-md:hidden!">
|
||||||
<FaSizeSelect />
|
<FaSizeSelect />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
class="notice-button relative"
|
class="notice-button relative"
|
||||||
@click="visibleNotice"
|
@click="visibleNotice"
|
||||||
>
|
>
|
||||||
<div class="absolute top-2 right-2 size-1.5 !bg-danger rounded-full"></div>
|
<div class="absolute top-2 right-2 size-1.5 bg-danger! rounded-full"></div>
|
||||||
</FaIconButton>
|
</FaIconButton>
|
||||||
|
|
||||||
<!-- 聊天按钮 -->
|
<!-- 聊天按钮 -->
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
class="chat-button relative"
|
class="chat-button relative"
|
||||||
@click="openChat"
|
@click="openChat"
|
||||||
>
|
>
|
||||||
<div class="breathing-dot absolute top-2 right-2 size-1.5 !bg-success rounded-full"></div>
|
<div class="breathing-dot absolute top-2 right-2 size-1.5 bg-success! rounded-full"></div>
|
||||||
</FaIconButton>
|
</FaIconButton>
|
||||||
|
|
||||||
<!-- 设置按钮 -->
|
<!-- 设置按钮 -->
|
||||||
@@ -183,18 +183,13 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useFullscreen, useWindowSize } from "@vueuse/core";
|
import { useFullscreen, useWindowSize } from "@vueuse/core";
|
||||||
import { LanguageEnum, MenuTypeEnum } from "@/enums/appEnum";
|
import { LanguageEnum, MenuTypeEnum } from "@/enums/appEnum";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore, useMenuStore, useUserStore, useConfigStore } from "@stores";
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
|
||||||
import { useMenuStore } from "@stores/modules/menu.store";
|
|
||||||
import AppConfig from "@/config";
|
import AppConfig from "@/config";
|
||||||
import { useConfigStore } from "@stores/modules/config.store";
|
|
||||||
import { languageOptions } from "@/locales";
|
import { languageOptions } from "@/locales";
|
||||||
import { mittBus } from "@utils/sys";
|
import { mittBus, themeAnimation } from "@utils";
|
||||||
import { themeAnimation } from "@utils/ui";
|
|
||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { useHeaderBar } from "@/hooks/core/useHeaderBar";
|
import { useHeaderBar } from "@/hooks/core/useHeaderBar";
|
||||||
import FaUserMenu from "./widget/FaUserMenu.vue";
|
import FaUserMenu from "./widget/FaUserMenu.vue";
|
||||||
import FaSizeSelect from "@/components/others/fa-size-select/index.vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaHeaderBar" });
|
defineOptions({ name: "FaHeaderBar" });
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -32,7 +32,7 @@
|
|||||||
<div v-for="(item, key) in logoConfigs" :key="key">
|
<div v-for="(item, key) in logoConfigs" :key="key">
|
||||||
<ElFormItem :label="item.config_name">
|
<ElFormItem :label="item.config_name">
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
<SingleImageUpload
|
<FaUpload
|
||||||
v-model="item.config_value"
|
v-model="item.config_value"
|
||||||
:data="{ type: key }"
|
:data="{ type: key }"
|
||||||
:name="'file'"
|
:name="'file'"
|
||||||
@@ -288,14 +288,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, onMounted, computed } from "vue";
|
import { ref, reactive, onMounted, computed } from "vue";
|
||||||
import ParamsAPI, { type ConfigTable } from "@/api/module_system/params";
|
import ParamsAPI, { type ConfigTable } from "@/api/module_system/params";
|
||||||
import { useConfigStore } from "@stores";
|
import { useAppStore, useConfigStore } from "@stores";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
|
||||||
import SingleImageUpload from "@/components/others/fa-upload/index.vue";
|
|
||||||
import { useAppStore } from "@stores/modules/app.store";
|
|
||||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaConfigInfoDrawer" });
|
||||||
|
|
||||||
// 定义列表项类型
|
// 定义列表项类型
|
||||||
interface ListItem {
|
interface ListItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="py-4 mt-3 border-t border-g-300/80">
|
<ul class="py-4 mt-3 border-t border-g-300/80">
|
||||||
<li class="btn-item" @click="goPage('/profile')">
|
<li class="btn-item" @click="goPage('/fastlink/profile')">
|
||||||
<FaSvgIcon icon="ri:user-3-line" />
|
<FaSvgIcon icon="ri:user-3-line" />
|
||||||
<span>{{ $t("topBar.user.userCenter") }}</span>
|
<span>{{ $t("topBar.user.userCenter") }}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElPopover>
|
</ElPopover>
|
||||||
|
|
||||||
<ConfigInfoDrawer v-model="paramDrawerVisible" />
|
<FaConfigInfoDrawer v-model="paramDrawerVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,10 +93,8 @@
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessageBox } from "element-plus";
|
import { ElMessageBox } from "element-plus";
|
||||||
import ConfigInfoDrawer from "@views/module_system/param/components/ConfigInfoDrawer.vue";
|
import { useUserStore } from "@stores";
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
import { WEB_LINKS, mittBus } from "@utils";
|
||||||
import { WEB_LINKS } from "@utils/constants";
|
|
||||||
import { mittBus } from "@utils/sys";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaUserMenu" });
|
defineOptions({ name: "FaUserMenu" });
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
popper-class="horizontal-menu-popper"
|
popper-class="horizontal-menu-popper"
|
||||||
class="w-full border-none"
|
class="w-full border-none"
|
||||||
>
|
>
|
||||||
<HorizontalSubmenu
|
<FaHorizontalSubmenu
|
||||||
v-for="item in filteredMenuItems"
|
v-for="item in filteredMenuItems"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:item="item"
|
:item="item"
|
||||||
@@ -26,8 +26,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AppRouteRecord } from "@/types/router";
|
import type { AppRouteRecord } from "@/types/router";
|
||||||
import HorizontalSubmenu from "./widget/HorizontalSubmenu.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaHorizontalMenu" });
|
defineOptions({ name: "FaHorizontalMenu" });
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="!p-0">
|
<ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="p-0!">
|
||||||
<template #title>
|
<template #title>
|
||||||
<FaMenuRouteIcon :icon="item.meta.icon" :color="theme?.iconColor" class="mr-1 text-lg" />
|
<FaMenuRouteIcon :icon="item.meta.icon" :color="theme?.iconColor" class="mr-1 text-lg" />
|
||||||
<span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
|
<span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 递归调用自身处理子菜单 -->
|
<!-- 递归调用自身处理子菜单 -->
|
||||||
<HorizontalSubmenu
|
<FaHorizontalSubmenu
|
||||||
v-for="child in filteredChildren"
|
v-for="child in filteredChildren"
|
||||||
:key="child.path"
|
:key="child.path"
|
||||||
:item="child"
|
:item="child"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
:index="item.path || item.meta.title"
|
:index="item.path || item.meta.title"
|
||||||
@click="goPage(item)"
|
@click="goPage(item)"
|
||||||
>
|
>
|
||||||
<MenuRouteIcon
|
<FaMenuRouteIcon
|
||||||
:icon="item.meta.icon"
|
:icon="item.meta.icon"
|
||||||
:color="theme?.iconColor"
|
:color="theme?.iconColor"
|
||||||
class="mr-1 text-lg"
|
class="mr-1 text-lg"
|
||||||
@@ -47,9 +47,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, type PropType } from "vue";
|
import { computed, type PropType } from "vue";
|
||||||
import { AppRouteRecord } from "@/types/router";
|
import { AppRouteRecord } from "@/types/router";
|
||||||
import { handleMenuJump } from "@utils/navigation";
|
import { handleMenuJump, formatMenuTitle } from "@utils";
|
||||||
import { formatMenuTitle } from "@utils/navigation";
|
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
|
defineOptions({ name: "FaHorizontalSubmenu" });
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
@wheel="handleWheel"
|
@wheel="handleWheel"
|
||||||
>
|
>
|
||||||
<div class="box-border flex-c flex-shrink-0 flex-nowrap h-15 whitespace-nowrap">
|
<div class="box-border flex-c shrink-0 flex-nowrap h-15 whitespace-nowrap">
|
||||||
<template v-for="item in processedMenuList" :key="item.meta.title">
|
<template v-for="item in processedMenuList" :key="item.meta.title">
|
||||||
<div
|
<div
|
||||||
v-if="!item.meta.isHide"
|
v-if="!item.meta.isHide"
|
||||||
class="menu-item relative flex-shrink-0 h-10 px-3 text-sm flex-c c-p hover:text-theme"
|
class="menu-item relative shrink-0 h-10 px-3 text-sm flex-c c-p hover:text-theme"
|
||||||
:class="{
|
:class="{
|
||||||
'menu-item-active text-theme': item.isActive,
|
'menu-item-active text-theme': item.isActive,
|
||||||
}"
|
}"
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
<FaMenuRouteIcon
|
<FaMenuRouteIcon
|
||||||
:icon="item.meta.icon"
|
:icon="item.meta.icon"
|
||||||
class="text-lg text-g-700 dark:text-g-800 mr-1"
|
class="text-lg text-g-700 dark:text-g-800 mr-1"
|
||||||
:class="item.isActive && '!text-theme'"
|
:class="item.isActive && 'text-theme!'"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="text-md text-g-700 dark:text-g-800"
|
class="text-md text-g-700 dark:text-g-800"
|
||||||
:class="item.isActive && '!text-theme'"
|
:class="item.isActive && 'text-theme!'"
|
||||||
>
|
>
|
||||||
{{ item.formattedTitle }}
|
{{ item.formattedTitle }}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,10 +56,8 @@
|
|||||||
import { ref, computed, onMounted, nextTick } from "vue";
|
import { ref, computed, onMounted, nextTick } from "vue";
|
||||||
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
|
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
import { formatMenuTitle } from "@utils/navigation";
|
import { formatMenuTitle, handleMenuJump } from "@utils";
|
||||||
import { handleMenuJump } from "@utils/navigation";
|
|
||||||
import type { AppRouteRecord } from "@/types/router";
|
import type { AppRouteRecord } from "@/types/router";
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaMixedMenu" });
|
defineOptions({ name: "FaMixedMenu" });
|
||||||
|
|
||||||
|
|||||||
@@ -131,14 +131,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppConfig from "@/config";
|
import AppConfig from "@/config";
|
||||||
import { useConfigStore } from "@stores/modules/config.store";
|
import { useConfigStore, useSettingsStore, useMenuStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { MenuTypeEnum, MenuWidth } from "@/enums/appEnum";
|
import { MenuTypeEnum, MenuWidth } from "@/enums/appEnum";
|
||||||
import { useMenuStore } from "@stores/modules/menu.store";
|
import { isIframe, handleMenuJump } from "@utils";
|
||||||
import { isIframe } from "@utils/navigation";
|
import SidebarSubmenu from "./widget/FaSidebarSubmenu.vue";
|
||||||
import { handleMenuJump } from "@utils/navigation";
|
|
||||||
import SidebarSubmenu from "./widget/SidebarSubmenu.vue";
|
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
|
|
||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { useWindowSize, useTimeoutFn } from "@vueuse/core";
|
import { useWindowSize, useTimeoutFn } from "@vueuse/core";
|
||||||
|
|
||||||
@@ -366,11 +362,11 @@ watch(menuOpen, (isMenuOpen: boolean) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "./style";
|
@use "@styles/fa-sidebar-menu";
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "./theme";
|
@use "@styles/fa-sidebar-menu-theme";
|
||||||
|
|
||||||
.layout-sidebar {
|
.layout-sidebar {
|
||||||
// 展开的宽度
|
// 展开的宽度
|
||||||
|
|||||||
+5
-5
@@ -15,7 +15,7 @@
|
|||||||
<div v-if="item.meta.showBadge" class="fa-badge" :style="'right: 10px'" />
|
<div v-if="item.meta.showBadge" class="fa-badge" :style="'right: 10px'" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<SidebarSubmenu
|
<FaSidebarSubmenu
|
||||||
:list="item.children"
|
:list="item.children"
|
||||||
:is-mobile="isMobile"
|
:is-mobile="isMobile"
|
||||||
:level="level + 1"
|
:level="level + 1"
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { AppRouteRecord } from "@/types/router";
|
import type { AppRouteRecord } from "@/types/router";
|
||||||
import { formatMenuTitle } from "@utils/navigation";
|
import { formatMenuTitle, handleMenuJump } from "@utils";
|
||||||
import { handleMenuJump } from "@utils/navigation";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
|
defineOptions({ name: "FaSidebarSubmenu" });
|
||||||
|
|
||||||
interface MenuTheme {
|
interface MenuTheme {
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
@@ -62,9 +62,7 @@
|
|||||||
import type { CSSProperties } from "vue";
|
import type { CSSProperties } from "vue";
|
||||||
import { useMediaQuery } from "@vueuse/core";
|
import { useMediaQuery } from "@vueuse/core";
|
||||||
import { useRoute, type RouteLocationNormalizedLoaded } from "vue-router";
|
import { useRoute, type RouteLocationNormalizedLoaded } from "vue-router";
|
||||||
import FaSvgIcon from "@/components/base/fa-svg-icon/index.vue";
|
import { useSettingsStore, useWorktabStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useWorktabStore } from "@stores/modules/worktab.store";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaPageContent" });
|
defineOptions({ name: "FaPageContent" });
|
||||||
|
|
||||||
|
|||||||
@@ -149,16 +149,14 @@
|
|||||||
import { Lock } from "@element-plus/icons-vue";
|
import { Lock } from "@element-plus/icons-vue";
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
import { ElInput } from "element-plus";
|
import { ElInput } from "element-plus";
|
||||||
import FaDialog from "@/components/modal/fa-dialog/index.vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import CryptoJS from "crypto-js";
|
import CryptoJS from "crypto-js";
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
import { useUserStore, useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { mittBus, useNow } from "@utils";
|
||||||
import { mittBus } from "@utils/sys";
|
|
||||||
import { useNow } from "@utils/common";
|
|
||||||
import bgDark from "@imgs/lock/bg_dark.webp";
|
import bgDark from "@imgs/lock/bg_dark.webp";
|
||||||
import bgLight from "@imgs/lock/bg_light.webp";
|
import bgLight from "@imgs/lock/bg_light.webp";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import type { ContainerWidthEnum } from "@/enums/appEnum";
|
import type { ContainerWidthEnum } from "@/enums/appEnum";
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,11 +1,10 @@
|
|||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useBreakpoints } from "@vueuse/core";
|
import { useBreakpoints } from "@vueuse/core";
|
||||||
import AppConfig from "@/config";
|
import AppConfig from "@/config";
|
||||||
import { SystemThemeEnum, MenuTypeEnum } from "@/enums/appEnum";
|
import { SystemThemeEnum, MenuTypeEnum } from "@/enums/appEnum";
|
||||||
import { mittBus } from "@utils/sys";
|
import { mittBus, StorageConfig } from "@utils";
|
||||||
import { StorageConfig } from "@utils";
|
|
||||||
import { useTheme } from "@/hooks/core/useTheme";
|
import { useTheme } from "@/hooks/core/useTheme";
|
||||||
import { useCeremony } from "@/hooks/core/useCeremony";
|
import { useCeremony } from "@/hooks/core/useCeremony";
|
||||||
import { useSettingsState } from "./useSettingsState";
|
import { useSettingsState } from "./useSettingsState";
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
import { MenuThemeEnum, MenuTypeEnum } from "@/enums/appEnum";
|
import { MenuThemeEnum, MenuTypeEnum } from "@/enums/appEnum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,43 +1,32 @@
|
|||||||
<!-- 设置面板 -->
|
<!-- 设置面板 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="layout-settings">
|
<div class="layout-settings">
|
||||||
<SettingDrawer v-model="showDrawer" @open="handleOpen" @close="handleClose">
|
<FaSettingDrawer v-model="showDrawer" @open="handleOpen" @close="handleClose">
|
||||||
<!-- 头部关闭按钮 -->
|
<!-- 头部关闭按钮 -->
|
||||||
<SettingHeader @close="closeDrawer" />
|
<FaSettingHeader @close="closeDrawer" />
|
||||||
<!-- 主题风格 -->
|
<!-- 主题风格 -->
|
||||||
<ThemeSettings />
|
<FaThemeSettings />
|
||||||
<!-- 菜单布局 -->
|
<!-- 菜单布局 -->
|
||||||
<MenuLayoutSettings />
|
<FaMenuLayoutSettings />
|
||||||
<!-- 菜单风格 -->
|
<!-- 菜单风格 -->
|
||||||
<MenuStyleSettings />
|
<FaMenuStyleSettings />
|
||||||
<!-- 系统主题色 -->
|
<!-- 系统主题色 -->
|
||||||
<ColorSettings />
|
<FaColorSettings />
|
||||||
<!-- 盒子样式 -->
|
<!-- 盒子样式 -->
|
||||||
<BoxStyleSettings />
|
<FaBoxStyleSettings />
|
||||||
<!-- 容器宽度 -->
|
<!-- 容器宽度 -->
|
||||||
<ContainerSettings />
|
<FaContainerSettings />
|
||||||
<!-- 基础配置 -->
|
<!-- 基础配置 -->
|
||||||
<BasicSettings />
|
<FaBasicSettings />
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<SettingActions />
|
<FaSettingActions />
|
||||||
</SettingDrawer>
|
</FaSettingDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettingsPanel } from "./composables/useSettingsPanel";
|
import { useSettingsPanel } from "./composables/useSettingsPanel";
|
||||||
|
|
||||||
import SettingDrawer from "./widget/SettingDrawer.vue";
|
|
||||||
import SettingHeader from "./widget/SettingHeader.vue";
|
|
||||||
import ThemeSettings from "./widget/ThemeSettings.vue";
|
|
||||||
import MenuLayoutSettings from "./widget/MenuLayoutSettings.vue";
|
|
||||||
import MenuStyleSettings from "./widget/MenuStyleSettings.vue";
|
|
||||||
import ColorSettings from "./widget/ColorSettings.vue";
|
|
||||||
import BoxStyleSettings from "./widget/BoxStyleSettings.vue";
|
|
||||||
import ContainerSettings from "./widget/ContainerSettings.vue";
|
|
||||||
import BasicSettings from "./widget/BasicSettings.vue";
|
|
||||||
import SettingActions from "./widget/SettingActions.vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaSettingsPanel" });
|
defineOptions({ name: "FaSettingsPanel" });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -68,5 +57,5 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "./style";
|
@use "@styles/fa-settings-panel";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+3
-5
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle :title="$t('setting.basics.title')" class="mt-10" />
|
<FaSectionTitle :title="$t('setting.basics.title')" class="mt-10" />
|
||||||
<SettingItem
|
<FaSettingItem
|
||||||
v-for="config in basicSettingsConfig"
|
v-for="config in basicSettingsConfig"
|
||||||
:key="config.key"
|
:key="config.key"
|
||||||
:config="config"
|
:config="config"
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import SettingItem from "./SettingItem.vue";
|
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle :title="$t('setting.box.title')" class="mt-10" />
|
<FaSectionTitle :title="$t('setting.box.title')" class="mt-10" />
|
||||||
<div class="box-border flex-cb p-1 mt-5 rounded-lg bg-g-200">
|
<div class="box-border flex-cb p-1 mt-5 rounded-lg bg-g-200">
|
||||||
<div
|
<div
|
||||||
v-for="option in boxStyleOptions"
|
v-for="option in boxStyleOptions"
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
class="w-[calc(50%-3px)] h-8.5 leading-8.5 text-sm text-center c-p select-none rounded-md transition-all duration-200"
|
class="w-[calc(50%-3px)] h-8.5 leading-8.5 text-sm text-center c-p select-none rounded-md transition-all duration-200"
|
||||||
:class="
|
:class="
|
||||||
isActive(option.type)
|
isActive(option.type)
|
||||||
? 'text-g-800 bg-[var(--default-box-color)] dark:!text-white dark:bg-g-300'
|
? 'text-g-800 bg-(--default-box-color) dark:text-white! dark:bg-g-300'
|
||||||
: 'hover:text-g-800 hover:bg-black/[0.04] dark:hover:bg-black/20'
|
: 'hover:text-g-800 hover:bg-black/4 dark:hover:bg-black/20'
|
||||||
"
|
"
|
||||||
@click="boxStyleHandlers.setBoxMode(option.type)"
|
@click="boxStyleHandlers.setBoxMode(option.type)"
|
||||||
>
|
>
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
+3
-4
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle :title="$t('setting.color.title')" class="mt-10" />
|
<FaSectionTitle :title="$t('setting.color.title')" class="mt-10" />
|
||||||
<div class="-mr-4">
|
<div class="-mr-4">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div
|
<div
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
>
|
>
|
||||||
<FaSvgIcon
|
<FaSvgIcon
|
||||||
icon="ri:check-fill"
|
icon="ri:check-fill"
|
||||||
class="text-base !text-white"
|
class="text-base text-white!"
|
||||||
v-show="color === systemThemeColor"
|
v-show="color === systemThemeColor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
+4
-5
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle :title="$t('setting.container.title')" class="mt-12.5" />
|
<FaSectionTitle :title="$t('setting.container.title')" class="mt-12.5" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div
|
<div
|
||||||
v-for="option in containerWidthOptions"
|
v-for="option in containerWidthOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="flex-cc flex-1 h-16 mt-5 mr-3.5 mb-3.5 cursor-pointer !border-2 rounded-lg !text-g-800 last:mr-0"
|
class="flex-cc flex-1 h-16 mt-5 mr-3.5 mb-3.5 cursor-pointer border-2! rounded-lg text-g-800! last:mr-0"
|
||||||
:class="{
|
:class="{
|
||||||
'border-theme [&_i]:!text-theme': containerWidth === option.value,
|
'border-theme [&_i]:text-theme!': containerWidth === option.value,
|
||||||
'border-full-d': containerWidth !== option.value,
|
'border-full-d': containerWidth !== option.value,
|
||||||
}"
|
}"
|
||||||
@click="containerHandlers.setWidth(option.value)"
|
@click="containerHandlers.setWidth(option.value)"
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="width > 1000">
|
<div v-if="width > 1000">
|
||||||
<SectionTitle :title="$t('setting.menuType.title')" />
|
<FaSectionTitle :title="$t('setting.menuType.title')" />
|
||||||
<div class="setting-box-wrap">
|
<div class="setting-box-wrap">
|
||||||
<div
|
<div
|
||||||
class="setting-item"
|
class="setting-item"
|
||||||
@@ -18,8 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useSettingsState } from "../composables/useSettingsState";
|
import { useSettingsState } from "../composables/useSettingsState";
|
||||||
|
|
||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SectionTitle :title="$t('setting.menu.title')" />
|
<FaSectionTitle :title="$t('setting.menu.title')" />
|
||||||
<div class="setting-box-wrap">
|
<div class="setting-box-wrap">
|
||||||
<div
|
<div
|
||||||
class="setting-item"
|
class="setting-item"
|
||||||
@@ -22,9 +22,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppConfig from "@/config";
|
import AppConfig from "@/config";
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
|
||||||
import { MenuTypeEnum, type MenuThemeEnum } from "@/enums/appEnum";
|
import { MenuTypeEnum, type MenuThemeEnum } from "@/enums/appEnum";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
|
|
||||||
const menuThemeList = AppConfig.themeList;
|
const menuThemeList = AppConfig.themeList;
|
||||||
const settingStore = useSettingsStore();
|
const settingStore = useSettingsStore();
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<p
|
<p
|
||||||
class="relative mt-7.5 mb-5.5 text-sm text-center text-g-800 before:absolute before:top-[10px] before:left-0 before:w-[50px] before:m-auto before:content-[''] before:border-b before:border-[var(--fa-gray-300)] after:absolute after:top-[10px] after:right-0 after:w-[50px] after:m-auto after:content-[''] after:border-b after:border-g-300"
|
class="relative mt-7.5 mb-5.5 text-sm text-center text-g-800 before:absolute before:top-[10px] before:left-0 before:w-[50px] before:m-auto before:content-[''] before:border-b before:border-(--fa-gray-300) after:absolute after:top-[10px] after:right-0 after:w-[50px] after:m-auto after:content-[''] after:border-b after:border-g-300"
|
||||||
:style="style"
|
:style="style"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
+5
-6
@@ -1,12 +1,10 @@
|
|||||||
<!-- 设置操作按钮 -->
|
<!-- 设置操作按钮 -->
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="mt-10 flex gap-8 border-t border-(--default-border) bg-(--fa-bg-color) pt-5">
|
||||||
class="mt-10 flex gap-8 border-t border-[var(--default-border)] bg-[var(--fa-bg-color)] pt-5"
|
<ElButton type="primary" class="flex-1 h-8!" @click="handleCopyConfig">
|
||||||
>
|
|
||||||
<ElButton type="primary" class="flex-1 !h-8" @click="handleCopyConfig">
|
|
||||||
{{ $t("setting.actions.copyConfig") }}
|
{{ $t("setting.actions.copyConfig") }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="danger" plain class="flex-1 !h-8" @click="handleResetConfig">
|
<ElButton type="danger" plain class="flex-1 h-8!" @click="handleResetConfig">
|
||||||
{{ $t("setting.actions.resetConfig") }}
|
{{ $t("setting.actions.resetConfig") }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,12 +12,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
import { SETTING_DEFAULT_CONFIG } from "@/config/setting";
|
import { SETTING_DEFAULT_CONFIG } from "@/config/setting";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { MenuThemeEnum } from "@/enums/appEnum";
|
import { MenuThemeEnum } from "@/enums/appEnum";
|
||||||
import { useTheme } from "@/hooks/core/useTheme";
|
import { useTheme } from "@/hooks/core/useTheme";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "SettingActions" });
|
defineOptions({ name: "SettingActions" });
|
||||||
|
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div
|
<div
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
class="flex-cc c-p size-7.5 !transition-all duration-200 rounded hover:bg-g-300/80"
|
class="flex-cc c-p size-7.5 transition-all! duration-200 rounded hover:bg-g-300/80"
|
||||||
>
|
>
|
||||||
<FaSvgIcon icon="ri:close-fill" class="block text-xl text-g-600" />
|
<FaSvgIcon icon="ri:close-fill" class="block text-xl text-g-600" />
|
||||||
</div>
|
</div>
|
||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SectionTitle :title="$t('setting.theme.title')" />
|
<FaSectionTitle :title="$t('setting.theme.title')" />
|
||||||
<div class="setting-box-wrap">
|
<div class="setting-box-wrap">
|
||||||
<div
|
<div
|
||||||
class="setting-item"
|
class="setting-item"
|
||||||
@@ -16,8 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SectionTitle from "./SectionTitle.vue";
|
import { useSettingsStore } from "@stores";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||||
import { useTheme } from "@/hooks/core/useTheme";
|
import { useTheme } from "@/hooks/core/useTheme";
|
||||||
|
|
||||||
Executable → Regular
+7
-12
@@ -5,7 +5,7 @@
|
|||||||
class="worktab-tags-shell box-border w-full select-none"
|
class="worktab-tags-shell box-border w-full select-none"
|
||||||
:class="[
|
:class="[
|
||||||
/** 三种模式统一底边线;谷歌模式底边与标签贴合,默认/卡片需要与底边留出间距(避免重合) */
|
/** 三种模式统一底边线;谷歌模式底边与标签贴合,默认/卡片需要与底边留出间距(避免重合) */
|
||||||
'border-b border-[var(--fa-card-border)]',
|
'border-b border-(--fa-card-border)',
|
||||||
tabStyle === 'tab-google' ? 'worktab-tags-shell--google pt-1 pb-0' : 'py-1',
|
tabStyle === 'tab-google' ? 'worktab-tags-shell--google pt-1 pb-0' : 'py-1',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
ref="scrollRef"
|
ref="scrollRef"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
class="float-left whitespace-nowrap !bg-transparent flex"
|
class="float-left whitespace-nowrap bg-transparent! flex"
|
||||||
:class="[chromeTabStrip ? 'pl-1' : '']"
|
:class="[chromeTabStrip ? 'pl-1' : '']"
|
||||||
ref="tabsRef"
|
ref="tabsRef"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
item.path === activeTab
|
item.path === activeTab
|
||||||
? chromeTabStrip
|
? chromeTabStrip
|
||||||
? 'activ-tab'
|
? 'activ-tab'
|
||||||
: 'activ-tab !text-theme'
|
: 'activ-tab text-theme!'
|
||||||
: chromeTabStrip
|
: chromeTabStrip
|
||||||
? ''
|
? ''
|
||||||
: 'text-g-600 dark:text-g-800',
|
: 'text-g-600 dark:text-g-800',
|
||||||
isCardTabs ? 'worktab-tab--card' : '',
|
isCardTabs ? 'worktab-tab--card' : '',
|
||||||
chromeTabStrip ? 'google-tab relative !h-8 !leading-8 !border-none' : '',
|
chromeTabStrip ? 'google-tab relative h-8! leading-8! border-none!' : '',
|
||||||
]"
|
]"
|
||||||
:style="
|
:style="
|
||||||
tabStyle === 'tab-google'
|
tabStyle === 'tab-google'
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="worktab-star focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--el-color-primary)] focus-visible:ring-offset-1 rounded"
|
class="worktab-star focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--el-color-primary) focus-visible:ring-offset-1 rounded"
|
||||||
:class="{ 'worktab-star--on': isQuickLinkBookmarked(item) }"
|
:class="{ 'worktab-star--on': isQuickLinkBookmarked(item) }"
|
||||||
:title="
|
:title="
|
||||||
isQuickLinkBookmarked(item) ? t('worktab.bookmarkRemove') : t('worktab.bookmarkAdd')
|
isQuickLinkBookmarked(item) ? t('worktab.bookmarkRemove') : t('worktab.bookmarkAdd')
|
||||||
@@ -171,16 +171,11 @@ import { computed, onMounted, ref, watch, nextTick, onUnmounted } from "vue";
|
|||||||
import { LocationQueryRaw, useRoute, useRouter } from "vue-router";
|
import { LocationQueryRaw, useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { refreshAppCaches } from "@stores";
|
import { refreshAppCaches, useWorktabStore, useUserStore, useSettingsStore } from "@stores";
|
||||||
import { useWorktabStore } from "@stores/modules/worktab.store";
|
|
||||||
import { useUserStore } from "@stores/modules/user.store";
|
|
||||||
import { formatMenuTitle } from "@utils/navigation";
|
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
import { MenuItemType } from "@/components/others/fa-menu-right/index.vue";
|
import { MenuItemType } from "@/components/others/fa-menu-right/index.vue";
|
||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { quickStartManager } from "@utils/common";
|
import { formatMenuTitle, quickStartManager } from "@utils";
|
||||||
import { WorkTab } from "@/types";
|
import { WorkTab } from "@/types";
|
||||||
|
|
||||||
defineOptions({ name: "FaWorkTab" });
|
defineOptions({ name: "FaWorkTab" });
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<!-- 全局浮层层(引导、通知等跨页面组件) -->
|
<!-- 全局浮层层(引导、通知等跨页面组件) -->
|
||||||
<div id="app-global">
|
<div id="app-global">
|
||||||
<FaGlobalComponent />
|
<FaGlobalComponent />
|
||||||
<Guide v-if="guideVisible" v-model="guideVisible" @skip="onGuideFinished" />
|
<FaGuide v-if="guideVisible" v-model="guideVisible" @skip="onGuideFinished" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,9 +44,7 @@
|
|||||||
* → settingStore.showGuide=false → 后续不再显示
|
* → settingStore.showGuide=false → 后续不再显示
|
||||||
*/
|
*/
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import Guide from "@/components/others/fa-guide/index.vue";
|
import { useAppStore, useSettingsStore } from "@stores";
|
||||||
import { useAppStore } from "@stores/modules/app.store";
|
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
|
||||||
|
|
||||||
defineOptions({ name: "AppLayout" });
|
defineOptions({ name: "AppLayout" });
|
||||||
|
|
||||||
@@ -66,5 +64,5 @@ function onGuideFinished(): void {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "./style";
|
@use "@styles/layouts";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -42,6 +42,16 @@
|
|||||||
<template v-if="$slots.footer" #footer>
|
<template v-if="$slots.footer" #footer>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="formMode" #footer>
|
||||||
|
<div class="fa-dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
|
||||||
|
<ElButton v-if="formMode !== 'detail'" @click="emit('cancel')">
|
||||||
|
{{ cancelText }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="primary" :loading="confirmLoading" @click="emit('confirm')">
|
||||||
|
{{ confirmText }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,9 +73,19 @@ const props = withDefaults(
|
|||||||
dialogClass?: string;
|
dialogClass?: string;
|
||||||
/** 遮罩层自定义 class */
|
/** 遮罩层自定义 class */
|
||||||
modalClass?: string;
|
modalClass?: string;
|
||||||
|
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||||
|
formMode?: "detail" | "create" | "update";
|
||||||
|
/** 确定按钮 loading 状态 */
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/** 确定按钮文本 */
|
||||||
|
confirmText?: string;
|
||||||
|
/** 取消按钮文本 */
|
||||||
|
cancelText?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
confirmText: "确定",
|
||||||
|
cancelText: "取消",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,6 +94,10 @@ const emit = defineEmits<{
|
|||||||
close: [];
|
close: [];
|
||||||
opened: [];
|
opened: [];
|
||||||
"fullscreen-change": [isFullscreen: boolean];
|
"fullscreen-change": [isFullscreen: boolean];
|
||||||
|
/** 点击取消按钮 */
|
||||||
|
cancel: [];
|
||||||
|
/** 点击确定按钮 */
|
||||||
|
confirm: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
@@ -112,6 +136,13 @@ const dialogAttrs = computed(() => {
|
|||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.core-overlay-dialog__actions {
|
.core-overlay-dialog__actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -26,6 +26,16 @@
|
|||||||
<template v-if="$slots.footer" #footer>
|
<template v-if="$slots.footer" #footer>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="formMode" #footer>
|
||||||
|
<div class="fa-drawer-footer" :style="'padding-right: var(--el-drawer-padding-primary)'">
|
||||||
|
<ElButton v-if="formMode !== 'detail'" @click="emit('cancel')">
|
||||||
|
{{ cancelText }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="primary" :loading="confirmLoading" @click="emit('confirm')">
|
||||||
|
{{ confirmText }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,9 +54,19 @@ const props = withDefaults(
|
|||||||
direction?: "rtl" | "ltr" | "ttb" | "btt";
|
direction?: "rtl" | "ltr" | "ttb" | "btt";
|
||||||
/** 透传到 el-drawer 的 class */
|
/** 透传到 el-drawer 的 class */
|
||||||
drawerClass?: string;
|
drawerClass?: string;
|
||||||
|
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||||
|
formMode?: "detail" | "create" | "update";
|
||||||
|
/** 确定按钮 loading 状态 */
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/** 确定按钮文本 */
|
||||||
|
confirmText?: string;
|
||||||
|
/** 取消按钮文本 */
|
||||||
|
cancelText?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
direction: "rtl",
|
direction: "rtl",
|
||||||
|
confirmText: "确定",
|
||||||
|
cancelText: "取消",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,6 +74,10 @@ const emit = defineEmits<{
|
|||||||
"update:modelValue": [v: boolean];
|
"update:modelValue": [v: boolean];
|
||||||
close: [];
|
close: [];
|
||||||
opened: [];
|
opened: [];
|
||||||
|
/** 点击取消按钮 */
|
||||||
|
cancel: [];
|
||||||
|
/** 点击确定按钮 */
|
||||||
|
confirm: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
@@ -92,6 +116,13 @@ const drawerAttrs = computed(() => {
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa-drawer-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.core-overlay-drawer__actions {
|
.core-overlay-drawer__actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -64,7 +64,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FaDialog from "@/components/modal/fa-dialog/index.vue";
|
|
||||||
import ExcelJS from "exceljs";
|
import ExcelJS from "exceljs";
|
||||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
|
|||||||
@@ -76,7 +76,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Download, UploadFilled } from "@element-plus/icons-vue";
|
import { Download, UploadFilled } from "@element-plus/icons-vue";
|
||||||
import FaDialog from "@/components/modal/fa-dialog/index.vue";
|
|
||||||
import { ElMessage, type UploadUserFile } from "element-plus";
|
import { ElMessage, type UploadUserFile } from "element-plus";
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||||
|
|||||||
@@ -121,8 +121,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "FaAiAssistant" });
|
defineOptions({ name: "FaAiAssistant" });
|
||||||
|
|
||||||
import FaSvgIcon from "@/components/base/fa-svg-icon/index.vue";
|
import { resolveIconForFaSvgIcon } from "@utils";
|
||||||
import { resolveIconForFaSvgIcon } from "@utils/menuIcon/remix";
|
|
||||||
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async function handleClipboard() {
|
|||||||
ElMessage.success(t("common.copySuccess"));
|
ElMessage.success(t("common.copySuccess"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.warning(t("common.copyFailed"));
|
ElMessage.warning(t("common.copyFailed"));
|
||||||
console.warn("[CopyButton] Copy failed:", error);
|
console.warn("[FaCopyButton] Copy failed:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 兼容性处理(useClipboard 有兼容性问题)
|
// 兼容性处理(useClipboard 有兼容性问题)
|
||||||
@@ -55,7 +55,7 @@ async function handleClipboard() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.warning(t("common.copyFailed"));
|
ElMessage.warning(t("common.copyFailed"));
|
||||||
console.warn("[CopyButton] Copy failed:", err);
|
console.warn("[FaCopyButton] Copy failed:", err);
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(input);
|
document.body.removeChild(input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<ElScrollbar v-if="scrollbar" :max-height="maxHeight" :view-style="{ overflowX: 'hidden' }">
|
||||||
|
<ElDescriptions v-bind="bindings" :class="ns.b()">
|
||||||
|
<template v-if="$slots.title" #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!$slots.default">
|
||||||
|
<ElDescriptionsItem
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.prop"
|
||||||
|
:label="item.label"
|
||||||
|
:span="item.span || span"
|
||||||
|
:label-class-name="item.labelClassName"
|
||||||
|
:class-name="item.className"
|
||||||
|
>
|
||||||
|
<!-- 带 slot 名称的项:通过具名插槽自定义 -->
|
||||||
|
<slot
|
||||||
|
v-if="item.slot"
|
||||||
|
:name="item.slot"
|
||||||
|
:item="item"
|
||||||
|
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||||
|
:row="data"
|
||||||
|
/>
|
||||||
|
<!-- Tag 模式 -->
|
||||||
|
<template v-else-if="item.tag">
|
||||||
|
<ElTag
|
||||||
|
v-if="item.tag === true && data && data[item.prop] !== undefined"
|
||||||
|
:type="resolveTagType(getNestedValue(data, item.prop))"
|
||||||
|
>
|
||||||
|
{{ getNestedValue(data, item.prop) }}
|
||||||
|
</ElTag>
|
||||||
|
<ElTag
|
||||||
|
v-else-if="typeof item.tag === 'object' && data"
|
||||||
|
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
|
||||||
|
>
|
||||||
|
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
<!-- 纯文本模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<slot
|
||||||
|
:name="item.prop"
|
||||||
|
:item="item"
|
||||||
|
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||||
|
:row="data"
|
||||||
|
>
|
||||||
|
{{ data ? getNestedValue(data, item.prop) : "" }}
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</template>
|
||||||
|
<!-- 完全自定义模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</ElDescriptions>
|
||||||
|
</ElScrollbar>
|
||||||
|
<!-- 不使用滚动条时直接渲染 -->
|
||||||
|
<ElDescriptions v-else v-bind="bindings" :class="ns.b()">
|
||||||
|
<template v-if="$slots.title" #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!$slots.default">
|
||||||
|
<ElDescriptionsItem
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.prop"
|
||||||
|
:label="item.label"
|
||||||
|
:span="item.span || span"
|
||||||
|
:label-class-name="item.labelClassName"
|
||||||
|
:class-name="item.className"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
v-if="item.slot"
|
||||||
|
:name="item.slot"
|
||||||
|
:item="item"
|
||||||
|
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||||
|
:row="data"
|
||||||
|
/>
|
||||||
|
<template v-else-if="item.tag">
|
||||||
|
<ElTag
|
||||||
|
v-if="item.tag === true && data && data[item.prop] !== undefined"
|
||||||
|
:type="resolveTagType(getNestedValue(data, item.prop))"
|
||||||
|
>
|
||||||
|
{{ getNestedValue(data, item.prop) }}
|
||||||
|
</ElTag>
|
||||||
|
<ElTag
|
||||||
|
v-else-if="typeof item.tag === 'object' && data"
|
||||||
|
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
|
||||||
|
>
|
||||||
|
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot
|
||||||
|
:name="item.prop"
|
||||||
|
:item="item"
|
||||||
|
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||||
|
:row="data"
|
||||||
|
>
|
||||||
|
{{ data ? getNestedValue(data, item.prop) : "" }}
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</ElDescriptions>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaDescriptions" });
|
||||||
|
|
||||||
|
import { computed, useAttrs } from "vue";
|
||||||
|
import { useNamespace } from "element-plus";
|
||||||
|
// ── 组件类型定义(内联,避免跨文件引用导致 TS Server 延迟)──
|
||||||
|
/** ElTag 可接受的 type 值 */
|
||||||
|
export type TagType = "primary" | "success" | "warning" | "danger" | "info";
|
||||||
|
|
||||||
|
/** Tag 渲染配置 */
|
||||||
|
export interface TagConfig {
|
||||||
|
/**
|
||||||
|
* Tag 映射表:key 为数据值,value 为 { type: ElTag类型, text: 显示文本 }
|
||||||
|
* 例:{ '0': { type: 'success', text: '启用' }, '1': { type: 'danger', text: '停用' } }
|
||||||
|
*/
|
||||||
|
map?: Record<string, { type?: TagType; text?: string }>;
|
||||||
|
/** 默认 type,当 map 未匹配到时使用 */
|
||||||
|
type?: TagType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个描述项配置 */
|
||||||
|
export interface DescriptionsItem {
|
||||||
|
/** 标签文本 */
|
||||||
|
label: string;
|
||||||
|
/** 对应 data 中的属性名,支持 'a.b.c' 嵌套路径 */
|
||||||
|
prop: string;
|
||||||
|
/** 列跨度,不传则使用组件级 span 默认值 */
|
||||||
|
span?: number;
|
||||||
|
/**
|
||||||
|
* 渲染模式:
|
||||||
|
* - 不设置:纯文本
|
||||||
|
* - true:作为 ElTag 渲染,文本为 data[prop]
|
||||||
|
* - TagConfig 对象:通过 map 映射类型和文本
|
||||||
|
*/
|
||||||
|
tag?: boolean | TagConfig;
|
||||||
|
/** 自定义插槽名称,设置后该 item 将通过具名插槽渲染 */
|
||||||
|
slot?: string;
|
||||||
|
/** 传递给 el-descriptions-item 的 label-class-name */
|
||||||
|
labelClassName?: string;
|
||||||
|
/** 传递给 el-descriptions-item 的 class-name */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const ns = useNamespace("descriptions");
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 每行显示的描述项数量,默认 4 */
|
||||||
|
column?: number;
|
||||||
|
/** 是否展示边框,默认 true */
|
||||||
|
border?: boolean;
|
||||||
|
/** 列表尺寸,默认 'default' */
|
||||||
|
size?: "default" | "small";
|
||||||
|
/** 标签宽度 */
|
||||||
|
labelWidth?: string;
|
||||||
|
/** 描述项配置列表 */
|
||||||
|
items?: DescriptionsItem[];
|
||||||
|
/** 数据对象,items 模式下自动取 data[item.prop] 渲染 */
|
||||||
|
data?: Record<string, unknown> | null;
|
||||||
|
/** 默认 span,当 item 未指定 span 时生效,默认 2 */
|
||||||
|
span?: number;
|
||||||
|
/** 是否需要内置 ElScrollbar 包裹,默认 true */
|
||||||
|
scrollbar?: boolean;
|
||||||
|
/** ElScrollbar 最大高度,默认 '70vh' */
|
||||||
|
maxHeight?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
column: 4,
|
||||||
|
border: true,
|
||||||
|
size: "default",
|
||||||
|
labelWidth: undefined,
|
||||||
|
items: () => [],
|
||||||
|
data: null,
|
||||||
|
span: 2,
|
||||||
|
scrollbar: true,
|
||||||
|
maxHeight: "70vh",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 传递给 ElDescriptions 的绑定属性
|
||||||
|
const bindings = computed(() => {
|
||||||
|
const bind: Record<string, unknown> = {
|
||||||
|
column: props.column,
|
||||||
|
border: props.border,
|
||||||
|
...attrs, // 透传 class、style 等属性
|
||||||
|
};
|
||||||
|
if (props.size !== "default") bind.size = props.size;
|
||||||
|
if (props.labelWidth !== undefined) bind.labelWidth = props.labelWidth;
|
||||||
|
return bind;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取嵌套属性值,支持 'a.b.c' 路径 */
|
||||||
|
function getNestedValue(obj: Record<string, unknown> | null, path: string): unknown {
|
||||||
|
if (!obj) return undefined;
|
||||||
|
return path.split(".").reduce((acc, key) => {
|
||||||
|
if (acc && typeof acc === "object" && key in acc) {
|
||||||
|
return (acc as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, obj as unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 ElTag 的 type 属性,确保返回 ElTag 可接受的有效类型值 */
|
||||||
|
function resolveTagType(_value: unknown, fallback?: string): TagType {
|
||||||
|
// 优先级 1: fallback 本身就是合法 TagType → 直接使用
|
||||||
|
if (fallback && isValidTagType(fallback)) return fallback;
|
||||||
|
// 优先级 2: 无有效 fallback 时,用 'info' 作为兜底
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_TYPES: Set<string> = new Set(["primary", "success", "warning", "danger", "info"]);
|
||||||
|
function isValidTagType(v: string): v is TagType {
|
||||||
|
return TAG_TYPES.has(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 ElTag 显示的文本,支持 tagMap 映射 */
|
||||||
|
function resolveTagText(value: unknown, tag: TagConfig): string {
|
||||||
|
const raw = value == null ? "" : String(value);
|
||||||
|
if (tag.map && raw in tag.map) {
|
||||||
|
return tag.map[raw].text ?? raw;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/*
|
||||||
|
* fa-descriptions 在 ElScrollbar 内部包裹时,
|
||||||
|
* 需要占满宽度,避免 ElDescriptions 边框被截断。
|
||||||
|
*/
|
||||||
|
:deep(.fa-descriptions) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -82,10 +82,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "FaIconSelect" });
|
defineOptions({ name: "FaIconSelect" });
|
||||||
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
|
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
|
||||||
import FaSvgIcon from "@/components/base/fa-svg-icon/index.vue";
|
import {
|
||||||
import { listLocalIconBasenames } from "@utils/icons";
|
listLocalIconBasenames,
|
||||||
import { isIconifyStoredIcon, resolveElementPlusIconComponent } from "@utils/menuIcon";
|
isIconifyStoredIcon,
|
||||||
import { resolveIconForFaSvgIcon } from "@utils/menuIcon/remix";
|
resolveElementPlusIconComponent,
|
||||||
|
resolveIconForFaSvgIcon,
|
||||||
|
} from "@utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div
|
<div
|
||||||
v-show="visible"
|
v-show="visible"
|
||||||
:style="menuStyle"
|
:style="menuStyle"
|
||||||
class="context-menu fa-card-xs !shadow-xl min-w-[var(--menu-width)] w-[var(--menu-width)]"
|
class="context-menu fa-card-xs shadow-xl! min-w-(--menu-width) w-(--menu-width)"
|
||||||
>
|
>
|
||||||
<ul class="menu-list m-0 list-none" :style="menuListStyle">
|
<ul class="menu-list m-0 list-none" :style="menuListStyle">
|
||||||
<template v-for="item in menuItems" :key="item.key">
|
<template v-for="item in menuItems" :key="item.key">
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
class="submenu-list fa-card-xs absolute left-full top-0 z-[2001] hidden w-max min-w-max list-none !shadow-xl"
|
class="submenu-list fa-card-xs absolute left-full top-0 z-2001 hidden w-max min-w-max list-none shadow-xl!"
|
||||||
:style="submenuListStyle"
|
:style="submenuListStyle"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
@@ -86,7 +86,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import type { CSSProperties } from "vue";
|
import type { CSSProperties } from "vue";
|
||||||
import FaSvgIcon from "@/components/base/fa-svg-icon/index.vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "FaMenuRight" });
|
defineOptions({ name: "FaMenuRight" });
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import FaSvgIcon from "@/components/base/fa-svg-icon/index.vue";
|
|
||||||
import {
|
import {
|
||||||
elementMenuIconToEpIconify,
|
elementMenuIconToEpIconify,
|
||||||
isElementPlusStoredIcon,
|
isElementPlusStoredIcon,
|
||||||
resolveElementPlusIconComponent,
|
resolveElementPlusIconComponent,
|
||||||
} from "@utils/menuIcon";
|
resolveIconForFaSvgIcon,
|
||||||
import { resolveIconForFaSvgIcon } from "@utils/menuIcon/remix";
|
} from "@utils";
|
||||||
|
|
||||||
defineOptions({ name: "FaMenuRouteIcon", inheritAttrs: false });
|
defineOptions({ name: "FaMenuRouteIcon", inheritAttrs: false });
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "FaSizeSelect" });
|
defineOptions({ name: "FaSizeSelect" });
|
||||||
|
|
||||||
import FaIconButton from "@/components/widget/fa-icon-button/index.vue";
|
|
||||||
import { ComponentSize } from "@/enums/settings/layout.enum";
|
import { ComponentSize } from "@/enums/settings/layout.enum";
|
||||||
import { useAppStore } from "@stores/modules/app.store";
|
import { useAppStore } from "@stores";
|
||||||
import { resolveIconForFaSvgIcon } from "@utils/menuIcon/remix";
|
import { resolveIconForFaSvgIcon } from "@utils";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sizeOptions = computed(() => {
|
const sizeOptions = computed(() => {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ defineSlots<{
|
|||||||
import { ref, reactive, computed } from "vue";
|
import { ref, reactive, computed } from "vue";
|
||||||
import { useResizeObserver } from "@vueuse/core";
|
import { useResizeObserver } from "@vueuse/core";
|
||||||
import type { FormInstance, PopoverProps, TableInstance } from "element-plus";
|
import type { FormInstance, PopoverProps, TableInstance } from "element-plus";
|
||||||
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
// 对象类型
|
// 对象类型
|
||||||
export type IObject = Record<string, any>;
|
export type IObject = Record<string, any>;
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ defineOptions({ name: "FaUpload" });
|
|||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { UploadRawFile, UploadRequestOptions, ElMessage, type UploadUserFile } from "element-plus";
|
import { UploadRawFile, UploadRequestOptions, ElMessage, type UploadUserFile } from "element-plus";
|
||||||
import ParamsAPI from "@/api/module_system/params";
|
import ParamsAPI from "@/api/module_system/params";
|
||||||
import FaCutterImg from "@/components/media/fa-cutter-img/index.vue";
|
import { dataURLToFile } from "@utils";
|
||||||
import { dataURLToFile } from "@utils/file/dataUrl";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import { storeToRefs } from "pinia";
|
|||||||
import AppConfig from "@/config";
|
import AppConfig from "@/config";
|
||||||
import { defaultSettings } from "@/config/setting";
|
import { defaultSettings } from "@/config/setting";
|
||||||
import { ThemeMode } from "@/enums";
|
import { ThemeMode } from "@/enums";
|
||||||
import { hexToRgba } from "@utils/ui";
|
import { hexToRgba } from "@utils";
|
||||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
import { useSettingsStore } from "@stores";
|
||||||
|
|
||||||
defineOptions({ name: "FaWatermark" });
|
defineOptions({ name: "FaWatermark" });
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- 表格头部,包含表格大小、刷新、全屏、列设置、其他设置 -->
|
<!-- 表格头部,包含表格大小、刷新、全屏、列设置、其他设置 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-cb max-md:!block" id="fa-table-header">
|
<div class="flex-cb max-md:block!" id="fa-table-header">
|
||||||
<div class="flex-wrap">
|
<div class="flex-wrap">
|
||||||
<slot name="left"></slot>
|
<slot name="left"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-c md:justify-end max-md:mt-3 max-sm:!hidden">
|
<div class="flex-c md:justify-end max-md:mt-3 max-sm:hidden!">
|
||||||
<!-- 搜索区域显示/隐藏:默认展示搜索(未高亮);点按收起后高亮表示当前为隐藏状态 -->
|
<!-- 搜索区域显示/隐藏:默认展示搜索(未高亮);点按收起后高亮表示当前为隐藏状态 -->
|
||||||
<ElTooltip
|
<ElTooltip
|
||||||
v-if="showSearchBar != null"
|
v-if="showSearchBar != null"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<div
|
<div
|
||||||
class="button"
|
class="button"
|
||||||
@click="search"
|
@click="search"
|
||||||
:class="!showSearchBar ? 'active !bg-theme hover:!bg-theme/80' : ''"
|
:class="!showSearchBar ? 'active bg-theme! hover:bg-theme/80!' : ''"
|
||||||
>
|
>
|
||||||
<FaSvgIcon icon="ri:search-line" :class="!showSearchBar ? 'text-white' : 'text-g-700'" />
|
<FaSvgIcon icon="ri:search-line" :class="!showSearchBar ? 'text-white' : 'text-g-700'" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,12 +44,12 @@
|
|||||||
<div
|
<div
|
||||||
v-for="item in tableSizeOptions"
|
v-for="item in tableSizeOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
class="table-size-btn-item [&_.el-dropdown-menu__item]:!mb-[3px] last:[&_.el-dropdown-menu__item]:!mb-0"
|
class="table-size-btn-item [&_.el-dropdown-menu__item]:mb-[3px]! last:[&_.el-dropdown-menu__item]:mb-0!"
|
||||||
>
|
>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:command="item.value"
|
:command="item.value"
|
||||||
:class="tableSize === item.value ? '!bg-g-300/55' : ''"
|
:class="tableSize === item.value ? 'bg-g-300/55!' : ''"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<div
|
<div
|
||||||
class="button"
|
class="button"
|
||||||
@click="toggleRowDrag"
|
@click="toggleRowDrag"
|
||||||
:class="isRowDrag ? 'active !bg-theme hover:!bg-theme/80' : ''"
|
:class="isRowDrag ? 'active bg-theme! hover:bg-theme/80!' : ''"
|
||||||
>
|
>
|
||||||
<FaSvgIcon icon="ri:drag-move-line" :class="isRowDrag ? 'text-white' : 'text-g-700'" />
|
<FaSvgIcon icon="ri:drag-move-line" :class="isRowDrag ? 'text-white' : 'text-g-700'" />
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
import { computed, ref, onMounted, onUnmounted } from "vue";
|
import { computed, ref, onMounted, onUnmounted } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { TableSizeEnum } from "@/enums/formEnum";
|
import { TableSizeEnum } from "@/enums/formEnum";
|
||||||
import { useTableStore } from "@stores/modules/table.store";
|
import { useTableStore } from "@stores";
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
|
|||||||
@@ -27,23 +27,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn v-else v-bind="cleanBodyColumnProps(col)">
|
<ElTableColumn v-else v-bind="cleanBodyColumnProps(col)">
|
||||||
<template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
|
<template #header="headerScope">
|
||||||
<slot
|
<component
|
||||||
:name="col.headerSlotName || `${col.prop}-header`"
|
v-if="col.useHeaderSlot && col.prop"
|
||||||
v-bind="{ ...headerScope, prop: col.prop, label: col.label }"
|
:is="() => renderColumnHeader(headerScope, col)"
|
||||||
>
|
/>
|
||||||
{{ col.label }}
|
|
||||||
</slot>
|
|
||||||
</template>
|
</template>
|
||||||
<template #default="slotScope">
|
<template #default="slotScope">
|
||||||
<slot
|
<component
|
||||||
v-if="col.useSlot && col.prop && shouldRenderSlotScope(slotScope)"
|
v-if="col.useSlot && col.prop && shouldRenderSlotScope(slotScope)"
|
||||||
:name="col.slotName || col.prop"
|
:is="() => renderCellSlot(slotScope, col)"
|
||||||
v-bind="{
|
|
||||||
...slotScope,
|
|
||||||
prop: col.prop,
|
|
||||||
value: col.prop ? slotScope.row[col.prop] : undefined,
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
<TableFormatterOutlet
|
<TableFormatterOutlet
|
||||||
v-else-if="col.formatter && !col.useSlot && shouldRenderSlotScope(slotScope)"
|
v-else-if="col.formatter && !col.useSlot && shouldRenderSlotScope(slotScope)"
|
||||||
@@ -53,7 +46,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="$slots.default" #default><slot /></template>
|
<template v-if="$slots.default" #default>
|
||||||
|
<component :is="() => renderDefaultSlot()" />
|
||||||
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div v-if="loading"></div>
|
<div v-if="loading"></div>
|
||||||
<ElEmpty v-else :description="emptyText" :image-size="120" />
|
<ElEmpty v-else :description="emptyText" :image-size="120" />
|
||||||
@@ -94,6 +89,7 @@ import {
|
|||||||
watchEffect,
|
watchEffect,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
useAttrs,
|
useAttrs,
|
||||||
|
useSlots,
|
||||||
isVNode,
|
isVNode,
|
||||||
h,
|
h,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
@@ -102,11 +98,10 @@ import {
|
|||||||
import type { ElTable, TableProps } from "element-plus";
|
import type { ElTable, TableProps } from "element-plus";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { ColumnOption } from "@/types";
|
import { ColumnOption } from "@/types";
|
||||||
import { useTableStore } from "@stores/modules/table.store";
|
import { useTableStore } from "@stores";
|
||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
||||||
import { useWindowSize } from "@vueuse/core";
|
import { useWindowSize } from "@vueuse/core";
|
||||||
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
|
|
||||||
defineOptions({ name: "FaTable" });
|
defineOptions({ name: "FaTable" });
|
||||||
@@ -116,6 +111,7 @@ const elTableRef = ref<InstanceType<typeof ElTable> | null>(null);
|
|||||||
const paginationRef = ref<HTMLElement>();
|
const paginationRef = ref<HTMLElement>();
|
||||||
const tableHeaderRef = ref<HTMLElement>();
|
const tableHeaderRef = ref<HTMLElement>();
|
||||||
const tableStore = useTableStore();
|
const tableStore = useTableStore();
|
||||||
|
const slots = useSlots();
|
||||||
const {
|
const {
|
||||||
isBorder,
|
isBorder,
|
||||||
isZebra,
|
isZebra,
|
||||||
@@ -361,6 +357,28 @@ const shouldRenderSlotScope = (slotScope: { $index?: number }) => {
|
|||||||
return slotScope.$index === undefined || slotScope.$index >= 0;
|
return slotScope.$index === undefined || slotScope.$index >= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Vue 3.5:useSlots() 直接调用 slot 渲染函数,模板中零 <slot> 元素 */
|
||||||
|
function renderColumnHeader(headerScope: Record<string, unknown>, col: Record<string, unknown>) {
|
||||||
|
const slotName = (col.headerSlotName || `${col.prop}-header`) as string;
|
||||||
|
return slots[slotName]?.({ ...headerScope, prop: col.prop, label: col.label }) ?? col.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCellSlot(slotScope: Record<string, unknown>, col: Record<string, unknown>) {
|
||||||
|
const slotName = (col.slotName || col.prop) as string;
|
||||||
|
const row = slotScope.row as Record<string, unknown> | undefined;
|
||||||
|
return (
|
||||||
|
slots[slotName]?.({
|
||||||
|
...slotScope,
|
||||||
|
prop: col.prop,
|
||||||
|
value: col.prop ? row?.[col.prop as string] : undefined,
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDefaultSlot() {
|
||||||
|
return slots.default?.() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ElTableColumn 若存在 default 插槽且插槽产物含任意非 Comment 的 vnode(含空白文本节点),
|
* ElTableColumn 若存在 default 插槽且插槽产物含任意非 Comment 的 vnode(含空白文本节点),
|
||||||
* 将不会执行 formatter(见 element-plus render-helper setColumnRenders)。
|
* 将不会执行 formatter(见 element-plus render-helper setColumnRenders)。
|
||||||
@@ -482,5 +500,5 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "./style";
|
@use "@styles/fa-table";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user