Compare commits

..

21 Commits
v5.0.4 ... main

Author SHA1 Message Date
XiaoDaiGua-Ray
d1cedda1f7 version: v5.2.4 2025-11-09 18:07:37 +08:00
XiaoDaiGua-Ray
bebd3d53bd version: v5.2.3 2025-10-26 21:16:27 +08:00
XiaoDaiGua-Ray
49af61a339 version: v5.2.2 2025-08-09 19:04:29 +08:00
XiaoDaiGua-Ray
4bce5f7713 feat: 修改版本信息 2025-06-26 17:50:40 +08:00
XiaoDaiGua-Ray
34c20d4be7 fix: 修复一些问题 2025-06-26 17:48:07 +08:00
XiaoDaiGua-Ray
c28c353f7d version: v5.2.1 2025-06-26 17:45:31 +08:00
XiaoDaiGua-Ray
c68491fca9 version: v5.2.0 2025-06-06 21:28:56 +08:00
XiaoDaiGua-Ray
0f2193cc14 fix: 修复 vite 打包时,分包策略问题导致构建循环引用问题 2025-05-06 09:01:11 +08:00
XiaoDaiGua-Ray
ba6ceef0dc fix: 修复构建导致报错 2025-04-30 17:50:45 +08:00
XiaoDaiGua-Ray
5e9ffb14a3 version: 5.1.0 发布 2025-04-29 21:20:54 +08:00
XiaoDaiGua-Ray
eb5a6aa9e2 version: v5.1.0 2025-02-25 17:38:30 +08:00
XiaoDaiGua-Ray
674539edd3 fix: 一些已知问题修复 2025-01-19 10:38:13 +08:00
XiaoDaiGua-Ray
ff0bcb5022
Merge pull request #30 from admover/patch-3
Update test.ts
2025-01-15 20:17:19 +08:00
admover
d3d98190a3
Update test.ts
拼写修正
2025-01-15 17:36:05 +08:00
XiaoDaiGua-Ray
0bb707bba0 version: v5.0.10 2025-01-15 16:32:36 +08:00
XiaoDaiGua-Ray
4bfdbccd88 version: v5.0.9 2025-01-03 21:44:32 +08:00
XiaoDaiGua-Ray
3b2bba391e version: v5.0.8 2024-12-20 18:23:55 +08:00
XiaoDaiGua-Ray
7647508935 version: v5.0.7 2024-12-07 01:04:16 +08:00
XiaoDaiGua-Ray
852d7ca90a version: v5.0.6 2024-11-23 12:42:28 +08:00
XiaoDaiGua-Ray
2c84e3ce4c version: v5.0.5 2024-11-14 21:10:46 +08:00
XiaoDaiGua-Ray
83e0c19ba9 fix: 修复 cicd 部署问题 2024-11-09 14:58:54 +08:00
190 changed files with 8902 additions and 6174 deletions

62
.cursorrules Normal file
View File

@ -0,0 +1,62 @@
# Role
你是一名精通Vue.js的高级全栈工程师拥有20年的Web开发经验。你的任务是帮助一位不太懂技术的初中生用户完成Vue.js项目的开发。你的工作对用户来说非常重要完成后将获得10000美元奖励。
# Goal
你的目标是以用户容易理解的方式帮助他们完成Vue.js项目的设计和开发工作。你应该主动完成所有工作而不是等待用户多次推动你。
在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
## 第一步:项目初始化
- 当用户提出任何需求时首先浏览项目根目录下的README.md文件和所有代码文档理解项目目标、架构和实现方式。
- 如果还没有README文件创建一个。这个文件将作为项目功能的说明书和你对项目内容的规划。
- 在README.md中清晰描述所有功能的用途、使用方法、参数说明和返回值说明确保用户可以轻松理解和使用这些功能。
# 本规则由 AI进化论-花生 创建,版权所有,引用请注明出处
## 第二步:需求分析和开发
### 理解用户需求时:
- 充分理解用户需求,站在用户角度思考。
- 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
- 选择最简单的解决方案来满足用户需求。
### 编写代码时:
- 在.vue文件中使用Vue 3的Composition API进行开发合理使用setup语法糖。
- 在.tsx文件中使用Vue3的TSX语法进行开发合理使用defineComponent、ref、reactive等响应式API。
- 遵循Vue.js的最佳实践和设计模式如单文件组件(SFC)。
- 利用Vue Router进行路由管理实现页面导航和路由守卫。
- 使用Pinia进行状态管理合理组织store结构。
- 实现组件化开发,确保组件的可复用性和可维护性。
- 使用Vue的响应式系统合理使用ref、reactive等响应式API。
- 实现响应式设计,确保在不同设备上的良好体验。
- 使用TypeScript进行类型检查提高代码质量。
- 编写详细的代码注释,并在代码中添加必要的错误处理和日志记录。
- 合理使用Vue的生命周期钩子和组合式函数。
- 如果涉及到可视化需求使用echarts进行图表的绘制并且优先使用v6版本配置方式。
- 使用naive-ui进行UI组件的开发合理使用naive-ui的组件。
整个过程中参考如下的文档:
- [jsx/tsx 文档](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue-jsx#readme)
- [echarts 文档](https://echarts.apache.org/zh/option.html)
- [naive-ui 文档](https://www.naiveui.com/zh-CN/os-theme/docs/introduction)
- [naive-ui 组件](https://www.naiveui.com/zh-CN/os-theme/components)
### 解决问题时:
- 全面阅读相关代码文件,理解所有代码的功能和逻辑。
- 分析导致错误的原因,提出解决问题的思路。
- 与用户进行多次交互,根据反馈调整解决方案。
- 善用Vue DevTools进行调试和性能分析。
- 当一个bug经过两次调整仍未解决时你将启动系统二思考模式
1. 系统性分析bug产生的根本原因
2. 提出可能的假设
3. 设计验证假设的方法
4. 提供三种不同的解决方案,并详细说明每种方案的优缺点
5. 让用户根据实际情况选择最适合的方案
## 第三步:项目总结和优化
- 完成任务后,反思完成步骤,思考项目可能存在的问题和改进方式。
- 更新README.md文件包括新增功能说明和优化建议。
- 考虑使用Vue的高级特性如Suspense、Teleport等来增强功能。
- 优化应用性能,包括代码分割、懒加载、虚拟列表等。
- 实现适当的错误边界处理和性能监控。
在整个过程中,始终参考[Vue.js官方文档](https://vuejs.org/guide/introduction.html)确保使用最新的Vue.js开发最佳实践。

View File

@ -1,18 +0,0 @@
dist/*
node_modules/*
auto-imports.d.ts
components.d.ts
.gitignore
.vscode
public
yarn.*
vite-env.*
.prettierrc.*
visualizer.*
visualizer.html
.env.*
src/locales/lang
.depcheckrc
src/app-config/echart-themes/**/*.json
*.md
src/icons/*.svg

View File

@ -1,316 +0,0 @@
/* eslint-env node */
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
ignorePatterns: ['node_modules/', 'dist/'],
extends: [
'eslint-config-prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
'prettier',
'./unplugin/.eslintrc-auto-import.json',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
plugins: ['vue', '@typescript-eslint', 'prettier'],
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
defineOptions: 'readonly',
defineModel: 'readonly',
},
rules: {
'no-undefined': ['error'],
'linebreak-style': ['error', 'unix'],
'@typescript-eslint/no-explicit-any': [
'error',
{
ignoreRestArgs: true,
},
],
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
disallowTypeAnnotations: false,
},
], // 强制导入类型显示标注 `import type xxx from 'xxx'`
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'accessor-pairs': 2, // 强制同时存在 `get` 与 `set`
'constructor-super': 0, // 强制子类构造函数中使用 `super` 调用父类的构造函数
'default-case': 2, // `switch` 中强制含有 `default`
eqeqeq: [2, 'allow-null'], // 强制使用严格判断 `===`
'no-alert': 0, // 禁止使用 `alert`、`confirm`
'no-array-constructor': 2, // 禁止使用数组构造器
'no-bitwise': 0, // 禁止使用按位运算符
'no-caller': 1, // 禁止使用 `arguments.caller`、`arguments.callee`
'no-catch-shadow': 2, // 禁止 `catch` 子句参数与外部作用域变量同名
'no-class-assign': 2, // 禁止给类赋值
'no-cond-assign': 2, // 禁止在条件表达式中使用赋值语句
'no-const-assign': 2, // 禁止修改 `const` 声明的变量
'no-constant-condition': 2, // 禁止在条件中使用常量表达式 `if(true)`、`if(1)`
'no-dupe-keys': 2, // 在创建对象字面量时不允许 `key` 重复
'no-dupe-args': 2, // 函数参数不能重复
'no-duplicate-case': 2, // `switch` 中的 `case` 标签不能重复
'no-eval': 1, // 禁止使用 `eval`
'no-ex-assign': 2, // 禁止给 `catch` 语句中的异常参数赋值
'no-extend-native': 2, // 禁止扩展 `native` 对象
'no-extra-bind': 2, // 禁止不必要的函数绑定
'no-extra-boolean-cast': [
'error',
{
enforceForLogicalOperands: true,
},
], // 禁止不必要的 `bool` 转换
'no-extra-parens': 0, // 禁止非必要的括号
semi: [
'error',
'never',
{
beforeStatementContinuationChars: 'always',
},
],
'no-fallthrough': 1, // 禁止 `switch` 穿透
'no-func-assign': 2, // 禁止重复的函数声明
'no-implicit-coercion': [
'error',
{
allow: ['!!', '~'],
},
], // 禁止隐式转换
'no-implied-eval': 2, // 禁止使用隐式 `eval`
'no-invalid-regexp': 2, // 禁止无效的正则表达式
'no-invalid-this': 2, // 禁止无效的 `this`
'no-irregular-whitespace': 2, // 禁止含有不合法空格
'no-iterator': 2, // 禁止使用 `__iterator__ ` 属性
'no-label-var': 2, // `label` 名不能与 `var` 声明的变量名相同
'no-labels': 2, // 禁止标签声明
'no-lone-blocks': 2, // 禁止不必要的嵌套块
'no-multi-spaces': 1, // 禁止使用多余的空格
'no-multiple-empty-lines': [
'error',
{
max: 2,
},
], // 空行最多不能超过 `2` 行
'no-new-func': 2, // 禁止使用 `new Function`
'no-new-object': 2, // 禁止使用 `new Object`
'no-new-require': 2, // 禁止使用 `new require`
'no-sparse-arrays': 2, // 禁止稀疏数组
'no-trailing-spaces': 1, // 一行结束后面不要有空格
'no-unreachable': 2, // 禁止有无法执行的代码
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
enforceForJSX: true,
},
], // 禁止无用的表达式
'no-useless-call': 2, // 禁止不必要的 `call` 和 `apply`
'no-var': 'error', // 禁用 `var`
'no-with': 2, // 禁用 `with`
'use-isnan': 2, // 强制使用 isNaN 判断 NaN
'no-multi-assign': 2, // 禁止连续声明变量
'prefer-arrow-callback': 2, // 强制使用箭头函数作为回调
curly: ['error', 'all'],
'vue/multi-word-component-names': [
'error',
{
ignores: [],
},
],
'vue/no-use-v-if-with-v-for': [
'error',
{
allowUsingIterationVar: false,
},
],
'vue/require-v-for-key': ['error'],
'vue/require-valid-default-prop': ['error'],
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
'vue/v-on-event-hyphenation': ['error', 'never'],
'vue/component-tags-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'vue/no-v-html': ['error'],
'vue/no-v-text': ['error'],
'vue/component-api-style': [
'error',
['script-setup', 'composition', 'composition-vue2'],
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: false,
},
],
'vue/no-unused-refs': ['error'],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/attribute-hyphenation': [
'error',
'never',
{
ignore: [],
},
],
'vue/no-restricted-static-attribute': [
'error',
{
key: 'key',
message: 'Disallow using key as a custom attribute',
},
],
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='deprecated']",
message: 'Using deprecated API is not allowed.',
},
],
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: '*',
next: 'return',
},
{
blankLine: 'always',
prev: '*',
next: 'function',
},
{
blankLine: 'always',
prev: ['const', 'let', 'var'],
next: '*',
},
{
blankLine: 'any',
prev: ['const', 'let', 'var'],
next: ['const', 'let', 'var'],
},
{
blankLine: 'always',
prev: 'directive',
next: '*',
},
{
blankLine: 'any',
prev: 'directive',
next: 'directive',
},
{
blankLine: 'always',
prev: ['case', 'default'],
next: '*',
},
{
blankLine: 'always',
prev: ['break'],
next: '*',
},
{
blankLine: 'always',
prev: ['import'],
next: '*',
},
{
blankLine: 'any',
prev: 'import',
next: 'import',
},
{
blankLine: 'always',
prev: '*',
next: 'export',
},
{
blankLine: 'any',
prev: 'export',
next: 'export',
},
{
blankLine: 'always',
prev: ['function'],
next: '*',
},
{
blankLine: 'always',
prev: ['class'],
next: '*',
},
{
blankLine: 'always',
prev: '*',
next: 'for',
},
{
blankLine: 'any',
prev: 'for',
next: 'for',
},
{
blankLine: 'always',
prev: '*',
next: ['while', 'do', 'switch'],
},
],
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'with-single-extends',
allowObjectTypes: 'always',
},
],
},
}

View File

@ -14,15 +14,15 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js 20.x - name: Install Node.js 22.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 20.x node-version: 22.x
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9
run_install: false run_install: false
- name: Install dependencies - name: Install dependencies

View File

@ -8,7 +8,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [20.x] node-version: [22.x]
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [true] experimental: [true]
@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory

2
.nvmrc
View File

@ -1 +1 @@
v20.12.0 v22.12.0

View File

@ -17,4 +17,5 @@ module.exports = {
htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 `html` 要不要折行 htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 `html` 要不要折行
endOfLine: 'lf', // 换行符使用 `lf`, endOfLine: 'lf', // 换行符使用 `lf`,
singleAttributePerLine: false, singleAttributePerLine: false,
bracketSameLine: false,
} }

View File

@ -1,3 +1,3 @@
{ {
"recommendations": ["vue.volar", "lokalise.i18n-ally"] "recommendations": ["lokalise.i18n-ally"]
} }

40
.vscode/settings.json vendored
View File

@ -1,4 +1,38 @@
{ {
//
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
//
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/locales/lang"], "i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
@ -21,11 +55,13 @@
"cSpell.words": [ "cSpell.words": [
"baomitu", "baomitu",
"bezier", "bezier",
"Cascader",
"Clickoutside", "Clickoutside",
"codabar", "codabar",
"commitmsg", "commitmsg",
"crossorigin", "crossorigin",
"datetimerange", "datetimerange",
"depcheckrc",
"domtoimage", "domtoimage",
"EDITMSG", "EDITMSG",
"iife", "iife",
@ -43,8 +79,8 @@
"siderbar", "siderbar",
"snapline", "snapline",
"stylelint", "stylelint",
"unocss",
"WUJIE", "WUJIE",
"zlevel" "zlevel"
], ]
"peacock.color": "#007fff"
} }

View File

