Merge pull request #409 from fastapiadmin/dev

Dev
This commit is contained in:
fastapiadmin
2026-05-17 06:58:52 +08:00
committed by GitHub
223 changed files with 4120 additions and 4430 deletions
+10 -11
View File
@@ -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
+18 -18
View File
@@ -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:
+32 -42
View File
@@ -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
View File
@@ -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** 保持一致。
+9
View File
@@ -0,0 +1,9 @@
export const BANNER = [
" ___ _ _ __ _ ",
" .' ..] / |_ (_) | ] (_) ",
" _| |_ ,--. .--. `| |-',--. _ .--. __ ,--. .--.| | _ .--..--. __ _ .--. ",
" '-| |-'`'_\\ : ( (`\\] | | `'_\\ :[ '/ '`\\ [ | `'_\\ : / /'`\\' | [ `.-. .-. | [ | [ `.-. | ",
" | | // | |, `'.'. | |,// | |,| \\__/ | | | // | |,| \\__/ | | | | | | | | | | | | | ",
" [___] \\'-;__/[\\__) )\\__/\\'-;__/| ;.__/ [___]\\'-;__/ '.__.;__][___||__||__][___][___||__] ",
" [__] ",
].join("\n");
+47
View File
@@ -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
View File
@@ -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,
]; ];
+5 -7
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/auth"; const API_PATH = "/system/auth";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/dept"; const API_PATH = "/system/dept";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/dict"; const API_PATH = "/system/dict";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/log"; const API_PATH = "/system/log";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/menu"; const API_PATH = "/system/menu";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/notice"; const API_PATH = "/system/notice";
+1 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/role"; const API_PATH = "/system/role";
+1 -1
View File
@@ -1,4 +1,4 @@
import request from "@utils/http"; import { request } from "@utils";
const API_PATH = "/system/tenant"; const API_PATH = "/system/tenant";
+1 -1
View File
@@ -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" });
+12 -17
View File
@@ -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" });
@@ -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" });
@@ -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 {
// 展开的宽度 // 展开的宽度
@@ -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,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";
@@ -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,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>
@@ -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";
@@ -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";
@@ -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";
@@ -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";
@@ -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";
@@ -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,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 }}
@@ -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" });
@@ -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>
@@ -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";
+7 -12
View File
@@ -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