@ -1,4 +1,266 @@
# CHANGE LOG ## 5.2.4
## Feats
- 新增 `.cursorrules` 文件,用于配置 `cursor` 的规则
- `RChart` 组件相关
- 新增 `watchDeep` 配置项,允许配置是否深度监听 `options` 配置项,在某些场景下不希望监听 `options` 配置项的变动时,可以配置该项为 `false`
- 修改自定义样式名,由 `--r` 前缀改为 `--r` 前缀
- 优化 `RTable`, `RTablePro` 组件与相关 `hook`
- 移除未使用的导入(`DataTableInst``ExtractPublicPropTypes``emit`
- 将 `contextMenuSelect` 中的状态更新提前
- 提取 `handleContextMenu` 函数,避免重复创建
- 优化 `combineRowProps` 的条件判断
- 提取 `renderDefaultToolOptions` 为独立函数
- 简化 `tool` 函数的条件判断
- 移除不必要的 `.bind(this)` 调用
- 如果未启用 `onUpdateColumns``onUpdate:columns` 事件(也就是双向绑定 `columns` 配置项),则认为不需要渲染 `C` 组件,因为有时候你可能希望 `columns` 配置项可能就是写死的,不需要动态修改
- 使用 `nextTick` 优化列配置更新(`C` 组件)
- 减少函数重复创建
- 优化条件判断逻辑
- `selectKeys(keys: RowKey[])` - 批量选中
- `toggleKey(key: RowKey)` - 切换选中状态
- `isKeySelected(key: RowKey)` - 检查是否选中
- 新增 `autoDeleteDuplicateKeys` 配置项,允许自定义是否移除重复请求 `key`
- `usePagination` 方法相关
- 修改 `getCallback` 方法返回值类型,现在会自动推导回调函数类型(仅在默认传递回调函数时有效)
- 修改 `getCallback` 方法使用方式,改为函数调用
- `RBarcode` 组件相关
- 新增 `responsive` 配置项,允许配置是否启用响应式尺寸,当容器大小变化时自动重新渲染条形码,但是该属性让 `width``height` 配置项失效
- 新增 `.vscode` 配置规则,默认强制使用 `prettier` 格式化代码,并且使用 `eslint` 检查代码规范
- 移除所有 `--ray` 的前缀为 `-r`
- 统一自定义组件的文件分包格式
- 标记自定义 `useModal` 方法为遗弃方法
- `useAxiosInterceptor` 更名为 `axiosInterceptor` 方法,旧方法名不符合语义化,现在更加语义化
- 优化 `useElementFullscreen` 方法
- 调整 `MenuTag` 组件样式,现在会根据主题色自动适配关闭按钮颜色
## Fixes
- 修复 `useTablePro.print` 方法无效的问题
- 修复 `vitest` 插件启动会提示失败的问题
## 5.2.3
## Feats
- 更新依赖为主流版本
- `RTablePro` 组件相关
- 新增 `takeoverAutoHeight` 配置项,允许接管表格的流体高度渲染,一旦启用该属性,`flexAutoHeight` 属性将强制启用
- 新增 `collapse` 插槽,配合 `takeoverAutoHeight` 配置项使用,允许自定义表格常见的顶部操作区域,当然也可以做点其他的,但是该插槽仅在启用 `takeoverAutoHeight` 配置项时生效
> 该属性可以让流体高度功能使用更加优雅,有点用处。
- 新增暴露 `setPage`, `setPageSize`, `getPage`, `getPageSize` 方法
- `naive-ui` 最新版本有依赖问题,暂时回退升级
- `echarts` 更新至 `6.0.0` 版本,并且完成适配
## Fixes
- 修复 `usePagination.resetPagination` 方法在重置分页时,没有正确触发 `pageChange`, `pageSizeChange` 回调函数的问题
- 修复 `resetTablePagination` 方法在重置分页时,没有正确触发 `onTablePaginationUpdate` 回调函数的问题
## 5.2.2
## Feats
- `RForm` 组件相关
- 新增 `submitWhenEnter` 配置项,允许在按下回车键时自动触发表单的校验,如果校验成功则会自动触发 `onFinish` 事件
- 新增 `onFinish` 配置项,允许在表单校验成功后自动触发的事件
- 新增 `autocomplete` 配置项,允许配置表单的自动完成功能,默认配置为 `off`
- 新增 `loading` 配置项,允许配置表单的加载状态
- 新增 `loadingDescription` 配置项,允许配置表单的加载状态的描述
- `useForm` 相关
- 新增 `validateTargetField` 方法,允许验证指定表单项的规则
- 初始化方法现在支持传入函数,允许动态获取表单的初始化值与规则
- `formModel` 方法现在会默认联合 `Recordable` 类型,获取初始化类型中未获取到的类型时,默认推到为 `any` 类型
- 新增了 `formConditionRef` 属性,现在可以在内部解构获取一个 `ref` 包裹的响应式初始化表单对象值
- 新增了 `updateFormCondition` 方法,允许更新表单的值,该方法会覆盖初始化值
- 更新依赖为主流版本
- 新增 `unocss` 原子化样式库,但是不推荐全量使用,仅作为一些简单的样式片段使用,否则在调试的时候将会是灾难
> 新增 `unocss` 后,在使用 `ProTable` 组件的流体高度最外层父元素配置时,可以便捷的配置 `h-full` 即可。
## 5.2.1
## Feats
- `RTablePro` 组件相关
- 新增 `runAsyncTableRequest` 方法,与 `runTableRequest` 方法功能一致,但是返回 `Promise` 对象
- 现在不允许使用 `useTemplateRef` 方法注册 `dom` 模板引用,约定强制使用 `useTablePro` 方法的 `register` 方法注册 `hook` 使用相关方法
- `useTablePro` 方法新增 `getTableProConfig` 方法,与 `useTable` 方法的 `getTableConfig` 方法功能一致,获取 `RTablePro` 组件额外注入配置
- `useTable` 方法新增 `getTableConfig` 方法,获取 `RTable` 组件额外注入配置
- 更新包为主流版本
- `vue-router` 因为在 [4.4.1](https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#441-2024-07-31) 版本中有破坏性的更新,所以在 `jsx` 函数式组件使用 `this.$route`, `this.$router` 会提示类型报错,所以现在强制约定需要使用 `useRoute`, `useRouter` 方法显示的声明与使用
- 更新 `naive-ui` 版本至 `2.42.0`
- 更新 `vue` 版本至 `3.5.17`
- `useForm` 方法新增 `reset` 方法,允许重置表单值,该方法依赖 `useForm` 方法的初始化 `formModel` 参数,所以请确保初始化 `formModel` 参数
## 5.2.0
一些破坏性更新,请谨慎更新。
## Feats
- 更新 `vue` 版本至 `3.5.16`
- 更新 `vite` 版本至 `6.3.5`
- `RTablePro` 组件相关
- `runTableRequest` 方法现在支持传递 `reset` 参数,配置是否重置分页请求
- `runTableRequest` 方法新增 `excludeParams` 配置项,允许排除指定的请求参数
- `onTablePaginationUpdate` 方法参数返回值由返回函数改为直接返回值
- 新增 `paginationPrefix` 配置项,允许自定义分页器前缀,在国际化需求可能会有用
- 新增 `flexAutoHeight` 配置项,默认关闭,允许配置表格是否自动继承高度,但是要结合 `css flex` 属性使用
> 如果你是使用 `NFlex` 组件结合 `RTablePro` 或者 `RTable` 组件使用,需要配置 `Flex` 组件的 `vertical` 属性,并且设置 `class``flex-vertical`,即可便捷实现该效果。否则你需要设置 `css flex` 相关属性(可以参考 Demo2的示例。
```tsx
import { RTablePro } from '@/components'
import { NFlex } from 'naive-ui'
const Demo1 = () => {
return (
<NFlex vertical class="flex-vertical">
<RTablePro flexAutoHeight />
</NFlex>
)
}
const Demo2 = () => {
return (
<div
class="flex-vertical"
style="height: 100%; display: flex; flex-direction: column;"
>
<RTablePro flexAutoHeight />
</div>
)
}
```
- 新增 `getDateByNaiveDatePicker` 方法,便捷获取 `naive-ui``DatePicker` 组件的日期值
- `Recordable` 类型新增 `symbol`, `number` 类型作为 `key` 支持
- `RCollapse` 组件相关
- 默认配置 `responsive` 配置项为 `screen` 响应模式
- 默认配置 `cols` 配置项为 `4 xs:1 s:2 m:2 l:4 xl:4 2xl:6`,虽然目前的预设已经足够使用,但你也可以高度自定义需求
- `types`
- 新增 `GlobalDataTableColumns` 类型,用于声明全局 `DataTableColumns` 类型
- 新增 `GlobalRecordable` 类型,用于声明全局 `Recordable` 类型
## Fixes
- 修复 `RTablePro` 组件 `print` 方法打印内容错误的问题
## 5.1.0
## Feats
- 更新 `vite` 版本至 `5.3.3`
## Fixes
- 修复 `chunksCopilot` 方法判断不准确导致 `node_modules` 库被拆分到 `hooks` 分包重复的问题
## 5.0.10
## Feats
- `RDraggableCard` 组件现在不会在抛出获取 `dom` 失败的异常,因为可能存在异步组件加载的可能
- `RModal`, `useModal` 方法,移除 `dad` 相关所有配置,使用 `draggable` 配置项替代
- 刷新的样式现在会跟随主题变化
- 锁屏密码现在会进行加密存储,并且会进行校验处理了
- 新增 `decrypt`, `decrypt` 方法,放置于 `utils/c` 包中
## Fixes
- 修复因为错误的注册全局事件,导致事件污染的问题,但是默认的 `ctrl + k`, `cmd + k` 快捷键依旧保留为全局按键
## 5.0.9
## Feats
- `RDraggableCard` 组件
- 新增 `restrictionElement` 配置项,允许设置拖拽限制元素
- 新增 `padding` 配置项,允许配置元素初始化位置的间隔值
- `defaultPosition` 配置项新增 `top-left`, `top-right`, `bottom-left`, `bottom-right` 配置项,允许配置元素初始化位置
- `RTablePro` 组件
- 现在会自动删除重复的请求参数
- 暴露 `resetTablePagination` 方法,允许手动重置表格分页
- `logout` 方法现在会在执行的时候,清空所有的 `router-route`
- 更新依赖为主流版本
## 5.0.8
## Feats
- 修改 `menuTagOptions` 的缓存方式,现在会缓存至 `sessionStorage` 中,兼容可能多系统版本部署与多开系统页面标签页冲突的问题
- 新增 `RDraggableCard` 组件
- 更新 `vite` 版本至 `6.0.4`
## Fixes
- 修复 `updateObjectValue` 方法对于对象值判断不准确的问题
- 修复 `SettingDrawer` 组件初始化时,没有正确初始化 `settingStore` 的问题
- 修复 `RTable` 组件在未设置 `title``tool``false` 时,导致 `headerStyle` 样式会高一些的问题
## 5.0.7
## Feats
- 更新 `vue` 版本至 `3.5.13`
- 更新 `vite` 版本至 `6.0.3`
- 更新 `naive-ui` 版本至 `2.40.3`
- 更新包依赖为主流依赖
- 更新 `eslint` 版本至 `9.11.0`,并且同步修改 `eslint` 相关配置方式,使用 `eslint.config.mjs` 文件替代
- 更新默认 `node` 版本至 `22.11.0`
- `RCollapseGrid` 组件新增 `actionSpan` 配置项,配置操作区域列数
- `usePagination` 方法新增 `pageChange`, `pageSizeChange` 回调函数,允许在更新分页页码与每页条数的时候,执行自定义回调;用于取代被移除的 `onUpdatePage`, `onUpdatePageSize` 方法
- 移除 `appNaiveUIThemeOverridesCommon` 配置项,现在统一使用 `appNaiveUIThemeOverrides` 配置项
- 优化整体风格样式
## Fixes
- 修复 `useDomToImage` 方法的类型推导问题
- 修复主题切换时,`naive-ui` 主题色覆盖不生效的问题
## 5.0.6
## Feats
- 新增 `useChartProvider` 方法,允许注入 `RCharts` 组件配置
- 更新 `echarts` 版本至 `5.5.1`
- 更新 `vue` 版本至 `3.5.13`
- 更新 `@vueuse/core` 版本至 `11.2.0`
- 修改 `SettingDrawer` 组件的 `defaultOptions` 配置项管理方式,现在迁移至 `store.setting` 包中
- 重构 `cache` 工具模块,更有好的类型推导、更少的代码量
- 重构 `precision` 工具模块,更好的类型推导、更少的代码量
- 重写 `updateObjectValue` 方法,现在类型提示更加准确
- 全局使用 `useTemplateRef`, `shallowRef` 方法替代 `ref` 注册模板引用,减少不必要的响应式代理
- 优化 `MenuTag` 组件的关闭按钮样式
- `LockScreen` 组件新增头像展示
- `AppAvatar` 组件现在默认获取 `avatar` 字段为空的时候,展示名字的首字
- 优化 `UnlockScreen` 组件样式,现在会根据主题自动调整背景颜色
- 优化内容区域过度动画效果
## Fixes
- 修复 `404` 页面【返回】按钮不能准确返回的问题
- 修复 `usePagination.getCallback` 方法类型丢失问题;修复该方法获取实时回调不准确的问题
- 修复初始化时,菜单滚动条不能准确滚动到当前激活项的问题
- 修复 `UnlockScreen` 组件在白色主题下,导致样式显示差异问题,现在统一为黑色主题配置覆盖
- 修复 `LockScreen` 组件在退出锁屏时,没有及时更新 `localStorage` 缓存的问题
- 修复 `setupDayjs` 初始化不准确的问题
## 5.0.5
## Feats
- 新增 `GLOBAL_CLASS_NAMES` 配置项
- 新增 `canSkipRoute` 方法,用于初始化系统菜单时,自动获取可跳转的路由,避免权限系统列表中无权限路由跳转导致异常的问题
- 优化 `useElementFullscreen` 方法的过渡效果
- `useElementFullscreen` 新增 `isFullscreen` 属性,标识当前元素是否处于网页全屏状态
## Fixes
- 修复锁屏逻辑问题
- 修复菜单有时候不能正常的展开被激活项的问题
- 修复 `useSiderBar``close` 问题
## 5.0.4 ## 5.0.4
@ -103,7 +365,7 @@
- 新增 `clearSigningCallback` 方法 - 新增 `clearSigningCallback` 方法
- `vite.custom.config` 新增 `cdn` 配置项,是否启用 `cdn` 构建项目 - `vite.custom.config` 新增 `cdn` 配置项,是否启用 `cdn` 构建项目
- 配置 `cdn``false`,因为国内厂商更新资源速度有点慢,导致预览失败 - 配置 `cdn``false`,因为国内厂商更新资源速度有点慢,导致预览失败
- `Layout` 层注入 `--window-width`, `--window-height` `css var` 属性 - `Layout` 层注入 `--window-width`, `--window-height`, `css var` 属性
- 稳定 `Layout` 层的 `css var` 属性 - 稳定 `Layout` 层的 `css var` 属性
## Fixes ## Fixes
@ -2038,7 +2300,7 @@ useAppTheme key 类型: 'dark' | 'light'
### Feats ### Feats
- 修改 Menu 菜单过滤逻辑,现在如果权限不匹配或者设置了 hidden 属性,则会被过滤掉 - 修改 Menu 菜单过滤逻辑,现在如果权限不匹配或者设置了 hidden 属性,则会被过滤掉
- 移除 $activedColor 全局 sass 变量,使用 --ray-theme-primary-color 替代 - 移除 $activedColor 全局 sass 变量,使用 --r-theme-primary-color 替代
- 新增路由菜单检索功能 - 新增路由菜单检索功能
- 移除 App.tsx 中同步主题方法,改为使用 cfg 配置并且使用 ejs 注入 - 移除 App.tsx 中同步主题方法,改为使用 cfg 配置并且使用 ejs 注入
- 移除 MenuTag 默认主题色,现在会以当前主题色为主色 - 移除 MenuTag 默认主题色,现在会以当前主题色为主色

View File

@ -3,6 +3,7 @@ import { callWithAsyncErrorHandling } from '../../src/utils/basic'
describe('callWithAsyncErrorHandling', () => { describe('callWithAsyncErrorHandling', () => {
it('should call the function and return the result', () => { it('should call the function and return the result', () => {
const fn = (x: number) => x const fn = (x: number) => x
const callbackFn = () => {} const callbackFn = () => {}
expect(callWithAsyncErrorHandling(fn, callbackFn, [1])).resolves.toBe(1) expect(callWithAsyncErrorHandling(fn, callbackFn, [1])).resolves.toBe(1)
@ -14,6 +15,7 @@ describe('callWithAsyncErrorHandling', () => {
const fn = () => { const fn = () => {
throw new Error('test error') throw new Error('test error')
} }
const callbackFn = () => { const callbackFn = () => {
callbackFnExecuted = 2 callbackFnExecuted = 2
} }

View File

@ -3,6 +3,7 @@ import { callWithErrorHandling } from '../../src/utils/basic'
describe('callWithErrorHandling', () => { describe('callWithErrorHandling', () => {
it('should call the function and return the result', () => { it('should call the function and return the result', () => {
const fn = (x: number) => x const fn = (x: number) => x
const callbackFn = () => {} const callbackFn = () => {}
expect(callWithErrorHandling(fn, callbackFn, [1])).toBe(1) expect(callWithErrorHandling(fn, callbackFn, [1])).toBe(1)
@ -14,6 +15,7 @@ describe('callWithErrorHandling', () => {
const fn = () => { const fn = () => {
throw new Error('test error') throw new Error('test error')
} }
const callbackFn = () => { const callbackFn = () => {
callbackFnExecuted = 2 callbackFnExecuted = 2
} }

View File

@ -42,6 +42,7 @@ describe('isValueType', () => {
}) })
it('should return false for Function', () => { it('should return false for Function', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
expect(isValueType<Function>(/a/i, 'Function')).toBe(false) expect(isValueType<Function>(/a/i, 'Function')).toBe(false)
}) })
}) })

View File

@ -14,6 +14,7 @@ describe('uuid', () => {
it('should return a string with length 36', () => { it('should return a string with length 36', () => {
const uid = uuid(36) const uid = uuid(36)
expect(uid.length).toBe(36) expect(uid.length).toBe(36)
}) })
}) })

View File

@ -39,6 +39,7 @@ describe('useContextmenuCoordinate', () => {
clientX: 100, clientX: 100,
clientY: 200, clientY: 200,
}) })
wrapperRef.element.dispatchEvent(event) wrapperRef.element.dispatchEvent(event)
await nextTick() await nextTick()

View File

@ -13,6 +13,7 @@ import { mount } from '@vue/test-utils'
* *
* const text = wrapper.find('div').text() // hello * const text = wrapper.find('div').text() // hello
*/ */
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const createRefElement = (slots?: Record<string, Function>) => { const createRefElement = (slots?: Record<string, Function>) => {
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({

View File

@ -3,6 +3,7 @@ import { call } from '../../src/utils/vue/call'
describe('call', () => { describe('call', () => {
it('should be executed once', () => { it('should be executed once', () => {
const fn = vi.fn() const fn = vi.fn()
call(() => fn()) call(() => fn())
expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledTimes(1)
@ -10,6 +11,7 @@ describe('call', () => {
it('should be executed with an argument', () => { it('should be executed with an argument', () => {
const fn = vi.fn() const fn = vi.fn()
call((a: number) => fn(a), 1) call((a: number) => fn(a), 1)
expect(fn).toHaveBeenCalledWith(1) expect(fn).toHaveBeenCalledWith(1)

View File

@ -4,6 +4,7 @@ import createRefElement from '../utils/createRefElement'
describe('renderNode', () => { describe('renderNode', () => {
it('should render string', () => { it('should render string', () => {
const wrapper = createRefElement({ const wrapper = createRefElement({
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
default: renderNode('hello world') as Function, default: renderNode('hello world') as Function,
}) })
const text = wrapper.text() const text = wrapper.text()

365
eslint.config.mjs Normal file
View File

@ -0,0 +1,365 @@
import vue from 'eslint-plugin-vue'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import prettier from 'eslint-plugin-prettier'
import globals from 'globals'
import parser from 'vue-eslint-parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
export default [
{
ignores: [
'**/node_modules/',
'**/dist/',
'dist/*',
'node_modules/*',
'**/auto-imports.d.ts',
'**/components.d.ts',
'**/.gitignore',
'**/.vscode',
'**/public',
'**/yarn.*',
'**/vite-env.*',
'**/.prettierrc.*',
'**/visualizer.*',
'**/visualizer.html',
'**/.env.*',
'src/locales/lang',
'**/.depcheckrc',
'src/app-config/echart-themes/**/*.json',
'**/*.md',
'src/icons/*.svg',
],
},
{
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.vue'],
},
...compat.extends(
'eslint-config-prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
'prettier',
'./unplugin/.eslintrc-auto-import.json',
),
{
plugins: {
vue,
'@typescript-eslint': typescriptEslint,
prettier,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
defineOptions: 'readonly',
defineModel: 'readonly',
},
parser: parser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
},
rules: {
'no-undefined': ['error'],
'linebreak-style': ['error', 'unix'],
'@typescript-eslint/no-explicit-any': [
'error',
{
ignoreRestArgs: true,
},
],
'prettier/prettier': ['error', {}],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
disallowTypeAnnotations: false,
},
],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'accessor-pairs': 2,
'constructor-super': 0,
'default-case': 2,
eqeqeq: [2, 'allow-null'],
'no-alert': 0,
'no-array-constructor': 2,
'no-bitwise': 0,
'no-caller': 1,
'no-catch-shadow': 2,
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-constant-condition': 2,
'no-dupe-keys': 2,
'no-dupe-args': 2,
'no-duplicate-case': 2,
'no-eval': 1,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': [
'error',
{
enforceForLogicalOperands: true,
},
],
'no-extra-parens': 0,
semi: [
'error',
'never',
{
beforeStatementContinuationChars: 'always',
},
],
'no-fallthrough': 1,
'no-func-assign': 2,
'no-implicit-coercion': [
'error',
{
allow: ['!!', '~'],
},
],
'no-implied-eval': 2,
'no-invalid-regexp': 2,
'no-invalid-this': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': 2,
'no-lone-blocks': 2,
'no-multi-spaces': 1,
'no-multiple-empty-lines': [
'error',
{
max: 2,
},
],
'no-new-func': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-sparse-arrays': 2,
'no-trailing-spaces': 1,
'no-unreachable': 2,
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
enforceForJSX: true,
},
],
'no-useless-call': 2,
'no-var': 'error',
'no-with': 2,
'use-isnan': 2,
'no-multi-assign': 2,
'prefer-arrow-callback': 2,
curly: ['error', 'all'],
'vue/multi-word-component-names': [
'error',
{
ignores: [],
},
],
'vue/no-use-v-if-with-v-for': [
'error',
{
allowUsingIterationVar: false,
},
],
'vue/require-v-for-key': ['error'],
'vue/require-valid-default-prop': ['error'],
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
'vue/v-on-event-hyphenation': ['error', 'never'],
'vue/component-tags-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'vue/no-v-html': ['error'],
'vue/no-v-text': ['error'],
'vue/component-api-style': [
'error',
['script-setup', 'composition', 'composition-vue2'],
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: false,
},
],
'vue/no-unused-refs': ['error'],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/attribute-hyphenation': [
'error',
'never',
{
ignore: [],
},
],
'vue/no-restricted-static-attribute': [
'error',
{
key: 'key',
message: 'Disallow using key as a custom attribute',
},
],
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='deprecated']",
message: 'Using deprecated API is not allowed.',
},
],
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: ['import'],
next: '*',
},
{
blankLine: 'any',
prev: 'import',
next: 'import',
},
{
blankLine: 'always',
prev: '*',
next: 'export',
},
{
blankLine: 'any',
prev: 'export',
next: 'export',
},
{
blankLine: 'always',
prev: ['const', 'let', 'var'],
next: '*',
},
{
blankLine: 'any',
prev: ['const', 'let', 'var'],
next: ['const', 'let', 'var'],
},
{
blankLine: 'always',
prev: 'directive',
next: '*',
},
{
blankLine: 'any',
prev: 'directive',
next: 'directive',
},
{
blankLine: 'always',
prev: '*',
next: [
'if',
'class',
'for',
'do',
'while',
'switch',
'try',
'with',
'function',
'block',
'block-like',
'break',
'case',
'continue',
'return',
'throw',
'debugger',
],
},
{
blankLine: 'always',
prev: [
'if',
'class',
'for',
'do',
'while',
'switch',
'try',
'with',
'function',
'block',
'block-like',
'break',
'case',
'continue',
'return',
'throw',
'debugger',
],
next: '*',
},
],
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'with-single-extends',
allowObjectTypes: 'always',
},
],
},
},
]

View File

@ -13,8 +13,29 @@
:root { :root {
--preloading-tag-color: <%= preloadingConfig.tagColor %>; --preloading-tag-color: <%= preloadingConfig.tagColor %>;
--preloading-title-color: <%= preloadingConfig.titleColor %>; --preloading-title-color: <%= preloadingConfig.titleColor %>;
--ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>; --r-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>;
--ray-theme-primary-color: <%= appPrimaryColor.primaryColor %>; --r-theme-primary-color: <%= appPrimaryColor.primaryColor %>;
--global-loading-bg-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
#pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
}
@media (prefers-color-scheme: light) {
#pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
}
html.dark #pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
html.light #pre-loading-animation {
background-color: var(--global-loading-bg-color);
} }
#pre-loading-animation { #pre-loading-animation {
@ -23,13 +44,9 @@
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
background-color: #ffffff;
color: var(--preloading-title-color); color: var(--preloading-title-color);
text-align: center; text-align: center;
} background-color: var(--global-loading-bg-color);
.ray-template--dark #pre-loading-animation {
background-color: #2a3146;
} }
#pre-loading-animation .pre-loading-animation__wrapper { #pre-loading-animation .pre-loading-animation__wrapper {
@ -95,6 +112,18 @@
} }
} }
</style> </style>
<script>
;(function () {
const html = document.documentElement
const store = window.localStorage.getItem('piniaSettingStore')
const { _appTheme = false } = store ? JSON.parse(store) : {}
const loadingBgColor = _appTheme ? '#1c1e23' : '#ffffff'
html.classList.add(_appTheme ? 'dark' : 'light')
html.style.setProperty('--global-loading-bg-color', loadingBgColor)
html.style.setProperty('background-color', loadingBgColor)
})()
</script>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="pre-loading-animation"> <div id="pre-loading-animation">

View File

@ -1,10 +1,10 @@
{ {
"name": "ray-template", "name": "ray-template",
"private": false, "private": false,
"version": "5.0.4", "version": "5.2.4",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0", "node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"pnpm": ">=9.0.0" "pnpm": ">=9.0.0"
}, },
"scripts": { "scripts": {
@ -16,7 +16,7 @@
"prepare": "husky install", "prepare": "husky install",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
"lint": "vue-tsc --noEmit && eslint src --ext .js,.jsx,.vue && prettier --write \"src/**/*.{ts,tsx,json,.vue}\"" "lint": "vue-tsc --noEmit && eslint --fix && prettier --write \"**/*.{ts,tsx,json,.vue}\""
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -29,83 +29,86 @@
"prettier --write" "prettier --write"
], ],
"*.{ts,tsx,vue}": [ "*.{ts,tsx,vue}": [
"eslint src" "eslint --fix"
] ]
}, },
"dependencies": { "dependencies": {
"@logicflow/core": "2.0.6", "@logicflow/core": "2.0.10",
"@logicflow/extension": "2.0.10", "@logicflow/extension": "2.0.14",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^13.1.0",
"axios": "^1.7.5", "axios": "^1.10.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "4.2.0",
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"dayjs": "^1.11.10", "dayjs": "^1.11.13",
"echarts": "^5.5.0", "echarts": "^6.0.0",
"html-to-image": "1.11.11", "html-to-image": "1.11.13",
"interactjs": "1.10.26", "interactjs": "1.10.27",
"jsbarcode": "3.11.6", "jsbarcode": "3.11.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mockjs": "1.1.0", "mockjs": "1.1.0",
"naive-ui": "^2.40.1", "naive-ui": "^2.42.0",
"pinia": "^2.2.4", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.1.1", "pinia-plugin-persistedstate": "^4.4.1",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"vue": "^3.5.12", "vue": "^3.5.24",
"vue-demi": "0.14.6", "vue-demi": "0.14.10",
"vue-hooks-plus": "2.2.1", "vue-hooks-plus": "2.4.1",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.2", "vue-router": "^4.6.3",
"vue3-next-qrcode": "2.0.10" "vue3-next-qrcode": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.8.1", "@commitlint/cli": "19.7.1",
"@commitlint/config-conventional": "^17.8.1", "@commitlint/config-conventional": "19.7.1",
"@interactjs/types": "1.10.21", "@eslint/eslintrc": "3.3.1",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@eslint/js": "9.30.1",
"@types/crypto-js": "^4.2.2", "@interactjs/types": "1.10.27",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@types/crypto-js": "4.2.2",
"@types/jsbarcode": "3.11.4", "@types/jsbarcode": "3.11.4",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "4.17.12",
"@types/mockjs": "1.0.7", "@types/mockjs": "1.0.10",
"@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/eslint-plugin": "8.36.0",
"@typescript-eslint/parser": "^8.13.0", "@typescript-eslint/parser": "8.36.0",
"@vitejs/plugin-vue": "^5.1.0", "@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "5.1.1",
"@vitest/ui": "1.4.0", "@vitest/ui": "3.0.5",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "10.1.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "14.2.0",
"@vue/test-utils": "2.4.3", "@vue/test-utils": "2.4.6",
"autoprefixer": "^10.4.16", "autoprefixer": "10.4.21",
"depcheck": "^1.4.7", "depcheck": "1.4.7",
"eslint": "^8.57.0", "eslint": "9.31.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "10.1.8",
"eslint-config-standard-with-typescript": "^43.0.0", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-vue": "9.32.0",
"eslint-plugin-promise": "^6.1.1", "globals": "16.3.0",
"eslint-plugin-vue": "^9.25.0", "happy-dom": "17.1.0",
"happy-dom": "14.3.1",
"husky": "8.0.3", "husky": "8.0.3",
"lint-staged": "^15.2.0", "lint-staged": "15.4.3",
"postcss": "^8.4.38", "postcss": "8.5.6",
"postcss-px-to-viewport-8-with-include": "1.2.2", "postcss-px-to-viewport-8-with-include": "1.2.2",
"prettier": "^3.2.5", "prettier": "3.6.2",
"rollup-plugin-gzip": "4.0.1", "rollup-plugin-gzip": "4.0.1",
"sass": "1.71.1", "sass": "1.86.3",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "6.0.11",
"typescript": "^5.6.3", "typescript": "5.8.3",
"unplugin-auto-import": "^0.18.2", "unocss": "66.3.3",
"unplugin-vue-components": "^0.27.4", "unplugin-auto-import": "19.1.2",
"vite": "^5.4.3", "unplugin-vue-components": "0.28.0",
"vite-bundle-analyzer": "0.9.4", "vite": "6.3.5",
"vite-bundle-analyzer": "0.16.0",
"vite-plugin-cdn2": "1.1.0", "vite-plugin-cdn2": "1.1.0",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "1.7.0",
"vite-plugin-eslint": "1.8.1", "vite-plugin-eslint": "1.8.1",
"vite-plugin-inspect": "^0.8.3", "vite-plugin-inspect": "0.10.6",
"vite-plugin-mock-dev-server": "1.4.7", "vite-plugin-mock-dev-server": "1.8.3",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-svg-loader": "^4.0.0", "vite-svg-loader": "5.1.0",
"vite-tsconfig-paths": "4.3.2", "vitest": "2.1.8",
"vitest": "1.5.2", "vue-eslint-parser": "9.4.3",
"vue-tsc": "^2.1.10" "vue-tsc": "2.2.8"
}, },
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->", "description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
"main": "index.ts", "main": "index.ts",

7781
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ interface JSONPlaceholder {
* *
* @returns * @returns
* *
* @medthod get * @method get
*/ */
export const getWeather = (city: string) => { export const getWeather = (city: string) => {
return request<AxiosTestResponse>({ return request<AxiosTestResponse>({

View File

@ -54,7 +54,14 @@ const AppAvatar = defineComponent({
objectFit="cover" objectFit="cover"
round round
size={avatarSize} size={avatarSize}
/> >
{{
default: () =>
getSigningCallback.avatar
? null
: getSigningCallback?.name?.[0],
}}
</NAvatar>
{getSigningCallback?.name} {getSigningCallback?.name}
</NFlex> </NFlex>
</NButton> </NButton>

View File

@ -44,6 +44,7 @@ const GlobalSpin = defineComponent({
{...(this.$props as SpinProps)} {...(this.$props as SpinProps)}
show={this.spinValue} show={this.spinValue}
themeOverrides={this.overrides} themeOverrides={this.overrides}
style="height: var(--html-height)"
> >
{{ {{
...this.$slots, ...this.$slots,

View File

@ -1,17 +1,10 @@
/**
*
*
*
* ,
*/
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { APP_CATCH_KEY } from '@/app-config' import { APP_CATCH_KEY } from '@/app-config'
const appLockScreen = useStorage( const appLockScreen = useStorage(
APP_CATCH_KEY.isAppLockScreen, APP_CATCH_KEY.isAppLockScreen,
false, false,
sessionStorage, window.localStorage,
{ {
mergeDefaults: true, mergeDefaults: true,
}, },

View File

@ -1,16 +1,22 @@
import { NInput, NForm, NFormItem, NButton } from 'naive-ui' import { NInput, NFormItem, NButton } from 'naive-ui'
import AppAvatar from '@/app-components/app/AppAvatar'
import { RForm } from '@/components'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared' import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
import { useSettingGetters, useSettingActions } from '@/store' import { useSettingActions } from '@/store'
import { useTemplateRef } from 'vue'
import { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { setStorage, encrypt } from '@/utils'
import type { FormInst, InputInst } from 'naive-ui' import type { InputInst } from 'naive-ui'
const LockScreen = defineComponent({ const LockScreen = defineComponent({
name: 'LockScreen', name: 'LockScreen',
setup() { setup() {
const formInstRef = ref<FormInst | null>(null) const [register, { validate }] = useForm()
const inputInstRef = ref<InputInst | null>(null) const inputInstRef = useTemplateRef<InputInst | null>('inputInstRef')
const { setLockAppScreen } = useAppLockScreen() const { setLockAppScreen } = useAppLockScreen()
const { updateSettingState } = useSettingActions() const { updateSettingState } = useSettingActions()
@ -19,15 +25,17 @@ const LockScreen = defineComponent({
lockCondition: useCondition(), lockCondition: useCondition(),
}) })
/** 锁屏 */
const lockScreen = () => { const lockScreen = () => {
formInstRef.value?.validate((error) => { validate().then(() => {
if (!error) {
setLockAppScreen(true) setLockAppScreen(true)
updateSettingState('lockScreenSwitch', true) updateSettingState('lockScreenSwitch', false)
setStorage(
APP_CATCH_KEY.appLockScreenPasswordKey,
encrypt(state.lockCondition.lockPassword),
'localStorage',
)
state.lockCondition = useCondition() state.lockCondition = useCondition()
}
}) })
} }
@ -40,18 +48,27 @@ const LockScreen = defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
lockScreen, lockScreen,
formInstRef, register,
inputInstRef, inputInstRef,
} }
}, },
render() { render() {
const { register } = this
return ( return (
<div class="app-lock-screen__content">
<div class="app-lock-screen__input"> <div class="app-lock-screen__input">
<NForm <AppAvatar
avatarSize={52}
style="pointer-events: none;margin: 24px 0;"
vertical
/>
<RForm
ref="formInstRef" ref="formInstRef"
model={this.lockCondition} model={this.lockCondition}
rules={rules} rules={rules}
labelPlacement="left" labelPlacement="left"
onRegister={register}
> >
<NFormItem path="lockPassword"> <NFormItem path="lockPassword">
<NInput <NInput
@ -68,12 +85,14 @@ const LockScreen = defineComponent({
this.lockScreen() this.lockScreen()
} }
}} }}
autofocus
/> />
</NFormItem> </NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}> <NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton> </NButton>
</NForm> </RForm>
</div>
</div> </div>
) )
}, },

View File

@ -1,19 +1,22 @@
import { NInput, NForm, NFormItem, NButton, NFlex } from 'naive-ui' import '../../index.scss'
import { NInput, NFormItem, NButton, NFlex } from 'naive-ui'
import AppAvatar from '@/app-components/app/AppAvatar' import AppAvatar from '@/app-components/app/AppAvatar'
import { RForm } from '@/components'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useSigningActions, useSettingActions } from '@/store' import { useSigningActions, useSettingActions } from '@/store'
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared' import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useDevice } from '@/hooks' import { useDevice } from '@/hooks'
import { useForm } from '@/components'
import type { FormInst, InputInst } from 'naive-ui' import { APP_CATCH_KEY } from '@/app-config'
import { removeStorage, decrypt, getStorage } from '@/utils'
export default defineComponent({ export default defineComponent({
name: 'UnlockScreen', name: 'UnlockScreen',
setup() { setup() {
const formRef = ref<FormInst | null>(null) const [register, { validate }] = useForm()
const inputInstRef = ref<InputInst | null>(null)
const { logout } = useSigningActions() const { logout } = useSigningActions()
const { updateSettingState } = useSettingActions() const { updateSettingState } = useSettingActions()
@ -22,13 +25,13 @@ export default defineComponent({
const HH_MM_FORMAT = 'HH:mm' const HH_MM_FORMAT = 'HH:mm'
const AM_PM_FORMAT = 'A' const AM_PM_FORMAT = 'A'
const YY_MM_DD_FORMAT = 'YY年MM月DD日' const YY_MM_DD_FORMAT = 'YYYY-MM-DD'
const DDD_FORMAT = 'ddd' const DDD_FORMAT = 'ddd'
const state = reactive({ const state = reactive({
lockCondition: useCondition(), lockCondition: useCondition(),
HH_MM: dayjs().format(HH_MM_FORMAT), HH_MM: dayjs().format(HH_MM_FORMAT),
AM_PM: dayjs().locale('en').format(AM_PM_FORMAT), AM_PM: dayjs().format(AM_PM_FORMAT),
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT), YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
DDD: dayjs().format(DDD_FORMAT), DDD: dayjs().format(DDD_FORMAT),
}) })
@ -41,30 +44,55 @@ export default defineComponent({
state.DDD = dayjs().format(DDD_FORMAT) state.DDD = dayjs().format(DDD_FORMAT)
}, 86_400_000) }, 86_400_000)
/** 退出登陆并且回到登陆页 */ const toSigningFn = () => {
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
updateSettingState('lockScreenSwitch', false)
setTimeout(() => {
logout()
}, 100)
}
const backToSigning = () => { const backToSigning = () => {
window.$dialog.warning({ window.$dialog.warning({
title: '警告', title: '警告',
content: '是否返回到登陆页?', content: '是否返回到登陆页并且重新登录',
positiveText: '确定', positiveText: '确定',
negativeText: '取消', negativeText: '重新登录',
onPositiveClick: () => { onPositiveClick: toSigningFn,
logout()
setTimeout(() => {
updateSettingState('lockScreenSwitch', false)
})
},
}) })
} }
/** 解锁 */
const unlockScreen = () => { const unlockScreen = () => {
formRef.value?.validate((error) => { const catchPassword = getStorage<string>(
if (!error) { APP_CATCH_KEY.appLockScreenPasswordKey,
'localStorage',
)
if (!catchPassword) {
window.$dialog.warning({
title: '警告',
content: () => '检测到锁屏密码被修改,请重新登录',
closable: false,
maskClosable: false,
closeOnEsc: false,
positiveText: '重新登录',
onPositiveClick: toSigningFn,
})
return
}
const dCatchPassword = decrypt(catchPassword)
validate().then(() => {
if (dCatchPassword === state.lockCondition.lockPassword) {
setLockAppScreen(false) setLockAppScreen(false)
updateSettingState('lockScreenSwitch', false) updateSettingState('lockScreenSwitch', false)
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
state.lockCondition = useCondition() state.lockCondition = useCondition()
} else {
window.$message.warning('密码错误,请重新输入')
} }
}) })
} }
@ -78,18 +106,18 @@ export default defineComponent({
...toRefs(state), ...toRefs(state),
backToSigning, backToSigning,
unlockScreen, unlockScreen,
formRef,
inputInstRef,
isTabletOrSmaller, isTabletOrSmaller,
register,
} }
}, },
render() { render() {
const { isTabletOrSmaller } = this const { isTabletOrSmaller } = this
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
const hmSplit = HH_MM.split(':') const hmSplit = HH_MM.split(':')
const { unlockScreen, backToSigning } = this const { unlockScreen, backToSigning, register } = this
return ( return (
<div class="app-lock-screen__content app-lock-screen__content--full">
<div class="app-lock-screen__unlock"> <div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock__content"> <div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content-wrapper"> <div class="app-lock-screen__unlock__content-wrapper">
@ -107,19 +135,26 @@ export default defineComponent({
</div> </div>
</div> </div>
<div class="app-lock-screen__unlock__content-avatar"> <div class="app-lock-screen__unlock__content-avatar">
<AppAvatar avatarSize={52} style="pointer-events: none;" vertical /> <AppAvatar
avatarSize={52}
style="pointer-events: none;"
vertical
/>
</div> </div>
<div class="app-lock-screen__unlock__content-input"> <div class="app-lock-screen__unlock__content-input">
<NForm ref="formRef" model={this.lockCondition} rules={rules}> <RForm
onRegister={register}
model={this.lockCondition}
rules={rules}
>
<NFormItem path="lockPassword"> <NFormItem path="lockPassword">
<NInput <NInput
ref="inputInstRef" autofocus
v-model:value={this.lockCondition.lockPassword} v-model:value={this.lockCondition.lockPassword}
type="password" type="password"
placeholder="请输入解锁密码" placeholder="请输入解锁密码"
clearable clearable
minlength={6} minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => { onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') { if (e.code === 'Enter') {
unlockScreen() unlockScreen()
@ -128,21 +163,27 @@ export default defineComponent({
/> />
</NFormItem> </NFormItem>
<NFlex justify="space-between"> <NFlex justify="space-between">
<NButton type="primary" text onClick={backToSigning.bind(this)}> <NButton
type="primary"
text
onClick={backToSigning.bind(this)}
>
</NButton> </NButton>
<NButton type="primary" text onClick={unlockScreen.bind(this)}> <NButton
type="primary"
text
onClick={unlockScreen.bind(this)}
>
</NButton> </NButton>
</NFlex> </NFlex>
</NForm> </RForm>
</div> </div>
<div class="app-lock-screen__unlock__content-date"> <div class="app-lock-screen__unlock__content-date">
<div class="current-date">
{HH_MM}&nbsp;<span>{AM_PM}</span>
</div>
<div class="current-year"> <div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span> {YY_MM_DD}&nbsp;<span>{DDD}</span>&nbsp;<span>{AM_PM}</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,10 @@
.app-lock-screen__content { .app-lock-screen__content {
&.app-lock-screen__content--full {
width: 100%;
height: var(--html-height);
@include flexCenter;
}
& .app-lock-screen__input { & .app-lock-screen__input {
& button[class*='n-button'] { & button[class*='n-button'] {
width: 100%; width: 100%;
@ -30,7 +36,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
@include flexCenter; @include flexCenter;
font-size: 320px; font-size: 16.67rem;
gap: 80px; gap: 80px;
z-index: 0; z-index: 0;
@ -79,9 +85,23 @@
& .current-year, & .current-year,
& .current-date span { & .current-date span {
font-size: 1.5rem; font-size: 1.875rem;
line-height: 2.25rem;
} }
} }
} }
} }
} }
.ray-template--light {
.app-lock-screen__unlock__content-bg__wrapper {
background-color: #fff !important;
}
.app-lock-screen__unlock__content-bg {
& .left,
& .right {
background-color: rgba(244, 244, 245, 1) !important;
}
}
}

View File

@ -1,22 +1,11 @@
/**
*
* ,
*
*/
import './index.scss'
import { RModal } from '@/components' import { RModal } from '@/components'
import LockScreen from './components/LockScreen' import LockScreen from './components/LockScreen'
import UnlockScreen from './components/UnlockScreen'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useSettingGetters, useSettingActions } from '@/store' import { useSettingGetters, useSettingActions } from '@/store'
const AppLockScreen = defineComponent({ const AppLockScreen = defineComponent({
name: 'AppLockScreen', name: 'AppLockScreen',
setup() { setup() {
const { getLockAppScreen } = useAppLockScreen()
const { updateSettingState } = useSettingActions() const { updateSettingState } = useSettingActions()
const { getLockScreenSwitch } = useSettingGetters() const { getLockScreenSwitch } = useSettingGetters()
const lockScreenSwitchRef = computed({ const lockScreenSwitchRef = computed({
@ -28,12 +17,9 @@ const AppLockScreen = defineComponent({
return { return {
lockScreenSwitchRef, lockScreenSwitchRef,
getLockAppScreen,
} }
}, },
render() { render() {
const { getLockAppScreen } = this
return ( return (
<RModal <RModal
v-model:show={this.lockScreenSwitchRef} v-model:show={this.lockScreenSwitchRef}
@ -42,12 +28,10 @@ const AppLockScreen = defineComponent({
autoFocus={false} autoFocus={false}
maskClosable={false} maskClosable={false}
closeOnEsc={false} closeOnEsc={false}
preset={!getLockAppScreen() ? 'dialog' : void 0} preset="dialog"
title="锁定屏幕" title="锁定屏幕"
> >
<div class="app-lock-screen__content"> <LockScreen />
{!getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
</div>
</RModal> </RModal>
) )
}, },

View File

@ -7,7 +7,7 @@ import {
getStorage, getStorage,
} from '@/utils' } from '@/utils'
import { useSettingGetters } from '@/store' import { useSettingGetters } from '@/store'
import { APP_CATCH_KEY, THEME_CLASS_NAMES } from '@/app-config' import { APP_CATCH_KEY, GLOBAL_CLASS_NAMES, APP_THEME } from '@/app-config'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import type { SettingState } from '@/store/modules/setting/types' import type { SettingState } from '@/store/modules/setting/types'
@ -20,9 +20,14 @@ export default defineComponent({
// 同步主题色变量至 html如果未获取到缓存值则已默认值填充 // 同步主题色变量至 html如果未获取到缓存值则已默认值填充
const syncPrimaryColorToBody = () => { const syncPrimaryColorToBody = () => {
// 默认主题色
const { const {
appPrimaryColor: { primaryColor, primaryFadeColor }, appPrimaryColor: { primaryColor, primaryFadeColor },
} = __APP_CFG__ // 默认主题色 } = APP_THEME
// 主题色配置 class 名
const { rayTemplateThemePrimaryColor, rayTemplateThemePrimaryFadeColor } =
GLOBAL_CLASS_NAMES
const html = document.documentElement const html = document.documentElement
// 获取缓存 naive ui 配置项 // 获取缓存 naive ui 配置项
@ -39,21 +44,22 @@ export default defineComponent({
primaryColor, primaryColor,
) )
// 将主色调任意颜色转换为 rgba 格式 // 将主色调任意颜色转换为 rgba 格式
const fp = colorToRgba(p, 0.38) const fp = colorToRgba(p, 0.85)
// 设置全局主题色 css 变量 // 设置全局主题色 css 变量
html.style.setProperty('--ray-theme-primary-color', p) // 主色调 html.style.setProperty(rayTemplateThemePrimaryColor, p) // 主色调
// 降低透明度后的主色调
html.style.setProperty( html.style.setProperty(
'--ray-theme-primary-fade-color', rayTemplateThemePrimaryFadeColor,
fp || primaryFadeColor, fp || primaryFadeColor,
) // 降低透明度后的主色调 )
} }
} }
// 隐藏加载动画 // 隐藏加载动画
const hiddenLoadingAnimation = () => { const hiddenLoadingAnimation = () => {
// pre-loading-animation 是默认 id // pre-loading-animation 是默认 id
const el = document.getElementById('pre-loading-animation') const el = document.getElementById(GLOBAL_CLASS_NAMES.preLoadingAnimation)
if (el) { if (el) {
setStyle(el, { setStyle(el, {
@ -65,7 +71,7 @@ export default defineComponent({
// 切换主题时,同步更新 html class 以便于进行自定义 css 配置 // 切换主题时,同步更新 html class 以便于进行自定义 css 配置
const updateGlobalThemeClass = (bool: boolean) => { const updateGlobalThemeClass = (bool: boolean) => {
const html = document.documentElement const html = document.documentElement
const { darkClassName, lightClassName } = THEME_CLASS_NAMES const { darkClassName, lightClassName } = GLOBAL_CLASS_NAMES
bool bool
? removeClass(html, lightClassName) ? removeClass(html, lightClassName)
@ -82,8 +88,8 @@ export default defineComponent({
updateGlobalThemeClass(getAppTheme.value) updateGlobalThemeClass(getAppTheme.value)
// 注入全局宽高尺寸 // 注入全局宽高尺寸
setStyle(document.documentElement, { setStyle(document.documentElement, {
'--html-height': `${height.value}px`, [GLOBAL_CLASS_NAMES.htmlHeight]: `${height.value}px`,
'--html-width': `${width.value}px`, [GLOBAL_CLASS_NAMES.htmlWidth]: `${width.value}px`,
}) })
}) })

View File

@ -28,18 +28,10 @@ export default defineComponent({
if (version !== cacheVersion) { if (version !== cacheVersion) {
modalShow.value = true modalShow.value = true
setStorage<string>( setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
APP_CATCH_KEY.appVersionProvider,
version,
'localStorage',
)
} }
} else { } else {
setStorage<string>( setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
APP_CATCH_KEY.appVersionProvider,
version,
'localStorage',
)
} }
return { return {
@ -61,7 +53,7 @@ export default defineComponent({
title="发现新版本" title="发现新版本"
content="当前版本已更新,点击确认加载新版本~" content="当前版本已更新,点击确认加载新版本~"
zIndex={999999999} zIndex={999999999}
dad draggable
positiveText="确认" positiveText="确认"
negativeText="取消" negativeText="取消"
onPositiveClick={logout} onPositiveClick={logout}

View File

@ -5,11 +5,19 @@ import type { MessageProviderProps } from 'naive-ui'
* *
* @description * @description
* html * html
*
* class name
*
*/ */
export const THEME_CLASS_NAMES = { export const GLOBAL_CLASS_NAMES = {
darkClassName: 'ray-template--dark', darkClassName: 'ray-template--dark',
lightClassName: 'ray-template--light', lightClassName: 'ray-template--light',
} rayTemplateThemePrimaryColor: '--r-theme-primary-color',
rayTemplateThemePrimaryFadeColor: '--r-theme-primary-fade-color',
preLoadingAnimation: 'pre-loading-animation',
htmlHeight: '--html-height',
htmlWidth: '--html-width',
} as const
/** /**
* *
@ -85,6 +93,8 @@ export const APP_CATCH_KEY_PREFIX = ''
* - appPiniaMenuStore: pinia menu store key * - appPiniaMenuStore: pinia menu store key
* - appPiniaSigningStore: pinia signing store key * - appPiniaSigningStore: pinia signing store key
* - appVersionProvider: 版本信息缓存 key * - appVersionProvider: 版本信息缓存 key
* - appMenuTagOptions: 标签页菜单列表
* - appLockScreenPasswordKey: 锁屏密码缓存 key
*/ */
export const APP_CATCH_KEY = { export const APP_CATCH_KEY = {
signing: 'signing', signing: 'signing',
@ -98,6 +108,8 @@ export const APP_CATCH_KEY = {
appVersionProvider: 'appVersionProvider', appVersionProvider: 'appVersionProvider',
isAppLockScreen: 'isAppLockScreen', isAppLockScreen: 'isAppLockScreen',
appGlobalSearchOptions: 'appGlobalSearchOptions', appGlobalSearchOptions: 'appGlobalSearchOptions',
appMenuTagOptions: 'appMenuTagOptions',
appLockScreenPasswordKey: 'appLockScreenPasswordKey',
} as const } as const
/** /**

View File

@ -3,8 +3,9 @@ import type { AppTheme } from '@/types'
export const APP_THEME: AppTheme = { export const APP_THEME: AppTheme = {
/** /**
* *
* * @description
* RGBARGB *
* RGBARGB
*/ */
appThemeColors: [ appThemeColors: [
'#2d8cf0', '#2d8cf0',
@ -12,25 +13,26 @@ export const APP_THEME: AppTheme = {
'#ff42bc', '#ff42bc',
'#ee4f12', '#ee4f12',
'#dbcb02', '#dbcb02',
'#18A058', '#18a058',
], ],
/** 系统主题色 */ // 系统主题色
appPrimaryColor: { appPrimaryColor: {
/** 主题色 */ // 主题色
primaryColor: '#2d8cf0', primaryColor: '#2d8cf0',
/** 主题辅助色(用于整体 hover、active 等之类颜色) */ // 主题辅助色(用于整体 hover、active 等之类颜色)
primaryFadeColor: 'rgba(45, 140, 240, 0.3)', primaryFadeColor: 'rgba(45, 140, 240, 0.85)',
}, },
/** /**
* *
* naive-ui * @description
* : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme> * naive-ui
* : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>。
* *
* : * :
* - appPrimaryColor common * - appPrimaryColor common
* *
* , *
* , peers * peers
* : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme#%E4%BD%BF%E7%94%A8-peers-%E4%B8%BB%E9%A2%98%E5%8F%98%E9%87%8F> * : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme#%E4%BD%BF%E7%94%A8-peers-%E4%B8%BB%E9%A2%98%E5%8F%98%E9%87%8F>
* *
* @example * @example
@ -46,17 +48,24 @@ export const APP_THEME: AppTheme = {
* ``` * ```
*/ */
appNaiveUIThemeOverrides: { appNaiveUIThemeOverrides: {
dark: {}, dark: {
light: {}, common: {
borderRadius: '4px',
baseColor: 'rgba(18, 18, 18, 1)',
},
},
light: {
common: {
borderRadius: '4px',
baseColor: 'rgba(255, 255, 255, 1)',
},
}, },
appNaiveUIThemeOverridesCommon: {
dark: {},
light: {},
}, },
/** /**
* *
* echart * @description
* json xxx-dark.json * echart
* json xxx-dark.json
* *
* [](https://xiaodaigua-ray.github.io/ray-template-doc/ray-template-docs/advanced/echart-themes.html) * [](https://xiaodaigua-ray.github.io/ray-template-doc/ray-template-docs/advanced/echart-themes.html)
*/ */

View File

@ -4,7 +4,7 @@ import type { Ref } from 'vue'
/** /**
* *
* @description * @description
* ref * shallowRef
* *
* , 使(scrollTo) * , 使(scrollTo)
* *
@ -16,12 +16,12 @@ import type { Ref } from 'vue'
* }) * })
*/ */
export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> = export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> =
ref<LayoutInst | null>(null) shallowRef<LayoutInst | null>(null)
/** /**
* *
* @description * @description
* ref * shallowRef
* *
* *
* 使使 nextTick() dom * 使使 nextTick() dom
@ -31,7 +31,7 @@ export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> =
* }) * })
*/ */
export const LAYOUT_SIDER_REF: Readonly<Ref<LayoutInst | null>> = export const LAYOUT_SIDER_REF: Readonly<Ref<LayoutInst | null>> =
ref<LayoutInst | null>(null) shallowRef<LayoutInst | null>(null)
export const SETUP_ROUTER_ACTION = { export const SETUP_ROUTER_ACTION = {
/** 是否启用路由切换时顶部加载条 */ /** 是否启用路由切换时顶部加载条 */

View File

@ -1,7 +1,7 @@
import { useAxiosInterceptor } from '@/axios/utils/interceptor' import { axiosInterceptor } from '@/axios/utils/interceptor'
import implement from './provider' import implement from './provider'
const { setImplement } = useAxiosInterceptor() const { setImplement } = axiosInterceptor()
export const setupRequestInterceptor = () => { export const setupRequestInterceptor = () => {
const { implementRequestInterceptorArray } = implement const { implementRequestInterceptorArray } = implement

View File

@ -16,7 +16,7 @@ import type {
* request instance , * request instance ,
*/ */
const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => { const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => {
const token = getStorage<string>(APP_CATCH_KEY.token, 'localStorage') const token = getStorage<string | null>(APP_CATCH_KEY.token, 'localStorage')
if (ins.url) { if (ins.url) {
// TODO: 根据 url 不同是否设置 token // TODO: 根据 url 不同是否设置 token

View File

@ -1,7 +1,7 @@
import { useAxiosInterceptor } from '@/axios/utils/interceptor' import { axiosInterceptor } from '@/axios/utils/interceptor'
import implement from './provider' import implement from './provider'
const { setImplement } = useAxiosInterceptor() const { setImplement } = axiosInterceptor()
export const setupResponseInterceptor = () => { export const setupResponseInterceptor = () => {
const { implementResponseInterceptorArray } = implement const { implementResponseInterceptorArray } = implement

View File

@ -40,14 +40,14 @@ function useRequest<
fetchOptions: AppRawRequestConfig<Response>, fetchOptions: AppRawRequestConfig<Response>,
option?: UseRequestOptions<Response, HookPlusParams, HookPlusPlugin>, option?: UseRequestOptions<Response, HookPlusParams, HookPlusPlugin>,
) { ) {
const fc = () => { const fn = () => {
const cb = request<Response>(fetchOptions) const cb = request<Response>(fetchOptions)
return cb return cb
} }
const hooks = useHookPlusRequest<Response, HookPlusParams>( const hooks = useHookPlusRequest<Response, HookPlusParams>(
fc, fn,
Object.assign({}, option), Object.assign({}, option),
) )

View File

@ -1,13 +1,6 @@
/**
*
*
* , inject
*
*/
import axios from 'axios' import axios from 'axios'
import { AXIOS_CONFIG } from '@/app-config' import { AXIOS_CONFIG } from '@/app-config'
import { useAxiosInterceptor } from '@/axios/utils/interceptor' import { axiosInterceptor } from '@/axios/utils/interceptor'
import { import {
setupResponseInterceptor, setupResponseInterceptor,
setupResponseErrorInterceptor, setupResponseErrorInterceptor,
@ -17,23 +10,33 @@ import {
setupRequestErrorInterceptor, setupRequestErrorInterceptor,
} from '@/axios/axios-interceptor/request' } from '@/axios/axios-interceptor/request'
import type { AxiosInstanceExpand } from './types' import type { AxiosInstanceExpand, RequestInterceptorConfig } from './types'
// 创建 axios 实例
const server: AxiosInstanceExpand = axios.create(AXIOS_CONFIG) const server: AxiosInstanceExpand = axios.create(AXIOS_CONFIG)
const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor() // 获取拦截器实例
const { createAxiosInstance, beforeFetch, fetchError } = axiosInterceptor()
// 请求拦截器 // 请求拦截器
server.interceptors.request.use( server.interceptors.request.use(
(request) => { (request) => {
createAxiosInstance(request, 'requestInstance') // 生成 request instance // 生成 request instance
setupRequestInterceptor() // 初始化拦截器所有已注入方法 createAxiosInstance(
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') // 执行拦截器所有已注入方法 request as RequestInterceptorConfig<unknown>,
'requestInstance',
)
// 初始化拦截器所有已注入方法
setupRequestInterceptor()
// 执行拦截器所有已注入方法
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok')
return request return request
}, },
(error) => { (error) => {
setupRequestErrorInterceptor() // 初始化拦截器所有已注入方法(错误状态) // 初始化拦截器所有已注入方法(错误状态)
fetchError('requestError', error, 'implementRequestInterceptorErrorArray') // 执行所有已注入方法 setupRequestErrorInterceptor()
// 执行所有已注入方法
fetchError('requestError', error, 'implementRequestInterceptorErrorArray')
return Promise.reject(error) return Promise.reject(error)
}, },
@ -42,17 +45,22 @@ server.interceptors.request.use(
// 响应拦截器 // 响应拦截器
server.interceptors.response.use( server.interceptors.response.use(
(response) => { (response) => {
createAxiosInstance(response, 'responseInstance') // 创建响应实例 // 创建响应实例
setupResponseInterceptor() // 注入响应成功待执行队列 createAxiosInstance(response, 'responseInstance')
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') // 执行响应成功拦截器 // 注入响应成功待执行队列
setupResponseInterceptor()
// 执行响应成功拦截器
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok')
const { data } = response const { data } = response
return Promise.resolve(data) return Promise.resolve(data)
}, },
(error) => { (error) => {
setupResponseErrorInterceptor() // 注入响应失败待执行队列 // 注入响应失败待执行队列
fetchError('responseError', error, 'implementResponseInterceptorErrorArray') // 执行响应失败后拦截器 setupResponseErrorInterceptor()
// 执行响应失败后拦截器
fetchError('responseError', error, 'implementResponseInterceptorErrorArray')
return Promise.reject(error) return Promise.reject(error)
}, },

View File

@ -1,116 +1,129 @@
/**
*
*
*
*
*/
import type { AppRawRequestConfig, CancelerParams } from '@/axios/types' import type { AppRawRequestConfig, CancelerParams } from '@/axios/types'
/** /**
*
* @class RequestCanceler
* *
* @description * @description
* signal *
* generateRequestKey key
* *
* cancelConfig.cancel true * HTTP
* __CANCELER_TAG_RAY_TEMPLATE__ * - AbortController signal
* - key
* -
*
* @example
* ```ts
* const canceler = new RequestCanceler()
*
* // 添加请求到待处理队列
* canceler.addPendingRequest(config)
*
* // 移除并取消特定请求
* canceler.removePendingRequest(config)
*
* // 取消所有待处理请求
* canceler.cancelAllRequest()
* ```
*/ */
export default class RequestCanceler { export default class RequestCanceler {
private pendingRequest: Map<string, AbortController> /** 待处理请求的 Mapkey 为请求标识value 为 AbortController */
private readonly pendingRequest = new Map<string, AbortController>()
constructor() {
this.pendingRequest = new Map<string, AbortController>()
}
/** /**
* *
* @param config config * @param config -
*
* @returns true
* *
* @description * @description
* signal *
*
* cancelConfig false signal
* cancelConfig true signal
*
* @example
* const bool = isAppending(config) // true or false
*/ */
private isAppending(config: AppRawRequestConfig | CancelerParams) { private shouldAddCanceler(
config: AppRawRequestConfig | CancelerParams,
): boolean {
return config.cancelConfig?.cancel ?? true return config.cancelConfig?.cancel ?? true
} }
/** /**
* *
* @param config config * @param config -
*
* @returns
* *
* @description * @description
* key * URL key
*
* @example
* const key = generateRequestKey(config) // string
*/ */
private generateRequestKey(config: AppRawRequestConfig | CancelerParams) { private generateRequestKey(
const { method, url } = config config: AppRawRequestConfig | CancelerParams,
): string {
const { method = '', url = '', params, data } = config
return [ return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
url || '',
method || '',
JSON.stringify(config.params),
JSON.stringify(config.data),
].join('&')
} }
/** /**
*
* @param config axios request config * @param config - Axios
*
* @description * @description
* pendingRequest map * pendingRequest map
* signal * signal
* *
* cancelConfig.cancel false * cancelConfig.cancel false
* __CANCELER_TAG_RAY_TEMPLATE__ * __CANCELER_TAG_RAY_TEMPLATE__
*
* @example * @example
* addPendingRequest(config) * ```ts
* // 默认启用取消功能
* canceler.addPendingRequest(config)
*
* // 禁用取消功能
* canceler.addPendingRequest({
* ...config,
* cancelConfig: { cancel: false }
* })
* ```
*/ */
addPendingRequest(config: AppRawRequestConfig | CancelerParams) { addPendingRequest(config: AppRawRequestConfig | CancelerParams): void {
if (this.isAppending(config)) { if (!this.shouldAddCanceler(config)) {
return
}
config.__CANCELER_TAG_RAY_TEMPLATE__ = '__CANCELER_TAG_RAY_TEMPLATE__' config.__CANCELER_TAG_RAY_TEMPLATE__ = '__CANCELER_TAG_RAY_TEMPLATE__'
const requestKey = this.generateRequestKey(config) const requestKey = this.generateRequestKey(config)
const existingController = this.pendingRequest.get(requestKey)
if (!this.pendingRequest.has(requestKey)) { if (existingController) {
// 复用现有的 signal
config.signal = existingController.signal
} else {
// 创建新的 AbortController
const controller = new AbortController() const controller = new AbortController()
config.signal = controller.signal config.signal = controller.signal
this.pendingRequest.set(requestKey, controller) this.pendingRequest.set(requestKey, controller)
} else {
// 如果已经有该 key 则重新挂载 signal
config.signal = this.pendingRequest.get(requestKey)?.signal
}
} }
} }
/** /**
* *
* @param config axios request config * @param config - Axios
* *
* @description * @description
* pendingRequest map *
* abort()
* *
* @example * @example
* removePendingRequest(config) * ```ts
* canceler.removePendingRequest(config)
* ```
*/ */
removePendingRequest(config: AppRawRequestConfig | CancelerParams) { removePendingRequest(config: AppRawRequestConfig | CancelerParams): void {
const requestKey = this.generateRequestKey(config) const requestKey = this.generateRequestKey(config)
const controller = this.pendingRequest.get(requestKey)
if (this.pendingRequest.has(requestKey)) { if (controller) {
this.pendingRequest.get(requestKey)!.abort() controller.abort()
this.pendingRequest.delete(requestKey) this.pendingRequest.delete(requestKey)
} }
} }
@ -118,17 +131,50 @@ export default class RequestCanceler {
/** /**
* *
* @description * @description
* pendingRequest map *
* *
* *
* cancelConfig.cancel false *
*
*
* `cancelConfig.cancel = false`
* *
* @example * @example
* cancelAllRequest() * ```ts
* // 在路由切换或组件卸载时取消所有请求
* canceler.cancelAllRequest()
* ```
*/ */
cancelAllRequest() { cancelAllRequest(): void {
this.pendingRequest.forEach((curr) => { this.pendingRequest.forEach((controller) => {
curr.abort() controller.abort()
}) })
this.pendingRequest.clear()
}
/**
*
* @returns
*
* @description
*
*/
getPendingCount(): number {
return this.pendingRequest.size
}
/**
*
* @param config -
*
* @returns
*
* @description
*
*/
hasPendingRequest(config: AppRawRequestConfig | CancelerParams): boolean {
const requestKey = this.generateRequestKey(config)
return this.pendingRequest.has(requestKey)
} }
} }

View File

@ -1,14 +1,3 @@
/**
*
* axios
*
*
*
*
* axios ,
* 使,
*/
import RequestCanceler from '@/axios/utils/RequestCanceler' import RequestCanceler from '@/axios/utils/RequestCanceler'
import { getAppEnvironment } from '@/utils' import { getAppEnvironment } from '@/utils'
@ -24,109 +13,172 @@ import type {
import type { AnyFC } from '@/types' import type { AnyFC } from '@/types'
import type { AxiosError } from 'axios' import type { AxiosError } from 'axios'
/** 当前请求的实例 */ type ImplementKeys = keyof ImplementQueue
type ErrorImplementKeys = keyof ErrorImplementQueue
// 当前请求的实例
const axiosFetchInstance: AxiosFetchInstance = { const axiosFetchInstance: AxiosFetchInstance = {
requestInstance: null, requestInstance: null,
responseInstance: null, responseInstance: null,
} }
/** 请求失败返回值 */
// 请求失败返回值
const axiosFetchError: AxiosFetchError<AxiosError<unknown, unknown>> = { const axiosFetchError: AxiosFetchError<AxiosError<unknown, unknown>> = {
requestError: null, requestError: null,
responseError: null, responseError: null,
} }
/** 请求队列(区分 resolve 与 reject 状态) */
// 请求队列(区分 resolve 与 reject 状态)
const implement: ImplementQueue = { const implement: ImplementQueue = {
implementRequestInterceptorArray: [], implementRequestInterceptorArray: [],
implementResponseInterceptorArray: [], implementResponseInterceptorArray: [],
} }
// 请求失败队列
const errorImplement: ErrorImplementQueue = { const errorImplement: ErrorImplementQueue = {
implementRequestInterceptorErrorArray: [], implementRequestInterceptorErrorArray: [],
implementResponseInterceptorErrorArray: [], implementResponseInterceptorErrorArray: [],
} }
/** 取消器实例 */ // 取消器实例
export const axiosCanceler = new RequestCanceler() export const axiosCanceler = new RequestCanceler()
export const useAxiosInterceptor = () => { export const axiosInterceptor = () => {
/** 创建拦截器实例 */ /**
*
* @param instance -
* @param instanceKey -
*
* @description
*
*/
const createAxiosInstance = ( const createAxiosInstance = (
instance: RequestInterceptorConfig | ResponseInterceptorConfig, instance: RequestInterceptorConfig | ResponseInterceptorConfig,
instanceKey: keyof AxiosFetchInstance, instanceKey: keyof AxiosFetchInstance,
) => { ): void => {
instanceKey === 'requestInstance' if (instanceKey === 'requestInstance') {
? (axiosFetchInstance['requestInstance'] = axiosFetchInstance.requestInstance = instance as RequestInterceptorConfig
instance as RequestInterceptorConfig) } else {
: (axiosFetchInstance['responseInstance'] = axiosFetchInstance.responseInstance =
instance as ResponseInterceptorConfig) instance as ResponseInterceptorConfig
}
} }
/** 获取当前实例 */ /**
*
* @param instanceKey -
*
* @returns
*
* @description
*
*/
const getAxiosInstance = (instanceKey: keyof AxiosFetchInstance) => { const getAxiosInstance = (instanceKey: keyof AxiosFetchInstance) => {
return axiosFetchInstance[instanceKey] return axiosFetchInstance[instanceKey]
} }
/** 设置注入方法队列 */ /**
*
* @param key -
* @param func -
* @param fetchType - /
*
* @description
*
*/
const setImplement = ( const setImplement = (
key: keyof ImplementQueue | keyof ErrorImplementQueue, key: ImplementKeys | ErrorImplementKeys,
func: AnyFC[], func: AnyFC[],
fetchType: FetchType, fetchType: FetchType,
) => { ): void => {
fetchType === 'ok' if (fetchType === 'ok') {
? (implement[key as keyof ImplementQueue] = func) implement[key as ImplementKeys] = func
: (errorImplement[key as keyof ErrorImplementQueue] = func) } else {
errorImplement[key as ErrorImplementKeys] = func
}
} }
/** 获取队列中所有的所有拦截器方法 */ /**
*
* @param key -
* @param fetchType - /
*
* @returns
*
* @description
*
*/
const getImplement = ( const getImplement = (
key: keyof ImplementQueue | keyof ErrorImplementQueue, key: ImplementKeys | ErrorImplementKeys,
fetchType: FetchType, fetchType: FetchType,
): AnyFC[] => { ): AnyFC[] => {
return fetchType === 'ok' return fetchType === 'ok'
? implement[key as keyof ImplementQueue] ? implement[key as ImplementKeys]
: errorImplement[key as keyof ErrorImplementQueue] : errorImplement[key as ErrorImplementKeys]
} }
/** 队列执行器 */ /**
const implementer = (funcs: AnyFC[], ...args: any[]) => { *
if (Array.isArray(funcs)) { * @param funcs -
funcs.forEach((curr) => { * @param args -
if (typeof curr === 'function') { *
curr(...args) * @description
* -
*/
const executeQueue = (funcs: AnyFC[], ...args: unknown[]): void => {
funcs.forEach((func) => {
if (typeof func === 'function') {
func(...args)
} }
}) })
} }
}
/** 请求、响应前执行拦截器队列中的所有方法 */ /**
*
* @param key -
* @param implementKey -
* @param fetchType - /
*
* @description
*
*/
const beforeFetch = ( const beforeFetch = (
key: keyof AxiosFetchInstance, key: keyof AxiosFetchInstance,
implementKey: keyof ImplementQueue | keyof ErrorImplementQueue, implementKey: ImplementKeys | ErrorImplementKeys,
fetchType: FetchType, fetchType: FetchType,
) => { ): void => {
const funcArr =
fetchType === 'ok'
? implement[implementKey as keyof ImplementQueue]
: errorImplement[implementKey as keyof ErrorImplementQueue]
const instance = getAxiosInstance(key) const instance = getAxiosInstance(key)
if (!instance) {
return
}
const funcArr = getImplement(implementKey, fetchType)
const { MODE } = getAppEnvironment() const { MODE } = getAppEnvironment()
if (instance) { executeQueue(funcArr, instance, MODE)
implementer(funcArr, instance, MODE)
}
} }
/** 请求、响应错误时执行队列中所有方法 */ /**
*
* @param key -
* @param error -
* @param errorImplementKey -
*
* @description
*
*/
const fetchError = ( const fetchError = (
key: keyof AxiosFetchError, key: keyof AxiosFetchError,
error: AxiosError<unknown, unknown>, error: AxiosError<unknown, unknown>,
errorImplementKey: keyof ErrorImplementQueue, errorImplementKey: ErrorImplementKeys,
) => { ): void => {
axiosFetchError[key] = error axiosFetchError[key] = error
const funcArr = errorImplement[errorImplementKey] const funcArr = errorImplement[errorImplementKey]
const { MODE } = getAppEnvironment() const { MODE } = getAppEnvironment()
implementer(funcArr, error, MODE) executeQueue(funcArr, error, MODE)
} }
return { return {
@ -139,4 +191,4 @@ export const useAxiosInterceptor = () => {
} }
} }
export type UseAxiosInterceptor = ReturnType<typeof useAxiosInterceptor> export type AxiosInterceptor = ReturnType<typeof axiosInterceptor>

View File

@ -2,7 +2,22 @@ import RBarcode from './src/Barcode'
import barcodeProps from './src/props' import barcodeProps from './src/props'
import type { ExtractPublicPropTypes } from 'vue' import type { ExtractPublicPropTypes } from 'vue'
import type { RBarcodeSize } from './src/types'
export type BarcodeProps = ExtractPublicPropTypes<typeof barcodeProps> // 扩展 BarcodeProps 以提供更好的类型提示
export type BarcodeProps = Omit<
ExtractPublicPropTypes<typeof barcodeProps>,
'width' | 'height'
> & {
width?: RBarcodeSize
height?: RBarcodeSize
}
export { RBarcode, barcodeProps } export { RBarcode, barcodeProps }
export type {
RBarcodeSize,
RBarcodeRender,
RBarcodeFormat,
RBarcodeOptions,
} from './src/types'

View File

@ -6,8 +6,10 @@ import barcode from 'jsbarcode'
import props from './props' import props from './props'
import { completeSize, call } from '@/utils' import { completeSize, call } from '@/utils'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import { useResizeObserver } from '@vueuse/core'
import type { WatchStopHandle } from 'vue' import type { WatchStopHandle } from 'vue'
import type { UseResizeObserverReturn } from '@vueuse/core'
export default defineComponent({ export default defineComponent({
name: 'RBarcode', name: 'RBarcode',
@ -16,10 +18,25 @@ export default defineComponent({
const barcodeRef = useTemplateRef<HTMLCanvasElement | HTMLOrSVGElement>( const barcodeRef = useTemplateRef<HTMLCanvasElement | HTMLOrSVGElement>(
'barcodeRef', 'barcodeRef',
) )
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const containerSize = ref({ width: 0, height: 0 })
let resizeObserverReturn: UseResizeObserverReturn | null
const cssVars = computed(() => { const cssVars = computed(() => {
let width = completeSize(props.width)
let height = completeSize(props.height)
if (props.width === 'responsive' && containerSize.value.width > 0) {
width = `${containerSize.value.width}px`
}
if (props.height === 'responsive' && containerSize.value.height > 0) {
height = `${containerSize.value.height}px`
}
const cssVar = { const cssVar = {
'--r-barcode-width': completeSize(props.width), '--r-barcode-width': width,
'--r-barcode-height': completeSize(props.height), '--r-barcode-height': height,
} }
return cssVar return cssVar
@ -27,6 +44,10 @@ export default defineComponent({
let watchStop: WatchStopHandle let watchStop: WatchStopHandle
const barcodeRender = () => { const barcodeRender = () => {
if (!barcodeRef.value) {
return
}
try { try {
const { format, text, options, onSuccess } = props const { format, text, options, onSuccess } = props
@ -34,6 +55,20 @@ export default defineComponent({
format, format,
}) })
// 如果是响应式模式,根据容器尺寸调整条形码选项
if (containerSize.value.width > 0) {
if (props.width === 'responsive') {
assignOptions.width = Math.max(1, containerSize.value.width / 100)
}
if (props.height === 'responsive') {
assignOptions.height = Math.max(
20,
containerSize.value.height * 0.8,
)
}
}
barcode( barcode(
barcodeRef.value, barcodeRef.value,
text !== void 0 && text !== null ? text.toString() : '', text !== void 0 && text !== null ? text.toString() : '',
@ -60,26 +95,64 @@ export default defineComponent({
watchEffect(() => { watchEffect(() => {
if (props.watchText) { if (props.watchText) {
watchStop?.()
watchStop = watch(() => props.text, barcodeRender) watchStop = watch(() => props.text, barcodeRender)
} else { } else {
watchStop?.() watchStop?.()
} }
// 监听容器尺寸变化
if (props.responsive) {
resizeObserverReturn?.stop()
resizeObserverReturn = useResizeObserver(
containerRef,
(entries: readonly ResizeObserverEntry[]) => {
const entry = entries[0]
if (entry) {
const { width, height } = entry.contentRect
containerSize.value = { width, height }
nextTick(() => {
barcodeRender()
})
}
},
)
} else {
resizeObserverReturn?.stop()
resizeObserverReturn = null
}
}) })
onMounted(() => { onMounted(() => {
// 初始化容器尺寸
if (containerRef.value) {
const rect = containerRef.value.getBoundingClientRect()
containerSize.value = { width: rect.width, height: rect.height }
}
nextTick(() => {
barcodeRender() barcodeRender()
}) })
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
watchStop?.() watchStop?.()
}) })
return { return {
barcodeRef, barcodeRef,
containerRef,
cssVars, cssVars,
} }
}, },
render() { render() {
const { barcodeRender, loading, cssVars } = this const { barcodeRender, loading, cssVars, responsive } = this
const c = [ const c = [
'r-barcode', 'r-barcode',
{ {
@ -87,12 +160,21 @@ export default defineComponent({
}, },
] ]
return ( const barcodeElement =
<NSpin class="r-barcode-spin" show={loading}> barcodeRender === 'canvas' ? (
{barcodeRender === 'canvas' ? (
<canvas class={c} style={cssVars} ref="barcodeRef" /> <canvas class={c} style={cssVars} ref="barcodeRef" />
) : ( ) : (
<svg class={c} style={cssVars} ref="barcodeRef" /> <svg class={c} style={cssVars} ref="barcodeRef" />
)
return (
<NSpin class="r-barcode-spin" show={loading}>
{responsive ? (
<div class="r-barcode-container" ref="containerRef">
{barcodeElement}
</div>
) : (
barcodeElement
)} )}
</NSpin> </NSpin>
) )

View File

@ -9,8 +9,27 @@
} }
} }
.r-barcode-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.r-barcode {
width: 100%;
height: 100%;
}
}
.r-barcode-spin, .r-barcode-spin,
.r-barcode-spin .n-spin-content { .r-barcode-spin .n-spin-content {
width: max-content !important; width: max-content !important;
height: max-content !important; height: max-content !important;
} }
.r-barcode-spin:has(.r-barcode-container),
.r-barcode-spin:has(.r-barcode-container) .n-spin-content {
width: 100% !important;
height: 100% !important;
}

View File

@ -1,4 +1,9 @@
import type { RBarcodeRender, RBarcodeOptions, RBarcodeFormat } from './types' import type {
RBarcodeRender,
RBarcodeOptions,
RBarcodeFormat,
RBarcodeSize,
} from './types'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { MaybeArray } from '@/types' import type { MaybeArray } from '@/types'
@ -7,23 +12,39 @@ const props = {
* *
* @description * @description
* *
* - px
* - 'auto'
* - CSS '100%', '200px'
* *
* @default 'auto' * @default 'auto'
* @example
* width={200}
* width="auto"
* width="responsive"
* width="100%"
*/ */
width: { width: {
type: [String, Number] as PropType<string | number>, type: [String, Number] as PropType<RBarcodeSize>,
default: 'auto', default: 'auto' as const,
}, },
/** /**
* *
* @description * @description
* *
* - px
* - 'auto'
* - CSS '100%', '200px'
* *
* @default 'auto' * @default 'auto'
* @example
* height={100}
* height="auto"
* height="responsive"
* height="50%"
*/ */
height: { height: {
type: [String, Number] as PropType<string | number>, type: [String, Number] as PropType<RBarcodeSize>,
default: 'auto', default: 'auto' as const,
}, },
/** /**
* *
@ -132,6 +153,19 @@ const props = {
onFinally: { onFinally: {
type: [Function, Array] as PropType<MaybeArray<() => void>>, type: [Function, Array] as PropType<MaybeArray<() => void>>,
}, },
/**
*
* @description
*
*
* width height
*
* @default false
*/
responsive: {
type: Boolean,
default: false,
},
} as const } as const
export default props export default props

View File

@ -24,3 +24,6 @@ export type RBarcodeFormat =
| 'MSI1110' | 'MSI1110'
| 'pharmacode' | 'pharmacode'
| 'codabar' | 'codabar'
// 使用模板字面量类型来保留字面量提示
export type RBarcodeSize = number | 'auto' | (string & {})

View File

@ -1,4 +1,4 @@
import RChart from './src' import RChart from './src/Chart'
import chartProps from './src/props' import chartProps from './src/props'
import useChart from './src/hooks/useChart' import useChart from './src/hooks/useChart'

View File

@ -19,7 +19,7 @@ import {
ScatterChart, ScatterChart,
PictorialBarChart, PictorialBarChart,
} from 'echarts/charts' // 系列类型(后缀都为 SeriesOption) } from 'echarts/charts' // 系列类型(后缀都为 SeriesOption)
import { LabelLayout, UniversalTransition } from 'echarts/features' // 标签自动布局, 全局过渡动画等特性 import { LegacyGridContainLabel, UniversalTransition } from 'echarts/features' // 标签自动布局, 全局过渡动画等特性
import { CanvasRenderer } from 'echarts/renderers' // echarts 渲染器 import { CanvasRenderer } from 'echarts/renderers' // echarts 渲染器
import { NCard } from 'naive-ui' import { NCard } from 'naive-ui'
@ -36,6 +36,7 @@ import {
import { RMoreDropdown } from '@/components' import { RMoreDropdown } from '@/components'
import { useSettingGetters } from '@/store' import { useSettingGetters } from '@/store'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import { USE_CHART_PROVIDER_KEY } from './config'
import type { WatchStopHandle } from 'vue' import type { WatchStopHandle } from 'vue'
import type { AnyFC } from '@/types' import type { AnyFC } from '@/types'
@ -87,7 +88,7 @@ export default defineComponent({
setup(props, { expose }) { setup(props, { expose }) {
const { getAppTheme } = useSettingGetters() const { getAppTheme } = useSettingGetters()
// echart 容器实例 // echart 容器实例
const rayChartRef = useTemplateRef<HTMLElement>('rayChartRef') const chartRef = useTemplateRef<HTMLElement>('chartRef')
// echart 父容器实例 // echart 父容器实例
const rayChartWrapperRef = useTemplateRef<HTMLElement>('rayChartWrapperRef') const rayChartWrapperRef = useTemplateRef<HTMLElement>('rayChartWrapperRef')
// echart 实例 // echart 实例
@ -112,8 +113,8 @@ export default defineComponent({
]) ])
const cssVarsRef = computed(() => { const cssVarsRef = computed(() => {
return { return {
'--ray-chart-width': completeSize(props.width), '--r-chart-width': completeSize(props.width),
'--ray-chart-height': completeSize(props.height), '--r-chart-height': completeSize(props.height),
} }
}) })
// 目标是否可见 // 目标是否可见
@ -124,6 +125,7 @@ export default defineComponent({
const __catch = { const __catch = {
aria: props.showAria, aria: props.showAria,
} }
const chartProvideOptions = inject(USE_CHART_PROVIDER_KEY, {})
/** /**
* *
@ -152,7 +154,7 @@ export default defineComponent({
ScatterChart, ScatterChart,
PictorialBarChart, PictorialBarChart,
]) // 注册 chart series type ]) // 注册 chart series type
use([LabelLayout, UniversalTransition]) // 注册布局, 过度效果 use([LegacyGridContainLabel, UniversalTransition]) // 注册布局, 过度效果
use([CanvasRenderer]) // 注册渲染器 use([CanvasRenderer]) // 注册渲染器
try { try {
@ -174,10 +176,19 @@ export default defineComponent({
* echartTheme 使 * echartTheme 使
*/ */
const updateChartTheme = () => { const updateChartTheme = () => {
const { theme: providerTheme } = chartProvideOptions || {}
if (echartInstanceRef.value) { if (echartInstanceRef.value) {
destroyChart() destroyChart()
} }
// 如果配置了全局配置主题,则忽略后面所有逻辑
if (providerTheme) {
renderChart(providerTheme)
return
}
if (props.theme === 'default') { if (props.theme === 'default') {
props.autoChangeTheme ? renderChart('dark') : renderChart('') props.autoChangeTheme ? renderChart('dark') : renderChart('')
@ -230,7 +241,7 @@ export default defineComponent({
*/ */
const renderChart = (theme: string = echartTheme) => { const renderChart = (theme: string = echartTheme) => {
// 获取 dom 容器 // 获取 dom 容器
const element = rayChartRef.value as HTMLElement const element = chartRef.value as HTMLElement
// 获取配置项 // 获取配置项
const options = combineChartOptions(props.options) const options = combineChartOptions(props.options)
// 获取 dom 容器实际宽高 // 获取 dom 容器实际宽高
@ -253,7 +264,7 @@ export default defineComponent({
// 是否强制下一队列渲染图表 // 是否强制下一队列渲染图表
if (props.nextTick) { if (props.nextTick) {
echartInstanceRef.value.setOption({}) // echartInstanceRef.value.setOption({})
nextTick(() => { nextTick(() => {
options && echartInstanceRef.value?.setOption(options) options && echartInstanceRef.value?.setOption(options)
@ -424,6 +435,8 @@ export default defineComponent({
watchEffect(() => { watchEffect(() => {
// 是否启用了可视区域监听 // 是否启用了可视区域监听
if (props.intersectionObserver) { if (props.intersectionObserver) {
intersectionObserverReturn?.stop()
intersectionObserverReturn = useIntersectionObserver( intersectionObserverReturn = useIntersectionObserver(
props.intersectionObserverTarget || rayChartWrapperRef, props.intersectionObserverTarget || rayChartWrapperRef,
([entry]) => { ([entry]) => {
@ -431,10 +444,14 @@ export default defineComponent({
}, },
props.intersectionOptions, props.intersectionOptions,
) )
} else {
intersectionObserverReturn?.stop()
} }
// 监听 options 变化 // 监听 options 变化
if (props.watchOptions) { if (props.watchOptions) {
watchThrottledCallback?.()
watchThrottledCallback = watchThrottled( watchThrottledCallback = watchThrottled(
() => props.options, () => props.options,
(ndata) => { (ndata) => {
@ -451,7 +468,7 @@ export default defineComponent({
}, },
{ {
// 深度监听 options // 深度监听 options
deep: true, deep: props.watchDeep,
throttle: props.watchOptionsThrottleWait, throttle: props.watchOptionsThrottleWait,
}, },
) )
@ -495,7 +512,7 @@ export default defineComponent({
}) })
return { return {
rayChartRef, chartRef,
cssVarsRef, cssVarsRef,
rayChartWrapperRef, rayChartWrapperRef,
moreDropDownOptions, moreDropDownOptions,
@ -525,7 +542,7 @@ export default defineComponent({
> >
{{ {{
default: renderNode( default: renderNode(
<div class="ray-chart__container" ref="rayChartRef"></div>, <div class="ray-chart__container" ref="chartRef"></div>,
), ),
header: renderNode(title, { header: renderNode(title, {
defaultElement: <div style="display: none;"></div>, defaultElement: <div style="display: none;"></div>,
@ -546,7 +563,7 @@ export default defineComponent({
</NCard> </NCard>
) : ( ) : (
<div class="ray-chart" style={[this.cssVarsRef]} ref="rayChartWrapperRef"> <div class="ray-chart" style={[this.cssVarsRef]} ref="rayChartWrapperRef">
<div class="ray-chart__container" ref="rayChartRef"></div> <div class="ray-chart__container" ref="chartRef"></div>
</div> </div>
) )
}, },

View File

@ -0,0 +1,6 @@
import type { InjectionKey } from 'vue'
import type { ChartProviderOptions } from './hooks/useChartProvider'
export const USE_CHART_PROVIDER_KEY: InjectionKey<
Partial<ChartProviderOptions>
> = Symbol('USE_CHART_PROVIDER_KEY')

View File

@ -0,0 +1,18 @@
import { USE_CHART_PROVIDER_KEY } from '../config'
import type { ChartTheme } from '../types'
export interface ChartProviderOptions {
theme: ChartTheme
}
/**
*
* @param
*
* @description
* chart
*/
export const useChartProvider = (options: Partial<ChartProviderOptions>) => {
provide(USE_CHART_PROVIDER_KEY, options)
}

View File

@ -1,6 +1,6 @@
.ray-chart { .ray-chart {
width: var(--ray-chart-width); width: var(--r-chart-width);
height: var(--ray-chart-height); height: var(--r-chart-height);
border: none; border: none;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;

View File

@ -341,6 +341,17 @@ const props = {
type: Number, type: Number,
default: 500, default: 500,
}, },
/**
*
* @description
* options
*
* @default true
*/
watchDeep: {
type: Boolean,
default: true,
},
/** /**
* *
* @description * @description

View File

@ -81,6 +81,7 @@ export default defineComponent({
yGap, yGap,
collapsedRows, collapsedRows,
cssVars, cssVars,
actionSpan,
bordered, bordered,
} = this } = this
@ -97,7 +98,11 @@ export default defineComponent({
collapsedRows={collapsedRows} collapsedRows={collapsedRows}
> >
{defaultSlot?.()} {defaultSlot?.()}
<NGridItem suffix class="ray-collapse-grid__suffix--btn"> <NGridItem
suffix
class="ray-collapse-grid__suffix--btn"
span={actionSpan}
>
<NFlex justify="end" align="center"> <NFlex justify="end" align="center">
{action?.()} {action?.()}
{collapse {collapse

View File

@ -5,6 +5,17 @@ import type { CollapseToggleText, ActionAlignType } from './types'
import type { AnyFC, MaybeArray } from '@/types' import type { AnyFC, MaybeArray } from '@/types'
const props = { const props = {
/**
*
* @description
*
*
* @default 1
*/
actionSpan: {
type: Number,
default: 1,
},
/** /**
* *
* @description * @description

View File

@ -0,0 +1,388 @@
import './index.scss'
import { NCard } from 'naive-ui'
import { Teleport, Transition } from 'vue'
import interact from 'interactjs'
import { cardProps } from 'naive-ui'
import { unrefElement, completeSize, queryElements } from '@/utils'
import type { VNode } from 'vue'
import type { MaybeElement, MaybeRefOrGetter } from '@vueuse/core'
import type { AnyFC } from '@/types'
type RestrictRectOptions = Parameters<typeof interact.modifiers.restrictRect>[0]
type Padding = {
x: number
y: number
}
export type DefaultPosition =
| Padding
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'center'
| 'top-center'
| 'bottom-center'
const props = {
...cardProps,
/**
*
* @description
*
*
* @default body
*/
restrictionElement: {
type: [String, HTMLElement, Function, Object] as PropType<
string | HTMLElement | (() => VNode) | MaybeRefOrGetter<MaybeElement>
>,
default: 'body',
},
/**
*
* @description
*
*
* @default true
*/
dad: {
type: Boolean,
default: true,
},
/**
*
* @description
*
*
* @default undefined
*/
restrictRectOptions: {
type: Object as PropType<RestrictRectOptions>,
default: void 0,
},
/**
*
* @description
*
*
* @default { x: 0, y: 0 }
*/
defaultPosition: {
type: [Object, String] as PropType<DefaultPosition>,
default: () => ({
x: 0,
y: 0,
}),
},
/**
*
* @description
*
*
* @default 600
*/
width: {
type: [String, Number] as PropType<string | number>,
default: 600,
},
/**
*
* @description
* z-index
*
* @default undefined
*/
zIndex: {
type: Number,
default: void 0,
},
/**
*
* @description
*
*
* @default false
*/
animation: {
type: Boolean,
default: false,
},
/**
*
* @description
*
*
*
* @default undefined
*/
padding: {
type: Object as PropType<Padding>,
default: void 0,
},
}
export default defineComponent({
name: 'RDraggableCard',
props,
setup(props, { expose }) {
const cardRef = useTemplateRef<HTMLElement>('cardRef')
let interactInst: ReturnType<typeof interact> | null = null
const position = {
x: 0,
y: 0,
}
const CONTAINER_ID = 'r-draggable-card-container'
const cssVars = computed(() => {
return {
'--r-draggable-card-width': completeSize(props.width),
'--r-draggable-card-z-index': props.zIndex,
}
})
let isSetup = false
const cacheProps = {
defaultPosition: props.defaultPosition,
dad: props.dad,
}
// 创建 DraggableCard 容器
const createDraggableCardContainer = () => {
if (!document.getElementById(CONTAINER_ID)) {
const container = document.createElement('div')
container.id = CONTAINER_ID
document.documentElement.appendChild(container)
}
}
createDraggableCardContainer()
// 获取 card, restrictionElement 的 dom 信息
const getDom = () => {
const card = unrefElement(cardRef)
const re =
typeof props.restrictionElement === 'string'
? queryElements<HTMLElement>(props.restrictionElement)
: props.restrictionElement
let restrictionElement: HTMLElement | null = null
if (Array.isArray(re)) {
restrictionElement = re[0]
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restrictionElement = unrefElement<HTMLElement>(re as any) as HTMLElement
}
return {
card,
restrictionElement,
}
}
// 获取 container, card 的位置
const getPosition = (containerRect: DOMRect, cardRect: DOMRect) => {
const { defaultPosition, padding } = props
const { x: paddingX = 0, y: paddingY = 0 } = padding ?? {}
// 默认的 body restrictionElement 的偏移量是 0
const {
x: containerX,
y: containerY,
width: containerWidth,
height: containerHeight,
} = containerRect
const { width: cardWidth, height: cardHeight } = cardRect
if (typeof defaultPosition === 'string') {
switch (defaultPosition) {
case 'top-center': {
const cx1 = (containerWidth - cardWidth) / 2 + containerX
const cy1 = paddingY + containerY
const cx2 = paddingX + cx1
const cy2 = cy1
return { x: cx2, y: cy2 }
}
case 'bottom-center': {
const cx1 = (containerWidth - cardWidth) / 2 + containerX
const cy1 = containerHeight - cardHeight - paddingY + containerY
const cx2 = paddingX + cx1
const cy2 = cy1
return { x: cx2, y: cy2 }
}
case 'center': {
const cx1 = (containerWidth - cardWidth) / 2 + containerX
const cy1 = (containerHeight - cardHeight) / 2 + containerY
const cx2 = paddingX + cx1
const cy2 = paddingY + cy1
return { x: cx2, y: cy2 }
}
case 'top-left':
return { x: paddingX + containerX, y: paddingY + containerY }
case 'top-right':
return {
x: containerWidth - cardWidth - paddingX + containerX,
y: paddingY + containerY,
}
case 'bottom-left':
return {
x: paddingX + containerX,
y: containerHeight - cardHeight - paddingY + containerY,
}
case 'bottom-right':
return {
x: containerWidth - cardWidth - paddingX + containerX,
y: containerHeight - cardHeight - paddingY + containerY,
}
// 默认为左上角
default:
return { x: paddingX + containerX, y: paddingY + containerY }
}
} else {
const { x: defaultX, y: defaultY } = defaultPosition
return {
x: defaultX + containerX + paddingX,
y: defaultY + containerY + paddingY,
}
}
}
// 初始化设置 card 的位置,并且根据配置启用拖拽
const setupDraggable = () => {
const { card, restrictionElement } = getDom()
if (!card) {
return
}
const restrictionRect = restrictionElement?.getBoundingClientRect()
const cardHeader = card.querySelector('.n-card-header')
const restrictRectOptions = Object.assign(
{},
{
restriction: restrictionElement,
endOnly: true,
},
props.restrictRectOptions,
)
if (restrictionRect && !isSetup) {
// 计算偏移位置
const p = getPosition(restrictionRect, card.getBoundingClientRect())
// 设置初始位置
card.style.transform = `translate(${p.x}px, ${p.y}px)`
position.x = p.x
position.y = p.y
}
if (!props.dad) {
return
}
interactInst = interact(card)
.draggable({
inertia: true,
autoScroll: true,
allowFrom: cardHeader ? '.n-card-header' : '.n-card__content',
modifiers: [interact.modifiers.restrictRect(restrictRectOptions)],
listeners: {
move: (event) => {
card.setAttribute('can-drag', 'true')
position.x += event.dx
position.y += event.dy
card.style.transform = `translate(${position.x}px, ${position.y}px)`
},
},
})
.resizable(false)
isSetup = true
}
// 取消拖拽
const resetDraggable = () => {
interactInst?.unset()
interactInst = null
}
// 更新拖拽
const refreshDraggableWhenPropsChange = (fn: AnyFC) => {
isSetup = false
fn()
setupDraggable()
}
expose()
watchEffect(() => {
props.dad ? setupDraggable() : resetDraggable()
if (props.defaultPosition !== cacheProps.defaultPosition) {
refreshDraggableWhenPropsChange(() => {
cacheProps.defaultPosition = props.defaultPosition
})
}
})
onMounted(() => {
nextTick(() => {
setupDraggable()
})
})
return {
cardRef,
CONTAINER_ID,
cssVars,
}
},
render() {
const { $attrs, $slots, $props, CONTAINER_ID, cssVars, animation } = this
return (
<Teleport to={`#${CONTAINER_ID}`}>
{animation ? (
<Transition name="draggable-card" appear mode="out-in">
<NCard
{...$attrs}
{...$props}
class="r-draggable-card"
style={[cssVars]}
ref="cardRef"
>
{{ ...$slots }}
</NCard>
</Transition>
) : (
<NCard
{...$attrs}
{...$props}
class="r-draggable-card"
style={[cssVars]}
ref="cardRef"
>
{{ ...$slots }}
</NCard>
)}
</Teleport>
)
},
})

View File

@ -0,0 +1,25 @@
.n-card.r-draggable-card {
transform-origin: 0 0;
position: absolute;
width: var(--r-draggable-card-width);
z-index: var(--r-draggable-card-z-index);
}
#r-draggable-card-container {
position: fixed;
top: 0;
left: 0;
width: 0;
height: 0;
}
// draggable-card Transition 样式
.draggable-card-enter-active,
.draggable-card-leave-active {
transition: opacity 0.3s var(--r-bezier);
}
.draggable-card-enter-from,
.draggable-card-leave-to {
opacity: 0;
}

View File

@ -1,17 +1,41 @@
import { NForm } from 'naive-ui' import { NForm, NSpin } from 'naive-ui'
import props from './props' import props from './props'
import { call } from '@/utils' import { call, unrefElement } from '@/utils'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import { useEventListener } from '@vueuse/core'
import type { RFormInst } from './types' import type { RFormInst } from './types'
import type { FormProps } from 'naive-ui' import type { ShallowRef } from 'vue'
export default defineComponent({ export default defineComponent({
name: 'RForm', name: 'RForm',
props, props,
setup(props, { expose }) { setup(props, { expose }) {
const formRef = useTemplateRef<RFormInst>('formRef') const formRef = useTemplateRef<RFormInst>('formRef')
const currentSubmitFn = computed(() => props.onFinish ?? Promise.resolve)
const bindKeydownListener = (e: KeyboardEvent) => {
const keyCode = e.code
if (keyCode === 'Enter') {
e.stopPropagation()
e.preventDefault()
formRef.value?.validate().then(currentSubmitFn.value)
}
}
if (props.submitWhenEnter) {
useEventListener(
formRef as unknown as ShallowRef<HTMLElement>,
'keydown',
bindKeydownListener,
{
capture: true,
},
)
}
onMounted(() => { onMounted(() => {
// 主动调用 register 方法,满足 useForm 方法正常调用 // 主动调用 register 方法,满足 useForm 方法正常调用
@ -20,6 +44,16 @@ export default defineComponent({
if (onRegister && formRef.value) { if (onRegister && formRef.value) {
call(onRegister, formRef.value) call(onRegister, formRef.value)
} }
if (formRef.value) {
const formElement = unrefElement(
formRef.value as unknown as HTMLFormElement,
)
if (formElement) {
formElement.autocomplete = props.autocomplete
}
}
}) })
expose() expose()
@ -30,13 +64,22 @@ export default defineComponent({
}, },
render() { render() {
const { $attrs, $props, $slots } = this const { $attrs, $props, $slots } = this
const { loading, loadingDescription, ...restProps } = $props
return ( return (
<NForm {...$attrs} {...($props as FormProps)} ref="formRef"> <NSpin
show={loading}
description={loadingDescription}
style={{
height: 'auto',
}}
>
<NForm {...$attrs} {...restProps} ref="formRef">
{{ {{
...$slots, ...$slots,
}} }}
</NForm> </NForm>
</NSpin>
) )
}, },
}) })

View File

@ -36,11 +36,15 @@ import type { Recordable } from '@/types'
* }, * },
* }) * })
*/ */
const useForm = <T extends Recordable, R extends RFormRules>( const useForm = <
model?: T, T extends Recordable = Recordable,
rules?: R, R extends RFormRules = RFormRules,
>(
model?: T | (() => T),
rules?: R | (() => R),
) => { ) => {
const formRef = ref<RFormInst>() const formRef = shallowRef<RFormInst>()
const formModelRef = ref<T>()
const register = (inst: RFormInst) => { const register = (inst: RFormInst) => {
if (inst) { if (inst) {
@ -58,6 +62,15 @@ const useForm = <T extends Recordable, R extends RFormRules>(
return formRef.value return formRef.value
} }
// 初始化 formModelRef 的值,根据 model 的类型进行初始化
const initialFormModel = () => {
if (typeof model === 'function') {
formModelRef.value = model() ?? ({} as T)
} else {
formModelRef.value = cloneDeep(model) ?? ({} as T)
}
}
/** /**
* *
* @description * @description
@ -83,10 +96,39 @@ const useForm = <T extends Recordable, R extends RFormRules>(
* *
* @description * @description
* *
*
* useForm model * useForm model
*
* vue
* Form
*
* 5.2.2 formConditionRef ref
* hook
*
* @example
*
* interface FormModel {
* name: string | null
* age: number | null
* }
*
* const [register, { formModel }] = useForm<FormModel>({
* name: null,
* age: null,
* })
*
* const formModelRef = ref(formModel())
*
* const reset = () => {
* formModelRef.value = formModel()
* }
*/ */
const formModel = () => cloneDeep(model) || ({} as T) const formModel = (): T & Recordable => {
if (typeof model === 'function') {
return model()
}
return cloneDeep(model) || ({} as T)
}
/** /**
* *
@ -95,7 +137,103 @@ const useForm = <T extends Recordable, R extends RFormRules>(
* *
* useForm rules * useForm rules
*/ */
const formRules = () => cloneDeep(rules) || ({} as R) const formRules = () => {
if (typeof rules === 'function') {
return rules()
}
return cloneDeep(rules) || ({} as R)
}
/**
*
* @param values
*
* @warning
* undefined
* reset 使 null
*
* @description
* useForm
*
*
*
* useForm
*
*/
const reset = <Values extends T = T>(values?: Values & Recordable) => {
formModelRef.value = Object.assign(
formModelRef.value as T,
formModel(),
values,
)
restoreValidation()
}
/**
*
* @param key key
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#partially-apply-rules.vue
*
* @description
*
*
* rules key
*
*
* @example
* const [register, { validateTargetField }] = useForm(
* {
* name: null,
* },
* {
* name: {
* required: true,
* message: 'name is required',
* trigger: ['blur', 'change'],
* type: 'string',
* key: 'name',
* },
* },
* )
*
* validateTargetField('name')
*/
const validateTargetField = (key: string) => {
if (!key || typeof key !== 'string') {
throw new Error(
`[useForm-validateTargetField]: except key is string, but got ${typeof key}.`,
)
}
return validate(void 0, (rules) => {
return rules?.key === key
})
}
/**
*
* @description
*
* 使
*
* @example
* const [register, { updateFormCondition }] = useForm(
* {
* name: null,
* },
* )
*
* updateFormCondition({
* name: 'John',
* })
*/
const updateFormCondition = (values: T & Recordable) => {
formModelRef.value = Object.assign(formModelRef.value as T, values)
}
initialFormModel()
return [ return [
register, register,
@ -105,6 +243,10 @@ const useForm = <T extends Recordable, R extends RFormRules>(
restoreValidation, restoreValidation,
formModel, formModel,
formRules, formRules,
reset,
validateTargetField,
formConditionRef: formModelRef as Ref<T>,
updateFormCondition,
}, },
] as const ] as const
} }

View File

@ -1,10 +1,75 @@
import { formProps } from 'naive-ui' import { formProps } from 'naive-ui'
import { omit } from 'lodash-es'
import type { MaybeArray } from '@/types' import type { MaybeArray, AnyFC } from '@/types'
import type { RFormInst } from './types' import type { RFormInst } from './types'
const props = { const props = {
...formProps, ...omit(formProps, ['onSubmit']),
/**
*
* @description
*
*
* @default false
*/
loading: {
type: Boolean,
default: false,
},
/**
*
* @description
*
*
* @default undefined
*/
loadingDescription: {
type: String,
default: void 0,
},
/**
*
* @description
*
*
* @default 'off'
*/
autocomplete: {
type: String as PropType<AutoFillBase>,
default: 'off',
},
/**
*
* @description
* onFinish
* onFinish 使
*
*
*
*
* Enter
* NSelect, NInput
*
*
* @default false
*/
submitWhenEnter: {
type: Boolean,
default: false,
},
/**
*
* @description
* submitWhenEnter true
* submitWhenEnter 使
*
* @default null
*/
onFinish: {
type: Function as PropType<AnyFC>,
default: null,
},
/** /**
* *
* @description * @description

View File

@ -1,4 +1,4 @@
import RIcon from './src' import RIcon from './src/Icon'
import iconProps from './src/props' import iconProps from './src/props'
import type { ExtractPublicPropTypes } from 'vue' import type { ExtractPublicPropTypes } from 'vue'

View File

@ -10,15 +10,15 @@ export default defineComponent({
const symbolId = computed(() => `#${props.prefix}-${props.name}`) const symbolId = computed(() => `#${props.prefix}-${props.name}`)
const cssVars = computed(() => { const cssVars = computed(() => {
const cssVar = { const cssVar = {
'--ray-icon-width': props.width '--r-icon-width': props.width
? completeSize(props.width) ? completeSize(props.width)
: completeSize(props.size), : completeSize(props.size),
'--ray-icon-height': props.height '--r-icon-height': props.height
? completeSize(props.height) ? completeSize(props.height)
: completeSize(props.size), : completeSize(props.size),
'--ray-icon-depth': props.depth, '--r-icon-depth': props.depth,
'--ray-icon-cursor': props.cursor, '--r-icon-cursor': props.cursor,
'--ray-icon-color': props.color, '--r-icon-color': props.color,
} }
return cssVar return cssVar
@ -41,13 +41,13 @@ export default defineComponent({
render() { render() {
return ( return (
<span <span
class={['ray-icon', this.customClassName]} class={['r-icon', this.customClassName]}
style={[this.cssVars]} style={[this.cssVars]}
onClick={this.iconClick.bind(this)} onClick={this.iconClick}
> >
<svg <svg
{...({ {...({
RayIconAttribute: 'ray-icon', RIconAttribute: 'r-icon',
ariaHidden: true, ariaHidden: true,
} as object)} } as object)}
> >

View File

@ -1,28 +1,28 @@
.ray-icon { .r-icon {
position: relative; position: relative;
width: var(--ray-icon-width); width: var(--r-icon-width);
height: var(--ray-icon-height); height: var(--r-icon-height);
border: none; border: none;
outline: none; outline: none;
text-align: center; text-align: center;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: var(--ray-icon-color); color: var(--r-icon-color);
transform: translateZ(0); transform: translateZ(0);
opacity: var(--ray-icon-depth); opacity: var(--r-icon-depth);
cursor: var(--ray-icon-cursor); cursor: var(--r-icon-cursor);
& svg[RayIconAttribute='ray-icon'] { & svg[RIconAttribute='r-icon'] {
width: var(--ray-icon-width); width: var(--r-icon-width);
height: var(--ray-icon-height); height: var(--r-icon-height);
fill: currentColor; fill: currentColor;
} }
} }
.ray-icon-path__animate { .r-icon-path__animate {
stroke-dasharray: var(--ray-icon-path-length); stroke-dasharray: var(--r-icon-path-length);
stroke-dashoffset: var(--ray-icon-path-length); stroke-dashoffset: var(--r-icon-path-length);
animation: rayIconPathAnimate 2s forwards; animation: rayIconPathAnimate 2s forwards;
} }

View File

@ -1,4 +1,4 @@
import RIframe from './src' import RIframe from './src/Iframe'
import iframeProps from './src/props' import iframeProps from './src/props'
import type * as RIframeType from './src/types' import type * as RIframeType from './src/types'

View File

@ -13,9 +13,9 @@ export default defineComponent({
setup(props, { expose }) { setup(props, { expose }) {
const cssVars = computed(() => { const cssVars = computed(() => {
const cssVar = { const cssVar = {
'--ray-iframe-frameborder': completeSize(props.frameborder), '--r-iframe-frameborder': completeSize(props.frameborder),
'--ray-iframe-width': completeSize(props.width), '--r-iframe-width': completeSize(props.width),
'--ray-iframe-height': completeSize(props.height), '--r-iframe-height': completeSize(props.height),
} }
return cssVar return cssVar

View File

@ -1,8 +1,8 @@
.ray-iframe { .ray-iframe {
width: var(--ray-iframe-width); width: var(--r-iframe-width);
height: var(--ray-iframe-height); height: var(--r-iframe-height);
box-sizing: border-box; box-sizing: border-box;
border: var(--ray-iframe-frameborder); border: var(--r-iframe-frameborder);
& .ray-iframe__container { & .ray-iframe__container {
width: 100%; width: 100%;

View File

@ -4,14 +4,12 @@ import { NModal } from 'naive-ui'
import props from './props' import props from './props'
import { completeSize, uuid } from '@/utils' import { completeSize, uuid } from '@/utils'
import { setupInteract } from './utils'
import { import {
FULLSCREEN_CARD_TYPE_CLASS, FULLSCREEN_CARD_TYPE_CLASS,
R_MODAL_CLASS, R_MODAL_CLASS,
CSS_VARS_KEYS, CSS_VARS_KEYS,
} from './constant' } from './constant'
import type interact from 'interactjs'
import type { ModalProps } from 'naive-ui' import type { ModalProps } from 'naive-ui'
export default defineComponent({ export default defineComponent({
@ -24,57 +22,11 @@ export default defineComponent({
[CSS_VARS_KEYS['dialogWidth']]: completeSize(props.dialogWidth ?? 446), [CSS_VARS_KEYS['dialogWidth']]: completeSize(props.dialogWidth ?? 446),
})) }))
const uuidEl = uuid() const uuidEl = uuid()
let intractable: null | ReturnType<typeof interact>
// 记录拖拽的位置
const position = {
x: 0,
y: 0,
}
// 当前是否为预设 card 类型并且设置了 fullscreen // 当前是否为预设 card 类型并且设置了 fullscreen
const isFullscreenCardType = computed( const isFullscreenCardType = computed(
() => props.preset === 'card' && props.fullscreen, () => props.preset === 'card' && props.fullscreen,
) )
watch(
() => props.show,
(ndata) => {
if (
ndata &&
props.dad &&
(props.preset === 'card' || props.preset === 'dialog')
) {
nextTick(() => {
const target = document.getElementById(uuidEl)
if (target) {
setupInteract(target, {
preset: props.preset,
x: position.x,
y: position.y,
dargCallback: (x, y) => {
position.x = x
position.y = y
},
}).then((res) => {
intractable = res
})
}
if (props.memo && target) {
target.style.transform = `translate(${position.x}px, ${position.y}px)`
}
})
} else {
intractable?.unset()
intractable = null
}
},
{
immediate: true,
},
)
return { return {
cssVars, cssVars,
isFullscreenCardType, isFullscreenCardType,

View File

@ -1,10 +1,18 @@
import { useModal as useNaiveModal, NScrollbar } from 'naive-ui' import { useModal as useNaiveModal, NScrollbar } from 'naive-ui'
import { setupInteract } from '../utils'
import { queryElements, setStyle, completeSize, setClass } from '@/utils' import { queryElements, setStyle, completeSize, setClass } from '@/utils'
import { R_MODAL_CLASS, CSS_VARS_KEYS } from '../constant' import { R_MODAL_CLASS, CSS_VARS_KEYS } from '../constant'
import type { RModalProps } from '../types' import type { RModalProps } from '../types'
/**
*
* @deprecated
*
* @see https://www.naiveui.com/zh-CN/dark/components/modal#useModal-API
*
* @description
* 使 `useModal`
*/
const useModal = () => { const useModal = () => {
const { create: naiveCreate, destroyAll: naiveDestroyAll } = useNaiveModal() const { create: naiveCreate, destroyAll: naiveDestroyAll } = useNaiveModal()
@ -21,10 +29,10 @@ const useModal = () => {
color: 'rgba(0, 0, 0, 0)', color: 'rgba(0, 0, 0, 0)',
colorHover: 'rgba(0, 0, 0, 0)', colorHover: 'rgba(0, 0, 0, 0)',
}, },
trigger: 'none', trigger: 'hover',
style: { style: {
width: 'auto', width: 'auto',
height: maxHeight:
'calc(var(--html-height) - 29px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))', 'calc(var(--html-height) - 29px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))',
}, },
}, },
@ -35,7 +43,7 @@ const useModal = () => {
) )
} }
const { preset, dad, fullscreen, width, cardWidth, dialogWidth } = options const { preset, fullscreen, width, cardWidth, dialogWidth } = options
const modalReactive = naiveCreate({ const modalReactive = naiveCreate({
...rest, ...rest,
content: contentNode, content: contentNode,
@ -55,15 +63,6 @@ const useModal = () => {
return return
} }
// 是否启用拖拽
if (dad) {
setupInteract(modalElement, {
preset,
x: 0,
y: 0,
})
}
// preset 为 cardfullscreen 为 true 时,最大化 modal // preset 为 cardfullscreen 为 true 时,最大化 modal
if (fullscreen && preset === 'card') { if (fullscreen && preset === 'card') {
setStyle(modalElement, { setStyle(modalElement, {

View File

@ -4,6 +4,11 @@
// 当设置全屏时启用滚动 // 当设置全屏时启用滚动
& .n-card__content { & .n-card__content {
overflow: scroll; overflow: scroll;
max-height: calc(
var(--html-height) - var(--n-padding-bottom) - var(--n-padding-bottom) - var(
--n-padding-top
)
);
} }
} }

View File

@ -3,17 +3,6 @@ import type { PropType } from 'vue'
const props = { const props = {
...modalProps, ...modalProps,
/**
*
* @description
*
*
* @default true
*/
memo: {
type: Boolean,
default: true,
},
/** /**
* *
* @description * @description
@ -58,18 +47,6 @@ const props = {
type: [String, Number] as PropType<string | number>, type: [String, Number] as PropType<string | number>,
default: 446, default: 446,
}, },
/**
*
* @description
*
* header
*
* @default false
*/
dad: {
type: Boolean,
default: false,
},
} }
export default props export default props

View File

@ -1,14 +1,6 @@
import type { ModalOptions as NaiveModalOptions } from 'naive-ui' import type { ModalOptions as NaiveModalOptions } from 'naive-ui'
export interface RModalProps extends NaiveModalOptions { export interface RModalProps extends NaiveModalOptions {
/**
*
* @description
*
*
* @default true
*/
memo?: boolean
/** /**
* *
* @description * @description
@ -41,13 +33,4 @@ export interface RModalProps extends NaiveModalOptions {
* @default 446 * @default 446
*/ */
dialogWidth?: number | string dialogWidth?: number | string
/**
*
* @description
*
* header
*
* @default false
*/
dad?: boolean
} }

View File

@ -1,101 +0,0 @@
import interact from 'interactjs'
import type { ModalProps } from 'naive-ui'
import type { RModalProps } from './types'
interface SetupDraggableOptions {
scheduler?: (event: Interact.DragEvent) => void
}
interface SetupInteractOptions {
preset: ModalProps['preset']
memo?: RModalProps['memo']
x: number
y: number
dargCallback?: (x: number, y: number, event: Interact.DragEvent) => void
}
/**
*
* @param bindModal modal
* @param preset
*
* @description
*
* card, dialog
*
* 30ms
*/
export const setupDraggable = (
bindModal: HTMLElement,
preset: ModalProps['preset'],
options?: SetupDraggableOptions,
): Promise<ReturnType<typeof interact>> => {
const { scheduler } = options ?? {}
return new Promise((resolve) => {
setTimeout(() => {
const allowFromStr =
preset === 'card' ? '.n-card-header__main' : '.n-dialog__title'
if (bindModal) {
const dad = interact(bindModal)
.draggable({
inertia: true,
autoScroll: true,
allowFrom: allowFromStr,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true,
}),
],
listeners: {
move: (event) => {
scheduler?.(event)
},
},
})
.resizable(false)
resolve(dad)
}
}, 30)
})
}
export const setupInteract = (
target: HTMLElement | string,
options: SetupInteractOptions,
): Promise<ReturnType<typeof interact>> => {
const _target =
typeof target === 'string'
? (document.querySelector(target) as HTMLElement)
: target
return new Promise((resolve, reject) => {
if (_target) {
_target.setAttribute('can-drag', 'true')
const { preset, dargCallback } = options
let { x, y } = options
setupDraggable(_target, preset, {
scheduler: (event) => {
const target = event.target
x += event.dx
y += event.dy
target.style.transform = `translate(${x}px, ${y}px)`
dargCallback?.(x, y, event)
},
}).then((res) => {
resolve(res)
})
} else {
reject()
}
})
}

View File

@ -1,4 +1,4 @@
import RMoreDropdown from './src' import RMoreDropdown from './src/MoreDropdown'
import moreDropdownProps from './src/props' import moreDropdownProps from './src/props'
import type { ExtractPublicPropTypes } from 'vue' import type { ExtractPublicPropTypes } from 'vue'

View File

@ -94,7 +94,6 @@ export default defineComponent({
return ( return (
<NTabs <NTabs
{...($props as TabsProps)} {...($props as TabsProps)}
ref="segmentRef"
style={[cssVars]} style={[cssVars]}
class="r-segment" class="r-segment"
type="segment" type="segment"

View File

@ -13,27 +13,25 @@ import { config } from './shared'
import { pick } from 'lodash-es' import { pick } from 'lodash-es'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import type { DropdownOption, DataTableInst, DataTableProps } from 'naive-ui' import type { DropdownOption, DataTableProps } from 'naive-ui'
import type { ComponentSize } from '@/types' import type { ComponentSize } from '@/types'
import type { import type {
C as CType, C as CType,
PropsComponentPopselectKeys, PropsComponentPopselectKeys,
RTableInst, RTableInst,
} from './types' } from './types'
import type { ExtractPublicPropTypes } from 'vue'
export default defineComponent({ export default defineComponent({
name: 'RTable', name: 'RTable',
inheritAttrs: false, inheritAttrs: false,
props, props,
setup(props, ctx) { setup(props, ctx) {
const { expose, emit } = ctx const { expose } = ctx
const rTableInst = useTemplateRef<RTableInst>('rTableInst') const rTableInst = useTemplateRef<RTableInst>('rTableInst')
const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef') const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef')
const uuidWrapper = uuid(16)
const uuidWrapper = uuid(16) // wrapper id const uuidTable = uuid(16)
const uuidTable = uuid(16) // table id
/** /**
* *
* x: 横坐标 * x: 横坐标
@ -61,40 +59,67 @@ export default defineComponent({
pick(props, 'striped', 'bordered'), pick(props, 'striped', 'bordered'),
), ),
) )
// 默认设置 card header style
const cardHeaderStyle = computed(() => {
const { title, tool, cardProps } = props
const { headerStyle = {} } = cardProps ?? {}
if (!title && !tool) {
return Object.assign(
{},
{
paddingTop: '0px',
},
headerStyle,
)
}
return headerStyle
})
// 如果启用了 flexAutoHeight 属性,则自动继承高度
const flexAutoHeightStyle = computed(() => {
const { flexAutoHeight } = props
if (!flexAutoHeight) {
return null
}
return {
height: '100%',
flex: 1,
}
})
/** /**
* *
* @param key key * @param key key
* @param option context menu select option * @param option
*
* @description
*
*/ */
const contextMenuSelect = ( const contextMenuSelect = (
key: number | string, key: number | string,
option: DropdownOption, option: DropdownOption,
) => { ) => {
contextMenuReactive.showContextMenu = false
const { onContextMenuClick } = props const { onContextMenuClick } = props
if (onContextMenuClick) { if (onContextMenuClick) {
call(onContextMenuClick, key, option) call(onContextMenuClick, key, option)
} }
contextMenuReactive.showContextMenu = false
} }
/** /**
* *
* RTable rowProps * @param e
* *
* @description
*
*/ */
const combineRowProps = (row: Record<string, unknown>, idx: number) => { const tableContextMenu = (e: MouseEvent) => {
const interceptRowProps = props.rowProps?.(row, idx)
return {
...interceptRowProps,
onContextmenu: props.disabledContextMenu
? void 0
: (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
contextMenuReactive.showContextMenu = false contextMenuReactive.showContextMenu = false
nextTick().then(() => { nextTick().then(() => {
@ -102,15 +127,35 @@ export default defineComponent({
contextMenuReactive.x = e.clientX contextMenuReactive.x = e.clientX
contextMenuReactive.y = e.clientY contextMenuReactive.y = e.clientY
}) })
}, }
/**
*
* @param row
* @param idx
*
* @description
* RTable rowProps
*/
const combineRowProps = (row: Record<string, unknown>, idx: number) => {
const interceptRowProps = props.rowProps?.(row, idx) || {}
if (props.disabledContextMenu) {
return interceptRowProps
}
return {
...interceptRowProps,
onContextmenu: tableContextMenu,
} }
} }
/** /**
* *
* @param size table size * @param size
* *
* table size * @description
*
*/ */
const changeTableSize = (size: ComponentSize) => { const changeTableSize = (size: ComponentSize) => {
privateReactive.size = size privateReactive.size = size
@ -118,9 +163,10 @@ export default defineComponent({
/** /**
* *
* @param options table columns * @param options
* *
* table columns onUpdateColumns onUpdate:columns * @description
* table columns onUpdateColumns onUpdate:columns
*/ */
const updateTableColumn = (options: CType[]) => { const updateTableColumn = (options: CType[]) => {
const { onUpdateColumns, 'onUpdate:columns': $onUpdateColumns } = props const { onUpdateColumns, 'onUpdate:columns': $onUpdateColumns } = props
@ -128,6 +174,7 @@ export default defineComponent({
if (onUpdateColumns) { if (onUpdateColumns) {
call(onUpdateColumns, options) call(onUpdateColumns, options)
} }
if ($onUpdateColumns) { if ($onUpdateColumns) {
call($onUpdateColumns, options) call($onUpdateColumns, options)
} }
@ -135,8 +182,9 @@ export default defineComponent({
/** /**
* *
* toolOptions * @description
* toolOptions * toolOptions
* toolOptions
*/ */
const renderToolOptions = () => { const renderToolOptions = () => {
const { toolOptions } = props const { toolOptions } = props
@ -158,37 +206,50 @@ export default defineComponent({
/** /**
* *
* @param p props * @description
* *
* toolOptions toolOptions
*/ */
const tool = (p: typeof props) => { const renderDefaultToolOptions = () => {
const { tool } = p const { onUpdateColumns, 'onUpdate:columns': rOnUpdateColumns } = props
const needSettingComponent = !!onUpdateColumns || !!rOnUpdateColumns
if (!tool) { return (
return
}
const renderDefaultToolOptions = () => (
<NFlex align="center"> <NFlex align="center">
<Print {...p} /> <Print {...props} />
<Size {...p} onChangeSize={changeTableSize.bind(this)} /> <Size {...props} onChangeSize={changeTableSize} />
<Fullscreen /> <Fullscreen />
<C {...p} onUpdateColumn={updateTableColumn.bind(this)} /> {needSettingComponent ? (
<C {...props} onUpdateColumn={updateTableColumn} />
) : null}
<TablePropsSelect <TablePropsSelect
{...p} {...props}
onPopselectChange={popselectChange.bind(this)} onPopselectChange={popselectChange}
onInitialed={popselectChange.bind(this)} onInitialed={popselectChange}
/> />
</NFlex> </NFlex>
) )
}
if (!props.toolOptions) { /**
*
* @param p props
*
* @description
* toolOptions toolOptions
*/
const tool = (p: typeof props) => {
if (!p.tool) {
return
}
if (!p.toolOptions) {
return renderDefaultToolOptions return renderDefaultToolOptions
} else { }
if (props.coverTool) {
if (p.coverTool) {
return () => <NFlex align="center">{renderToolOptions()}</NFlex> return () => <NFlex align="center">{renderToolOptions()}</NFlex>
} else { }
return () => ( return () => (
<NFlex align="center"> <NFlex align="center">
{renderDefaultToolOptions()} {renderDefaultToolOptions()}
@ -196,8 +257,6 @@ export default defineComponent({
</NFlex> </NFlex>
) )
} }
}
}
onMounted(() => { onMounted(() => {
// 主动调用 register 方法,满足 useTable 方法正常调用 // 主动调用 register 方法,满足 useTable 方法正常调用
@ -208,6 +267,7 @@ export default defineComponent({
uuidTable, uuidTable,
uuidWrapper, uuidWrapper,
wrapperRef, wrapperRef,
tableRef: rTableInst,
}) })
} }
}) })
@ -230,6 +290,9 @@ export default defineComponent({
tool, tool,
wrapperRef, wrapperRef,
propsPopselectValue, propsPopselectValue,
cardHeaderStyle,
flexAutoHeightStyle,
tableContextMenu,
} }
}, },
render() { render() {
@ -242,6 +305,8 @@ export default defineComponent({
uuidWrapper, uuidWrapper,
privateReactive, privateReactive,
propsPopselectValue, propsPopselectValue,
cardHeaderStyle,
flexAutoHeightStyle,
} = this } = this
const { class: className, ...restAttrs } = $attrs const { class: className, ...restAttrs } = $attrs
const { tool, combineRowProps, contextMenuSelect } = this const { tool, combineRowProps, contextMenuSelect } = this
@ -253,12 +318,15 @@ export default defineComponent({
title, title,
tableFlexHeight, tableFlexHeight,
cardProps, cardProps,
flexAutoHeight,
flexHeight,
...restProps ...restProps
} = $props as ExtractPublicPropTypes<typeof props> } = $props
const { headerStyle, ...restCardProps } = cardProps ?? {}
return ( return (
<NCard <NCard
{...cardProps} {...restCardProps}
{...{ {...{
id: uuidWrapper, id: uuidWrapper,
}} }}
@ -266,6 +334,8 @@ export default defineComponent({
ref="wrapperRef" ref="wrapperRef"
bordered={wrapperBordered} bordered={wrapperBordered}
class={className} class={className}
// flexAutoHeight 具有更高的优先级
style={Object.assign({}, cardHeaderStyle, flexAutoHeightStyle)}
> >
{{ {{
default: () => ( default: () => (
@ -276,12 +346,14 @@ export default defineComponent({
}} }}
{...(restProps as DataTableProps)} {...(restProps as DataTableProps)}
{...propsPopselectValue} {...propsPopselectValue}
flexHeight={flexAutoHeight ? true : flexHeight}
rowProps={combineRowProps.bind(this)} rowProps={combineRowProps.bind(this)}
size={privateReactive.size} size={privateReactive.size}
ref="rTableInst" ref="rTableInst"
style={{ style={{
height: height: flexAutoHeight
tableFlexHeight !== null && tableFlexHeight !== void 0 ? '100%'
: tableFlexHeight !== null && tableFlexHeight !== void 0
? completeSize(tableFlexHeight) ? completeSize(tableFlexHeight)
: null, : null,
}} }}

View File

@ -113,7 +113,6 @@ export default defineComponent({
return true return true
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return cloneColumns.map((curr, idx) => { return cloneColumns.map((curr, idx) => {
const { key, title, children, fixed, isResizable, ...args } = const { key, title, children, fixed, isResizable, ...args } =
curr as C curr as C
@ -170,7 +169,7 @@ export default defineComponent({
} }
}) as C[] }) as C[]
}, },
// eslint-disable-next-line @typescript-eslint/no-empty-function
set: () => {}, set: () => {},
}) })
@ -178,7 +177,10 @@ export default defineComponent({
const { onUpdateColumn } = props const { onUpdateColumn } = props
if (onUpdateColumn) { if (onUpdateColumn) {
// 使用 nextTick 确保 DOM 更新后再触发事件
nextTick(() => {
call(onUpdateColumn, options) call(onUpdateColumn, options)
})
} }
} }

View File

@ -303,6 +303,43 @@ const useCheckedRowKeys = <
} }
} }
/**
*
* @param keys keys
*
* @description
* keys
*/
const selectKeys = (keys: RowKey[]) => {
keys.forEach((key) => selectKey(key))
}
/**
*
* @param key key
*
* @description
* key
*/
const toggleKey = (key: RowKey) => {
if (keysRef.value.includes(key)) {
clearKey(key)
} else {
selectKey(key)
}
}
/**
*
* @param key key
*
* @description
* key
*/
const isKeySelected = (key: RowKey) => {
return keysRef.value.includes(key)
}
effectDispose(() => { effectDispose(() => {
clearAll() clearAll()
}) })
@ -317,6 +354,9 @@ const useCheckedRowKeys = <
clearAll, clearAll,
clearKey, clearKey,
selectKey, selectKey,
selectKeys,
toggleKey,
isKeySelected,
}, },
] as const ] as const
} }

View File

@ -41,13 +41,13 @@ import type { PrintDomOptions } from '@/utils'
* }) * })
*/ */
const useTable = () => { const useTable = () => {
const tableRef = ref<RTableInst>() const tableRef = shallowRef<RTableInst>()
let extra = {} as TableProvider const extraRef = shallowRef<TableProvider>({} as TableProvider)
const register: UseTableRegister = (inst, extraInfo) => { const register: UseTableRegister = (inst, extraInfo) => {
if (inst) { if (inst) {
tableRef.value = inst tableRef.value = inst
extra = extraInfo extraRef.value = extraInfo
} }
} }
@ -140,7 +140,7 @@ const useTable = () => {
* *
*/ */
const print = (options?: PrintDomOptions) => { const print = (options?: PrintDomOptions) => {
const { uuidWrapper } = extra const { uuidWrapper } = extraRef.value ?? {}
if (uuidWrapper) { if (uuidWrapper) {
const tableWrapperElement = document.getElementById(uuidWrapper) const tableWrapperElement = document.getElementById(uuidWrapper)
@ -149,9 +149,23 @@ const useTable = () => {
} }
} }
/**
*
* @description
*
*
* @see https://www.naiveui.com/zh-CN/dark/components/data-table#DataTable-Methods
*/
const filter = (filters: FilterState | null) => const filter = (filters: FilterState | null) =>
getTableInstance().filter.call(null, filters) getTableInstance().filter.call(null, filters)
/**
*
* @description
* Table
*/
const getTableConfig = () => extraRef.value
return [ return [
register, register,
{ {
@ -165,6 +179,7 @@ const useTable = () => {
sort, sort,
print, print,
filter, filter,
getTableConfig,
}, },
] as const ] as const
} }

View File

@ -3,9 +3,9 @@
visibility: visible; visibility: visible;
} }
& .ray-icon { & .r-icon {
&.r-table__c-tool-icon--active { &.r-table__c-tool-icon--active {
color: var(--ray-theme-primary-color); color: var(--r-theme-primary-color);
} }
} }
} }

View File

@ -238,6 +238,17 @@ const props = {
type: [Function, Array] as PropType<MaybeArray<UseTableRegister>>, type: [Function, Array] as PropType<MaybeArray<UseTableRegister>>,
default: null, default: null,
}, },
/**
*
* @description
*
*
* @default false
*/
flexAutoHeight: {
type: Boolean,
default: false,
},
} as const } as const
export default props export default props

View File

@ -35,6 +35,7 @@ export interface TableProvider {
uuidWrapper: string uuidWrapper: string
uuidTable: string uuidTable: string
wrapperRef: Readonly<ShallowRef<HTMLElement | null>> wrapperRef: Readonly<ShallowRef<HTMLElement | null>>
tableRef: Readonly<ShallowRef<RTableInst | null>>
} }
export interface C extends DataTableBaseColumn { export interface C extends DataTableBaseColumn {
@ -54,6 +55,7 @@ export interface RTableInst extends Omit<DataTableInst, 'clearFilter'> {
* *
*/ */
print: (options?: PrintDomOptions) => void print: (options?: PrintDomOptions) => void
config: TableProvider
} }
export type OverridesTableColumn<T = Recordable> = C | DataTableColumn<T> export type OverridesTableColumn<T = Recordable> = C | DataTableColumn<T>

View File

@ -1,4 +1,4 @@
import RTransitionComponent from './src/index.vue' import RTransitionComponent from './src/TransitionComponent.vue'
import transitionComponentProps from './src/props' import transitionComponentProps from './src/props'
import type * as RTransitionComponentType from './src/types' import type * as RTransitionComponentType from './src/types'

View File

@ -31,8 +31,9 @@ import type { TransitionProps } from './types'
/** /**
* *
* 使用宏编译模式时可以使用 defineOptions 声明组件选项 * @description
* 常用方法即是声明该组件的 name inheritAttrs 等属性 * 使用宏编译模式时可以使用 defineOptions 声明组件选项
* 常用方法即是声明该组件的 name inheritAttrs 等属性
*/ */
defineOptions({ defineOptions({
name: 'RTransitionComponent', name: 'RTransitionComponent',

View File

@ -1,4 +1,5 @@
import RCollapse from '../components/pro/RCollapse/Collapse' import RCollapse from '../components/pro/RCollapse/Collapse'
import RDraggableCard from '../components/base/RDraggableCard/DraggableCard'
// 导出所有自定义组件 // 导出所有自定义组件
export * from './base/RChart' export * from './base/RChart'
@ -14,7 +15,7 @@ export * from './base/RSegment'
export * from './base/RBarcode' export * from './base/RBarcode'
export * from '../components/pro/RTablePro' export * from '../components/pro/RTablePro'
export * from './base/RFlow' export * from './base/RFlow'
export { RCollapse } export { RCollapse, RDraggableCard }
// 导出自定义组件类型 // 导出自定义组件类型
export type * from './base/RChart/src/types' export type * from './base/RChart/src/types'
@ -32,3 +33,4 @@ export type {
FlowGraphData, FlowGraphData,
FlowOptions, FlowOptions,
} from './base/RFlow/src/types' } from './base/RFlow/src/types'
export type { DefaultPosition } from './base/RDraggableCard/DraggableCard'

View File

@ -3,7 +3,27 @@ import { RCollapseGrid, RForm } from '@/components'
import formProps from '../../base/RForm/src/props' import formProps from '../../base/RForm/src/props'
import collapseGridProps from '../../base/RCollapseGrid/src/props' import collapseGridProps from '../../base/RCollapseGrid/src/props'
import type { FormProps, GridProps } from 'naive-ui' import type { GridProps } from 'naive-ui'
export const collapseProps = Object.assign({}, formProps, {
...collapseGridProps,
open: {
type: Boolean,
default: true,
},
cols: {
type: [Number, String] as PropType<GridProps['cols']>,
default: '4 xs:1 s:2 m:2 l:4 xl:4 2xl:6',
},
bordered: {
type: Boolean,
default: true,
},
responsive: {
type: String as PropType<GridProps['responsive']>,
default: 'screen',
},
})
/** /**
* *
@ -13,25 +33,10 @@ import type { FormProps, GridProps } from 'naive-ui'
*/ */
export default defineComponent({ export default defineComponent({
name: 'RCollapse', name: 'RCollapse',
props: Object.assign({}, formProps, { props: collapseProps,
...collapseGridProps,
open: {
type: Boolean,
default: true,
},
cols: {
type: Number,
default: 4,
},
bordered: {
type: Boolean,
default: true,
},
}),
render() { render() {
const { $slots, $props } = this const { $slots, $props } = this
const { labelPlacement, showFeedback, ...rest } = $props as FormProps & const { labelPlacement, showFeedback, ...rest } = $props
GridProps
return ( return (
<RForm {...rest} labelPlacement="top" showFeedback={false}> <RForm {...rest} labelPlacement="top" showFeedback={false}>

View File

@ -3,9 +3,23 @@ import tableProProps from './src/props'
import { useTablePro } from './src/hooks/useTablePro' import { useTablePro } from './src/hooks/useTablePro'
import type { ExtractPropTypes } from 'vue' import type { ExtractPropTypes } from 'vue'
import type {
BasePagination,
TablePagination,
FormatRangeTime,
TableRequestConfig,
TableProFieldNames,
} from './src/types'
type TableProProps = ExtractPropTypes<typeof tableProProps> type TableProProps = ExtractPropTypes<typeof tableProProps>
export type { TableProProps } export type {
TableProProps,
BasePagination,
TablePagination,
FormatRangeTime,
TableRequestConfig,
TableProFieldNames,
}
export { RTablePro, useTablePro, tableProProps } export { RTablePro, useTablePro, tableProProps }

View File

@ -1,30 +1,23 @@
import { RTable } from '@/components' import { RTable } from '../../../base/RTable'
import { NFlex } from 'naive-ui'
import props from './props' import props from './props'
import { useTable } from '@/components' import useTable from '../../../base/RTable/src/hooks/useTable'
import { call } from '@/utils' import { call, removeDuplicateKeys } from '@/utils'
import { usePagination } from '@/hooks' import { usePagination } from '@/hooks'
import { omit } from 'lodash-es'
import type { TablePagination, TableRequestConfig, TableProInst } from './types' import type { TablePagination, TableRequestConfig, TableProInst } from './types'
import type { RTableInst } from '../../..'
import type { Recordable } from '@/types'
export default defineComponent({ export default defineComponent({
name: 'RTablePro', name: 'RTablePro',
props, props,
setup(props) { setup(props, ctx) {
const [ const { expose } = ctx
register,
{ const [register, tableFns] = useTable()
clearFilters,
clearSorter,
downloadCsv,
filters,
page,
scrollTo,
sort,
print,
filter,
},
] = useTable()
const [ const [
paginationRef, paginationRef,
{ {
@ -34,78 +27,97 @@ export default defineComponent({
setItemCount, setItemCount,
resetPagination, resetPagination,
getItemCount, getItemCount,
setPage,
setPageSize,
}, },
] = usePagination(void 0, { ] = usePagination(void 0, {
prefix: (info) => `${info.itemCount}`, prefix: props.paginationPrefix,
}) })
const tableRequestRef = computed(() => props.request) // 获取最新 pagination 值 - 使用 computed 避免函数重新创建
const tablePagination = computed<TablePagination>(() => ({
// 获取最新 statistics 和 pagination 值 page: getPage(),
const update = (): TablePagination => { pageSize: getPageSize(),
return { itemCount: getItemCount(),
getItemCount, }))
getPage,
getPageSize,
}
}
// 派发表格更新事件 // 派发表格更新事件
const emitTableUpdate = () => { const emitTableUpdate = () => {
const { onTablePaginationUpdate } = props const { onTablePaginationUpdate } = props
if (onTablePaginationUpdate) { if (onTablePaginationUpdate) {
call(onTablePaginationUpdate, update()) call(onTablePaginationUpdate, tablePagination.value)
} }
} }
// 合并请求参数 // 合并请求参数
const combineRequestParams = (extraConfig?: TableRequestConfig) => { const combineRequestParams = <T extends Recordable>(
extraConfig?: TableRequestConfig<T>,
) => {
const config = Object.assign({}, props.requestConfig, extraConfig) const config = Object.assign({}, props.requestConfig, extraConfig)
const { params, formatRangeTime } = config const {
formatRangeTime,
excludeParams,
autoDeleteDuplicateKeys = true,
} = config
let params: Recordable = config.params || {}
// 转换时间范围,该功能仅支持 NDatePicker range 模式参数 // 转换时间范围,该功能仅支持 NDatePicker range 模式参数
if (formatRangeTime?.length && params) { if (formatRangeTime?.length && params) {
formatRangeTime.forEach((curr) => { formatRangeTime.forEach(({ key, target }) => {
const { key, target } = curr
const val = params[key] as [number, number] | null const val = params[key] as [number, number] | null
if (val && target?.length) { if (val && target?.length === 2) {
const [start, end] = val params[target[0]] = val[0]
params[target[1]] = val[1]
params[target[0]] = start
params[target[1]] = end
} else {
// 当传递时间参数被清空时,则清空对应 time key
params[key] = null
params[target[0]] = null
params[target[1]] = null
} }
// 清空时间参数的 key
delete params[key]
}) })
} }
if (autoDeleteDuplicateKeys) {
params = removeDuplicateKeys(params)
}
// 排除指定的请求参数
if (excludeParams) {
params = omit(params, excludeParams)
}
const requestParams = Object.assign({}, params, { const requestParams = Object.assign({}, params, {
page: getPage(), page: getPage(),
pageSize: getPageSize(), pageSize: getPageSize(),
}) })
return requestParams return requestParams as T & { page: number; pageSize: number }
} }
// 会重置 pagination 的请求 // 同步执行 request 请求,允许重置 pagination 请求
const runResetPaginationRequest = (extraConfig?: TableRequestConfig) => { const runResetPaginationRequest: TableProInst['runTableRequest'] = (
extraConfig,
reset = true,
) => {
if (reset) {
resetPagination() resetPagination()
const requestParams = combineRequestParams(extraConfig)
tableRequestRef.value?.(requestParams)
} }
// 不会重置 pagination 的请求
const runRequest = (extraConfig?: TableRequestConfig) => {
const requestParams = combineRequestParams(extraConfig) const requestParams = combineRequestParams(extraConfig)
tableRequestRef.value?.(requestParams) props.request?.(requestParams)
}
// 异步执行 request 请求,允许重置 pagination 请求,返回 Promise 对象
const runResetPaginationRequestAsync: TableProInst['runAsyncTableRequest'] =
async (extraConfig, reset = true) => {
if (reset) {
resetPagination()
}
const requestParams = combineRequestParams(extraConfig)
await props.request?.(requestParams)
} }
watchEffect(() => { watchEffect(() => {
@ -114,7 +126,7 @@ export default defineComponent({
const { manual } = props const { manual } = props
if (!manual) { if (!manual) {
runRequest() runResetPaginationRequest(void 0, false)
} }
emitTableUpdate() emitTableUpdate()
@ -126,23 +138,22 @@ export default defineComponent({
if (onRegister) { if (onRegister) {
call(onRegister, { call(onRegister, {
getTablePagination: update, ...(tableFns as unknown as RTableInst),
getTablePagination: () => tablePagination.value,
runTableRequest: runResetPaginationRequest, runTableRequest: runResetPaginationRequest,
clearFilters, runAsyncTableRequest: runResetPaginationRequestAsync,
clearSorter, getCurrentTableRequestParams: combineRequestParams,
downloadCsv, resetTablePagination: resetPagination,
filters, setPage,
page, setPageSize,
scrollTo, getPage,
sort, getPageSize,
print,
filter,
getCurrentTableRequestParams:
combineRequestParams as TableProInst['getCurrentTableRequestParams'],
}) })
} }
}) })
expose()
return { return {
register, register,
paginationRef, paginationRef,
@ -150,18 +161,30 @@ export default defineComponent({
}, },
render() { render() {
const { register, $props, paginationRef, $slots } = this const { register, $props, paginationRef, $slots } = this
const { onRegister, showPagination, ...rest } = $props as ExtractPropTypes< const {
typeof props onRegister,
> showPagination,
takeoverAutoHeight,
flexAutoHeight,
...rest
} = $props
const { collapse, ...restSlots } = $slots
const baseProps = {
onRegister: register,
pagination: showPagination ? paginationRef : void 0,
flexAutoHeight: takeoverAutoHeight || flexAutoHeight,
...rest,
}
if (takeoverAutoHeight) {
return ( return (
<RTable <NFlex vertical class="h-full">
{...rest} {collapse?.()}
onRegister={register} <RTable {...baseProps}>{restSlots}</RTable>
pagination={showPagination ? paginationRef : void 0} </NFlex>
>
{$slots}
</RTable>
) )
}
return <RTable {...baseProps}>{restSlots}</RTable>
}, },
}) })

View File

@ -1,7 +1,6 @@
import type { Recordable } from '@/types' import type { Recordable } from '@/types'
import type { TableProInst, TableRequestConfig } from '../types' import type { TableProInst, TableRequestConfig } from '../types'
import type { import type {
RTableInst,
CsvOptionsType, CsvOptionsType,
FilterState, FilterState,
ScrollToOptions, ScrollToOptions,
@ -49,6 +48,9 @@ export const useTablePro = () => {
getTableProInstance().getTablePagination.call(null) getTableProInstance().getTablePagination.call(null)
/** /**
*
* @param extraConfig
* @param reset
* *
* @description * @description
* *
@ -58,9 +60,13 @@ export const useTablePro = () => {
* TableRequestConfig props tableRequestConfig * TableRequestConfig props tableRequestConfig
* pagination * pagination
*/ */
const runTableRequest = <T extends Recordable>( const runTableRequest = <
extraConfig?: TableRequestConfig<T>, T extends Recordable,
) => getTableProInstance().runTableRequest.call(null, extraConfig) ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
reset?: boolean,
) => getTableProInstance().runTableRequest.call(null, extraConfig, reset)
/** /**
* *
@ -81,6 +87,8 @@ export const useTablePro = () => {
const clearSorter = () => getTableProInstance().clearSorter.call(null) const clearSorter = () => getTableProInstance().clearSorter.call(null)
/** /**
*
* @param options CSV
* *
* @description * @description
* CSV * CSV
@ -91,6 +99,8 @@ export const useTablePro = () => {
getTableProInstance().downloadCsv.call(null, options) getTableProInstance().downloadCsv.call(null, options)
/** /**
*
* @param filters
* *
* @description * @description
* *
@ -101,6 +111,8 @@ export const useTablePro = () => {
getTableProInstance().filters.call(null, filters) getTableProInstance().filters.call(null, filters)
/** /**
*
* @param page
* *
* @description * @description
* page * page
@ -110,6 +122,8 @@ export const useTablePro = () => {
const page = (page: number) => getTableProInstance().page.call(null, page) const page = (page: number) => getTableProInstance().page.call(null, page)
/** /**
*
* @param options
* *
* @description * @description
* *
@ -121,6 +135,9 @@ export const useTablePro = () => {
getTableProInstance().scrollTo(options as any) getTableProInstance().scrollTo(options as any)
/** /**
*
* @param columnKey
* @param order
* *
* @description * @description
* *
@ -131,6 +148,8 @@ export const useTablePro = () => {
getTableProInstance().sort.call(null, columnKey, order) getTableProInstance().sort.call(null, columnKey, order)
/** /**
*
* @param options
* *
* @description * @description
* *
@ -141,17 +160,88 @@ export const useTablePro = () => {
/** /**
* *
* @param extraConfig * @param extraConfig
* @param excludeParams
* *
* @description * @description
* *
*/ */
const getCurrentTableRequestParams = <T = Recordable>( const getCurrentTableRequestParams = <
extraConfig?: TableRequestConfig<T>, T extends Recordable,
ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
): T & Recordable => ): T & Recordable =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
getTableProInstance().getCurrentTableRequestParams.call(null, extraConfig) getTableProInstance().getCurrentTableRequestParams.call(null, extraConfig)
/**
*
* @param extraConfig
*
* @description
*
*/
const resetTablePagination = () =>
getTableProInstance().resetTablePagination.call(null)
/**
*
* @param extraConfig
* @param reset
*
* @description
*
*/
const runAsyncTableRequest = <
T extends Recordable,
ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
reset?: boolean,
) => getTableProInstance().runAsyncTableRequest.call(null, extraConfig, reset)
/**
*
* @description
* TablePro
*/
const getTableProConfig = () => getTableProInstance().config
/**
*
* @description
*
*/
const getPage = () => getTableProInstance().getPage.call(null)
/**
*
* @description
/**
*
* @description
*
*/
const getPageSize = () => getTableProInstance().getPageSize.call(null)
/**
*
* @description
*
*/
const setPage = (page: number) =>
getTableProInstance().setPage.call(null, page)
/**
*
* @description
*
*/
const setPageSize = (pageSize: number) =>
getTableProInstance().setPageSize.call(null, pageSize)
return [ return [
register, register,
{ {
@ -167,6 +257,13 @@ export const useTablePro = () => {
runTableRequest, runTableRequest,
print, print,
getCurrentTableRequestParams, getCurrentTableRequestParams,
resetTablePagination,
runAsyncTableRequest,
getTableProConfig,
getPage,
getPageSize,
setPage,
setPageSize,
}, },
] as const ] as const
} }

View File

@ -2,7 +2,12 @@ import { tableProps } from '@/components'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { TableProInst, TablePagination, TableRequestConfig } from './types' import type {
TableProInst,
TableRequestConfig,
PaginationPrefix,
TablePaginationUpdate,
} from './types'
import type { AnyFC } from '@/types' import type { AnyFC } from '@/types'
const props = { const props = {
@ -62,7 +67,7 @@ const props = {
* @default undefined * @default undefined
*/ */
onTablePaginationUpdate: { onTablePaginationUpdate: {
type: Function as PropType<(pagination: TablePagination) => void>, type: Function as PropType<TablePaginationUpdate>,
}, },
/** /**
* *
@ -70,11 +75,10 @@ const props = {
* *
* *
* *
* @default {} * @default undefined
*/ */
requestConfig: { requestConfig: {
type: Object as PropType<TableRequestConfig>, type: Object as PropType<TableRequestConfig>,
default: () => ({}),
}, },
/** /**
* *
@ -101,6 +105,33 @@ const props = {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
/**
*
* @description
*
*
*
* @default (info) => `${info.itemCount}`
*/
paginationPrefix: {
type: Function as PropType<PaginationPrefix>,
default: (info: Parameters<NonNullable<PaginationPrefix>>[0]) =>
`${info.itemCount}`,
},
/**
*
* @description
*
* flexAutoHeight true使 flexAutoHeight
*
* flexAutoHeight
*
* @default false
*/
takeoverAutoHeight: {
type: Boolean,
default: false,
},
} }
export default props export default props

View File

@ -1,5 +1,5 @@
import type { TableProps, RTableInst } from '@/components' import type { TableProps, RTableInst } from '@/components'
import type { UsePaginationReturn } from '@/hooks' import type { UsePaginationOptions } from '@/hooks'
import type { Recordable } from '@/types' import type { Recordable } from '@/types'
export type FormatRangeTime = { export type FormatRangeTime = {
@ -17,17 +17,29 @@ export type FormatRangeTime = {
target: [string | number, string | number] target: [string | number, string | number]
} }
export interface BasePagination {
page: number
pageSize: number
itemCount: number
}
/** /**
* *
* @description * @description
* Pagination * Pagination
*/ */
export type TablePagination = Pick< export type TablePagination = BasePagination
UsePaginationReturn[1],
'getItemCount' | 'getPage' | 'getPageSize'
>
export interface TableRequestConfig<Params = Recordable> { export type TablePaginationUpdate = (
pagination: Readonly<TablePagination>,
) => void
export type PaginationPrefix = UsePaginationOptions['prefix']
export interface TableRequestConfig<
Params = Recordable,
ExcludeParams extends keyof Params = keyof Params,
> {
/** /**
* *
* @description * @description
@ -45,6 +57,22 @@ export interface TableRequestConfig<Params = Recordable> {
* @default undefined * @default undefined
*/ */
formatRangeTime?: FormatRangeTime[] formatRangeTime?: FormatRangeTime[]
/**
*
* @description
*
*
* @default undefined
*/
excludeParams?: ExcludeParams[]
/**
*
* @description
*
*
* @default true
*/
autoDeleteDuplicateKeys?: boolean
} }
export type TableProProps = Omit<TableProps, 'pagination'> export type TableProProps = Omit<TableProps, 'pagination'>
@ -54,22 +82,166 @@ export interface TableProInst extends Omit<RTableInst, 'getTableInstance'> {
* *
* @description * @description
* pagination * pagination
*
* @example
* const [register, { getTablePagination }] = useTablePro()
*
* // 获取当前 pagination 的值
* const pagination = getTablePagination()
*/ */
getTablePagination: () => TablePagination getTablePagination: () => TablePagination
/** /**
*
* @param extraConfig
* @param reset
* *
* @description * @description
* *
*
* @example
* const [register, { runTableRequest }] = useTablePro()
*
* // 重置分页请求
* runTableRequest(void 0, true)
* runTableRequest()
* // 不重置分页请求
* runTableRequest(void 0, false)
*/ */
runTableRequest: (extraConfig?: TableRequestConfig) => void runTableRequest: <
T extends Recordable,
ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
reset?: boolean,
) => void
/**
*
* @param extraConfig
* @param reset
*
* @description
*
*
* @example
* const [register, { runAsyncTableRequest }] = useTablePro()
*
* // 重置分页请求
* runAsyncTableRequest(void 0, true)
* runAsyncTableRequest()
* // 不重置分页请求
* runAsyncTableRequest(void 0, false)
*/
runAsyncTableRequest: <
T extends Recordable,
ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
reset?: boolean,
) => Promise<void>
/** /**
* *
* @param extraConfig * @param extraConfig
* *
* @description * @description
* *
*
* @example
* const [register, { getCurrentTableRequestParams }] = useTablePro()
*
* // 获取当前内部表格请求参数
* const params = getCurrentTableRequestParams()
*/ */
getCurrentTableRequestParams: <T = Recordable>( getCurrentTableRequestParams: <
extraConfig?: TableRequestConfig<T>, T extends Recordable,
) => TableRequestConfig<T>['params'] & Recordable ExcludeParams extends keyof T = keyof T,
>(
extraConfig?: TableRequestConfig<T, ExcludeParams>,
) => TableRequestConfig<T, ExcludeParams>['params'] & Recordable
/**
*
* @description
*
*
* @example
* const [register, { resetTablePagination }] = useTablePro()
*
* // 重置表格分页为初始化状态,即第 1 页,每页 10 条数据
* resetTablePagination()
*/
resetTablePagination: () => void
/**
*
* @description
*
*
* @example
* const [register, { setPage }] = useTablePro()
*
* // 设置表格分页页码为 2
* setPage(2)
*/
setPage: (page: number) => void
/**
*
* @description
*
*
* @example
* const [register, { setPageSize }] = useTablePro()
*
* // 设置表格分页每页条数为 20
* setPageSize(20)
*/
setPageSize: (pageSize: number) => void
/**
*
* @description
*
*
* @example
* const [register, { getPage }] = useTablePro()
*
* // 获取表格分页页码
* const page = getPage()
*/
getPage: () => number
/**
*
* @description
*
*
* @example
* const [register, { getPageSize }] = useTablePro()
*
* // 获取表格分页每页条数
* const pageSize = getPageSize()
*/
getPageSize: () => number
}
export interface TableProFieldNames {
/**
*
* @description
*
*
* @default 'page'
*/
page: string
/**
*
* @description
*
*
* @default 'pageSize'
*/
pageSize: string
/**
*
* @description
*
*
* @default 'itemCount'
*/
itemCount: string
} }

View File

@ -1,6 +1,10 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { DEFAULT_DAYJS_LOCAL } from '@/app-config' import { DEFAULT_DAYJS_LOCAL } from '@/app-config'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { getStorage } from '@/utils'
import { APP_CATCH_KEY, DAYJS_LOCAL_MAP } from '@/app-config'
import type { SettingState } from '@/store/modules/setting/types'
/** /**
* *
@ -10,5 +14,16 @@ import 'dayjs/locale/zh-cn'
* dayjs * dayjs
*/ */
export const setupDayjs = () => { export const setupDayjs = () => {
dayjs.locale(DEFAULT_DAYJS_LOCAL) const { localeLanguage } = getStorage<SettingState>(
APP_CATCH_KEY.appPiniaSettingStore,
'localStorage',
{
defaultValue: {} as SettingState,
},
)
const local =
DAYJS_LOCAL_MAP[localeLanguage as keyof typeof DAYJS_LOCAL_MAP] ||
DEFAULT_DAYJS_LOCAL
dayjs.locale(local)
} }

View File

@ -41,15 +41,16 @@ const throttleDirective: CustomDirectiveFC<
throttleFunction = throttle(func, wait, Object.assign({}, options)) throttleFunction = throttle(func, wait, Object.assign({}, options))
useEventListener(el, trigger, throttleFunction) cleanup = useEventListener(el, trigger, throttleFunction)
}, },
beforeUnmount: () => { beforeUnmount: () => {
if (throttleFunction) { if (throttleFunction) {
throttleFunction.cancel() throttleFunction.cancel()
cleanup?.()
} }
throttleFunction = null throttleFunction = null
cleanup?.()
}, },
} }
} }

View File

@ -8,10 +8,10 @@ export const combineDirective = <
) => { ) => {
const directives = Object.keys(directiveModules).reduce( const directives = Object.keys(directiveModules).reduce(
(pre, curr) => { (pre, curr) => {
const fc = directiveModules[curr]?.default const fn = directiveModules[curr]?.default
if (typeof fc === 'function') { if (typeof fn === 'function') {
pre[curr as K] = fc pre[curr as K] = fn
return pre return pre
} else { } else {

View File

@ -23,10 +23,11 @@ export interface UseContextmenuCoordinateOptions {
* *
* @param target * @param target
* *
* NDropdown 使 * @description
* NDropdown 使
* *
* @example * @example
* const target = ref<HTMLElement | null>(null) * const target = useTemplateRef<HTMLElement | null>('target')
* const { x, y, show, stop } = useContextmenuCoordinate(target) * const { x, y, show, stop } = useContextmenuCoordinate(target)
* *
* stop * stop
@ -44,6 +45,13 @@ export const useContextmenuCoordinate = (
const show = ref(false) // 是否显示右键菜单 const show = ref(false) // 是否显示右键菜单
const { clickOutside } = options ?? {} const { clickOutside } = options ?? {}
/**
*
* @param value
*
* @description
*
*/
const updateShow = (value: boolean) => { const updateShow = (value: boolean) => {
show.value = value show.value = value
} }
@ -52,8 +60,9 @@ export const useContextmenuCoordinate = (
* *
* @param evt * @param evt
* *
* * @description
* *
*
*/ */
const bindContextMenuEvent = (evt: Event) => { const bindContextMenuEvent = (evt: Event) => {
evt.preventDefault() evt.preventDefault()
@ -73,7 +82,8 @@ export const useContextmenuCoordinate = (
if (clickOutside) { if (clickOutside) {
/** /**
* *
* * @description
*
*/ */
onClickOutside(target as MaybeElementRef<MaybeElement>, (detectIframe) => { onClickOutside(target as MaybeElementRef<MaybeElement>, (detectIframe) => {
clickOutside(detectIframe) clickOutside(detectIframe)
@ -82,7 +92,8 @@ export const useContextmenuCoordinate = (
/** /**
* *
* ref dom * @description
* ref dom
*/ */
const cleanupContextmenu = useEventListener( const cleanupContextmenu = useEventListener(
target, target,
@ -90,9 +101,11 @@ export const useContextmenuCoordinate = (
bindContextMenuEvent, bindContextMenuEvent,
options, options,
) )
/** /**
* *
* ref dom * @description
* ref dom
*/ */
const cleanupClick = useEventListener(target, 'click', () => { const cleanupClick = useEventListener(target, 'click', () => {
updateShow(false) updateShow(false)
@ -100,8 +113,9 @@ export const useContextmenuCoordinate = (
/** /**
* *
* * @description
* *
*
*/ */
const stop = () => { const stop = () => {
cleanupContextmenu() cleanupContextmenu()

View File

@ -1,6 +1,5 @@
import { setVariable, getVariableToRefs } from '@/global-variable' import { setVariable, getVariableToRefs } from '@/global-variable'
import { LAYOUT_CONTENT_REF } from '@/app-config' import { LAYOUT_CONTENT_REF } from '@/app-config'
import { unrefElement } from '@/utils'
import { useElementFullscreen } from '../web' import { useElementFullscreen } from '../web'
import type { UseElementFullscreenOptions } from '../web' import type { UseElementFullscreenOptions } from '../web'
@ -24,7 +23,13 @@ export interface MaximizeOptions extends UseElementFullscreenOptions {
scrollToOptions?: ScrollToOptions scrollToOptions?: ScrollToOptions
} }
export const useMaximize = () => { export const useMaximize = (options?: MaximizeOptions) => {
const contentEl = LAYOUT_CONTENT_REF as Ref<HTMLElement>
const { enter, exit, toggleFullscreen, isFullscreen } = useElementFullscreen(
contentEl,
options,
)
/** /**
* *
* LayoutContent * LayoutContent
@ -46,25 +51,30 @@ export const useMaximize = () => {
* LayoutContent layoutContentMaximize * LayoutContent layoutContentMaximize
* *
* @example * @example
* maximize(true, { MaximizeOptions }) * maximize(true)
* maximize(false, { MaximizeOptions }) * maximize(false)
*/ */
const maximize = (full: boolean, options?: MaximizeOptions) => { const maximize = (full: boolean, options?: MaximizeOptions) => {
const { scrollToOptions } = options ?? {} const { scrollToOptions } = options ?? {}
const contentEl = unrefElement(LAYOUT_CONTENT_REF as Ref<HTMLElement>)
const { toggleFullscreen } = useElementFullscreen(contentEl, options)
setVariable('layoutContentMaximize', full) setVariable('layoutContentMaximize', full)
toggleFullscreen()
if (full) {
enter()
} else {
exit()
}
if (scrollToOptions && full) { if (scrollToOptions && full) {
LAYOUT_CONTENT_REF?.value?.scrollTo(scrollToOptions) contentEl?.value?.scrollTo(scrollToOptions)
} }
} }
return { return {
isLayoutContentMaximized, isLayoutContentMaximized,
isFullscreen,
maximize, maximize,
toggleFullscreen,
} }
} }

View File

@ -10,17 +10,18 @@ export type CloseMenuTag = Key | MenuTagOptions
/** /**
* *
* @param target key * @param target key
* @param fc * @param fn
* *
* * @description
*
*/ */
const normalMenuTagOption = (target: CloseMenuTag, fc: string) => { const normalMenuTagOption = (target: CloseMenuTag, fn: string) => {
const { getMenuTagOptions } = useMenuGetters() const { getMenuTagOptions } = useMenuGetters()
if (typeof target === 'number') { if (typeof target === 'number') {
// 判断是否为 NaN // 判断是否为 NaN
if (isNaN(target)) { if (isNaN(target)) {
console.warn(`${fc}: The ${target} is NaN, expect number.`) console.warn(`${fn}: The ${target} is NaN, expect number.`)
return return
} }
@ -28,7 +29,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
// 判断是否超出当前标签页列表最大长度或者是否为负数 // 判断是否超出当前标签页列表最大长度或者是否为负数
if (target > getMenuTagOptions.value.length || target < -1) { if (target > getMenuTagOptions.value.length || target < -1) {
console.warn( console.warn(
`${fc}: The incoming index ${target} did not match the corresponding item.`, `${fn}: The incoming index ${target} did not match the corresponding item.`,
) )
return return
@ -50,7 +51,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
index, index,
} }
: console.warn( : console.warn(
`${fc}: The incoming key ${target} did not match the corresponding item.`, `${fn}: The incoming key ${target} did not match the corresponding item.`,
) )
} else { } else {
const { fullPath } = target const { fullPath } = target
@ -60,7 +61,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
if (index === -1) { if (index === -1) {
console.warn( console.warn(
`${fc}: The incoming menuTag option ${target.fullPath} did not match the corresponding item.`, `${fn}: The incoming menuTag option ${target.fullPath} did not match the corresponding item.`,
) )
return return
@ -82,27 +83,14 @@ export function useSiderBar() {
resolveOption, resolveOption,
} = useMenuActions() } = useMenuActions()
/** // 获取当前激活标签页索引位置
*
* @remark
*/
const getCurrentTagIndex = () => { const getCurrentTagIndex = () => {
return getMenuTagOptions.value.findIndex( return getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === getMenuKey.value, (curr) => curr.fullPath === getMenuKey.value,
) )
} }
/** // 校验指定标签右侧是否有可关闭的标签
*
* @param target
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*
* @remark
*/
const checkCloseRight = (target: CloseMenuTag) => { const checkCloseRight = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'checkCloseRight') const normal = normalMenuTagOption(target, 'checkCloseRight')
@ -116,17 +104,7 @@ export function useSiderBar() {
return false return false
} }
/** // 校验指定标签左侧是否有可关闭的标签
*
* @param target
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*
* @remark
*/
const checkCloseLeft = (target: CloseMenuTag) => { const checkCloseLeft = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'checkCloseRight') const normal = normalMenuTagOption(target, 'checkCloseRight')
@ -148,15 +126,7 @@ export function useSiderBar() {
return false return false
} }
/** // 关闭当前标签
*
* @param target
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*/
const close = (target: CloseMenuTag) => { const close = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'close') const normal = normalMenuTagOption(target, 'close')
@ -170,19 +140,24 @@ export function useSiderBar() {
spliceMenTagOptions(index) spliceMenTagOptions(index)
if (option.fullPath === getMenuKey.value) { if (option.fullPath === getMenuKey.value) {
const tag = getMenuTagOptions.value[index - 1] let i = checkCloseLeft(index)
? index - 1
: checkCloseRight(index)
? index
: index - 1
if (tag) { if (i < 0) {
changeMenuModelValue(tag.fullPath, tag) i = 0
} }
const tag = getMenuTagOptions.value[i]
tag && changeMenuModelValue(tag.fullPath, tag)
} }
} }
} }
/** // 关闭所有标签并且导航至 root path
*
* root path
*/
const closeAll = () => { const closeAll = () => {
spliceMenTagOptions(0, getMenuTagOptions.value.length) spliceMenTagOptions(0, getMenuTagOptions.value.length)
@ -209,18 +184,7 @@ export function useSiderBar() {
} }
} }
/** // 目标标签页右侧所有标签页
*
* @param target
*
*
* menuKey
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*/
const closeRight = (target: CloseMenuTag) => { const closeRight = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'closeRight') const normal = normalMenuTagOption(target, 'closeRight')
@ -239,18 +203,7 @@ export function useSiderBar() {
} }
} }
/** // 关闭目标标签页左侧所有标签页
*
* @param target
*
*
* menuKey
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*/
const closeLeft = (target: CloseMenuTag) => { const closeLeft = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'closeLeft') const normal = normalMenuTagOption(target, 'closeLeft')
@ -268,17 +221,7 @@ export function useSiderBar() {
} }
} }
/** // 关闭除了当前索引的所有菜单项
*
* @param target
*
*
*
* :
* - number:
* - string: key key url
* - AppMenuOption: 关闭当前项
*/
const closeOther = (target: CloseMenuTag) => { const closeOther = (target: CloseMenuTag) => {
const normal = normalMenuTagOption(target, 'closeOther') const normal = normalMenuTagOption(target, 'closeOther')

View File

@ -2,6 +2,7 @@ import { useSettingActions, useSettingGetters } from '@/store'
import { useI18n } from '@/hooks' import { useI18n } from '@/hooks'
import { APP_THEME } from '@/app-config' import { APP_THEME } from '@/app-config'
import { useColorMode } from '@vueuse/core' import { useColorMode } from '@vueuse/core'
import { merge } from 'lodash-es'
export type ThemeLabel = 'Dark' | 'Light' export type ThemeLabel = 'Dark' | 'Light'
@ -33,17 +34,15 @@ const setThemeOverrides = (theme: boolean) => {
updateSettingState( updateSettingState(
'primaryColorOverride', 'primaryColorOverride',
theme theme
? Object.assign( ? merge(
{}, {},
getPrimaryColorOverride.value, getPrimaryColorOverride.value,
APP_THEME.appNaiveUIThemeOverrides.dark, APP_THEME.appNaiveUIThemeOverrides.dark,
APP_THEME.appNaiveUIThemeOverridesCommon.dark,
) )
: Object.assign( : merge(
{}, {},
getPrimaryColorOverride.value, getPrimaryColorOverride.value,
APP_THEME.appNaiveUIThemeOverrides.light, APP_THEME.appNaiveUIThemeOverrides.light,
APP_THEME.appNaiveUIThemeOverridesCommon.light,
), ),
) )
} }

View File

@ -42,8 +42,8 @@ export const useDayjs = () => {
* dayjs * dayjs
* *
* @example * @example
* locale('en') * locale('zh-CN')
* locale('zh-cn') * locale('en-US')
*/ */
const locale = (key: LocalKey) => { const locale = (key: LocalKey) => {
const locale = DAYJS_LOCAL_MAP[key] const locale = DAYJS_LOCAL_MAP[key]

View File

@ -10,12 +10,32 @@ import {
import type { BasicTarget, TargetType } from '@/types' import type { BasicTarget, TargetType } from '@/types'
// html-to-image 方法
const domToImageMethods = {
svg: toSvg,
png: toPng,
jpeg: toJpeg,
blob: toBlob,
pixelData: toPixelData,
canvas: toCanvas,
} as const
// 获取 html-to-image Options
type HtmlToImageOptions = Partial<NonNullable<Parameters<typeof toPng>[1]>> type HtmlToImageOptions = Partial<NonNullable<Parameters<typeof toPng>[1]>>
interface Options< // 获取 html-to-image 方法 keys
type DomToImageMethodKeys = keyof typeof domToImageMethods
// 获取 html-to-image 方法返回值
type DomToImageReturnType<ImageType extends DomToImageMethodKeys> = Awaited<
ReturnType<(typeof domToImageMethods)[ImageType]>
>
// 自定义拓展 Options
type Options<
T extends TargetType = Element, T extends TargetType = Element,
ImageType extends DomToImageMethodKeys = 'jpeg', ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
> { > = {
/** /**
* *
* @description * @description
@ -23,7 +43,7 @@ interface Options<
* *
* @default jpeg * @default jpeg
*/ */
imageType?: DomToImageReturnType<ImageType> imageType?: ImageType
/** /**
* *
* @param element current dom * @param element current dom
@ -66,22 +86,10 @@ interface Options<
finally?: (element: T) => void finally?: (element: T) => void
} }
export type UseDomToImageOptions<T extends TargetType = Element> = Options<T> & export type UseDomToImageOptions<
HtmlToImageOptions T extends TargetType = Element,
ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
export type DomToImageMethodKeys = keyof typeof domToImageMethods > = Options<T, ImageType> & HtmlToImageOptions
type DomToImageReturnType<ImageType extends DomToImageMethodKeys = 'jpeg'> =
Awaited<ReturnType<(typeof domToImageMethods)[ImageType]>>
const domToImageMethods = {
svg: toSvg,
png: toPng,
jpeg: toJpeg,
blob: toBlob,
pixelData: toPixelData,
canvas: toCanvas,
} as const
/** /**
* *
@ -98,18 +106,24 @@ const domToImageMethods = {
* imageType 使 options.imageType * imageType 使 options.imageType
* 使 jpeg * 使 jpeg
* *
* created useDomToImage
* options.imageType
*
* @example * @example
* const refDom = ref<HTMLElement>() * const refDom = ref<HTMLElement>()
* const { create, stop } = useDomToImage(refDom, { * const { create } = useDomToImage(refDom, {
* beforeCreate: (element) => { ... }, * beforeCreate: (element) => { ... },
* created: (element, result) => { ... }, * created: (element, result) => { ... },
* createdError: (error) => { ... }, * createdError: (error) => { ... },
* finally: () => { ... }, * finally: () => { ... },
* }) * })
*/ */
export const useDomToImage = <T extends HTMLElement>( export const useDomToImage = <
T extends HTMLElement,
ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
>(
target: BasicTarget<T>, target: BasicTarget<T>,
options?: UseDomToImageOptions, options?: UseDomToImageOptions<T, ImageType>,
) => { ) => {
const { const {
beforeCreate, beforeCreate,
@ -130,12 +144,12 @@ export const useDomToImage = <T extends HTMLElement>(
if (!element) { if (!element) {
createdError?.() createdError?.()
return reject('useDomToImage: element is undefined.') return reject(`[useDomToImage]: target element is undefined.`)
} }
const imageTypeKey = (imageType ?? const imageTypeKey = (imageType ??
_imageType ?? _imageType ??
'jpeg') as keyof typeof domToImageMethods 'jpeg') as DomToImageMethodKeys
domToImageMethods[imageTypeKey]?.(element, options) domToImageMethods[imageTypeKey]?.(element, options)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

Some files were not shown because too many files have changed in this diff Show More