Compare commits

...

23 Commits
v5.0.0 ... main

Author SHA1 Message Date
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
XiaoDaiGua-Ray
ff1a67c843 version: v5.0.4 2024-11-09 14:55:41 +08:00
XiaoDaiGua-Ray
8f3969268a version: v5.0.3 2024-10-27 17:01:53 +08:00
XiaoDaiGua-Ray
7783872ef6 version: v5.0.2 2024-10-23 20:48:01 +08:00
XiaoDaiGua-Ray
707300774d version: v5.0.1 2024-10-23 11:31:25 +08:00
262 changed files with 13594 additions and 8983 deletions

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,301 +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'],
},
],
},
}

View File

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

View File

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

2
.nvmrc
View File

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

View File

@ -1,4 +1,5 @@
{
"editor.formatOnSave": true,
"i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
@ -21,6 +22,7 @@
"cSpell.words": [
"baomitu",
"bezier",
"Cascader",
"Clickoutside",
"codabar",
"commitmsg",
@ -32,6 +34,7 @@
"internalkey",
"jsbarcode",
"linebreak",
"logicflow",
"macarons",
"menutag",
"ndata",
@ -40,9 +43,10 @@
"Popselect",
"precommit",
"siderbar",
"snapline",
"stylelint",
"unocss",
"WUJIE",
"zlevel"
],
"peacock.color": "#007fff"
]
}

View File

@ -1,4 +1,272 @@
# CHANGE LOG
## 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
`ts` 版本与 `eslint` 解析插件版本更新至最新版,并且同步解决了以前历史遗留的一些问题。
并且,在该版本做了一些全局注入方式调整,请谨慎更新。
## Feats
- 移除 `vite-plugin-compression` 插件,使用 `rollup-plugin-gzip` 代替
- 更新 `@vitejs/plugin-vue-jsx` 版本至 `4.0.1`
- 优化注释
- 默认设置 `ContentWrapper``content-wrapper` 内容展示区域的宽高为 `100%`,继承父容器的宽高;该样式会自动的计算,也就是说会自动的适配不同尺寸的屏幕输出与判断是否显示 `FeatureWrapper`, `FooterWrapper`
- 更新 `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser` 版本至 `8.13.0`
- 更新 `typescript` 版本至 `5.6.3`
- 更新 `vue-tsc` 版本至 `2.1.10`
- 更新 `pnpm` 包管理器版本至 `9.12.3`
- 新增 `RFlow` 基础流程图组件(后期有时间会逐步加强该组件)
- 移除 `useElementFullscreen` 方法 `currentWindowSize` 返回值
- 新增 `--html-height`, `--html-width` 的全局 `css var` 属性,实时获取浏览器的尺寸
- 样式注入现在由注入至 `body` 改为注入至 `html`,避免 `teleport` 传送至 `body` 外的元素不能使用全局样式的问题
- `useBadge.show` 方法新增 `extraOption` 配置项,允许在显示 `badge` 到时候,传入额外的 `options` 配置项
## Fixes
- 修复菜单折叠后,`SiderBarLogo` 标题样式丢失问题
- 修复 `useModal` 因为 `Omit` 原因导致类型丢失问题,现在直接使用 `ModalProps` 作为类型
- 修复 `useModal` 创建全屏 `card` 的时候,内容区域边距样式被覆盖的问题,现在会尊重原有的 `card` 样式
## 5.0.3
个性化配置能力再次提升。
## Feats
- `SettingDrawer` 组件重构
## 5.0.2
## Feats
- 暴露 `setupAppMenu` 方法,提供自定义菜单渲染时机能力
## Fixes
- 修复 `MenuTag` 右键菜单不能被选中的问题
- 修复 `Menu` 设置 `iconSize`, `iconCollapseSize` 等属性,不能及时刷新渲染的问题,现在新增手动刷新操作按钮
> 菜单渲染是很复杂、很耗时的操作,所以在权衡以后还是考虑使用手动刷新操作方式更新菜单状态。
## 5.0.1
本次更新灵感来自于 `vben admin 5` 项目。
## Feats
- 更新了 `Menu`, `MenuTag`, `SiderBar` 样式细节
- 调整 `components`, `components-pro` 分包,现在都统一到 `components` 下,以 `base`, `pro` 进行区分,避免导出导入麻烦
- `MenuTag` 新增了一些操作方式
- `SiderBarLogo` 样式调整
## Fixes
- 新增 `tableFlexHeight` 配置项属性修复 `RTable` 组件因为不再透传至 `Table` 导致设置 `flexHeight` 进行动态高度设置失败的问题
- 修复 `RTable` 错误的 `title` 属性注入问题
- 修复 `MenuTag` 频繁右键会导致闪烁出现右键菜单的问题
## 5.0.0
@ -37,7 +305,7 @@
- 新增 `clearSigningCallback` 方法
- `vite.custom.config` 新增 `cdn` 配置项,是否启用 `cdn` 构建项目
- 配置 `cdn``false`,因为国内厂商更新资源速度有点慢,导致预览失败
- `Layout` 层注入 `--window-width`, `--window-height` `css var` 属性
- `Layout` 层注入 `--window-width`, `--window-height`, `css var` 属性
- 稳定 `Layout` 层的 `css var` 属性
## Fixes
@ -123,7 +391,7 @@
- `RChart` 组件 `setOptions` 方法配置项默认不启用 `merge` 模式
- 调整 `header` 的样式,增加了一点点间隙
- `useDevice` 新增 `observer` 配置项,可以自定义观察回调
- 新增 `components-pro` 包,助力简化业务开发
- 新增 `components` 包,助力简化业务开发
- 新增 `RTablePro` 组件,大幅简化中后台带有过滤请求条件的表格显示业务
- 新增 `RCollapse` 组件,允许折叠过滤条件

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { RModal } from '../../src/components/RModal/index'
import { RModal } from '../../src/components/base/RModal/index'
import { mount } from '@vue/test-utils'
describe('RModal', () => {

View File

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

View File

@ -17,8 +17,8 @@ describe('useDayjs', () => {
}
const localSpy = vi.spyOn(m, 'locale')
m.locale('en')
m.locale('zh-cn')
m.locale('en-US')
m.locale('zh-CN')
expect(localSpy).toHaveBeenCalledTimes(2)
})

View File

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

View File

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

View File

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

@ -15,6 +15,27 @@
--preloading-title-color: <%= preloadingConfig.titleColor %>;
--ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>;
--ray-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 {
@ -23,13 +44,9 @@
right: 0;
top: 0;
bottom: 0;
background-color: #ffffff;
color: var(--preloading-title-color);
text-align: center;
}
.ray-template--dark #pre-loading-animation {
background-color: #2a3146;
background-color: var(--global-loading-bg-color);
}
#pre-loading-animation .pre-loading-animation__wrapper {
@ -95,6 +112,18 @@
}
}
</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>
<div id="app"></div>
<div id="pre-loading-animation">

View File

@ -1,11 +1,11 @@
{
"name": "ray-template",
"private": false,
"version": "5.0.0",
"version": "5.2.2",
"type": "module",
"engines": {
"node": "^18.0.0 || >=20.0.0",
"pnpm": ">=8.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"pnpm": ">=9.0.0"
},
"scripts": {
"dev": "vite",
@ -16,7 +16,7 @@
"prepare": "husky install",
"test": "vitest",
"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": {
"hooks": {
@ -29,82 +29,85 @@
"prettier --write"
],
"*.{ts,tsx,vue}": [
"eslint src"
"eslint --fix"
]
},
"dependencies": {
"@vueuse/core": "^11.1.0",
"axios": "^1.7.5",
"@logicflow/core": "2.0.10",
"@logicflow/extension": "2.0.14",
"@vueuse/core": "^13.1.0",
"axios": "^1.9.0",
"clipboard": "^2.0.11",
"crypto-js": "4.2.0",
"currency.js": "^2.0.4",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"html-to-image": "1.11.11",
"interactjs": "1.10.26",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"html-to-image": "1.11.13",
"interactjs": "1.10.27",
"jsbarcode": "3.11.6",
"lodash-es": "^4.17.21",
"mockjs": "1.1.0",
"naive-ui": "^2.40.1",
"pinia": "^2.2.4",
"pinia-plugin-persistedstate": "^4.1.1",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"print-js": "^1.6.0",
"vue": "^3.5.12",
"vue-demi": "0.14.6",
"vue-hooks-plus": "2.2.1",
"vue": "^3.5.17",
"vue-demi": "0.14.10",
"vue-hooks-plus": "2.4.0",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.2",
"vue3-next-qrcode": "2.0.10"
"vue-router": "^4.5.1",
"vue3-next-qrcode": "3.0.2"
},
"devDependencies": {
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@interactjs/types": "1.10.21",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/crypto-js": "^4.2.2",
"@types/dom-to-image": "2.6.7",
"@commitlint/cli": "19.7.1",
"@commitlint/config-conventional": "19.7.1",
"@eslint/js": "9.28.0",
"@interactjs/types": "1.10.27",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@types/crypto-js": "4.2.2",
"@types/jsbarcode": "3.11.4",
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "1.0.7",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.1.0",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vitest/ui": "1.4.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "2.4.3",
"autoprefixer": "^10.4.16",
"depcheck": "^1.4.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.25.0",
"happy-dom": "14.3.1",
"@types/lodash-es": "4.17.12",
"@types/mockjs": "1.0.10",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@vitejs/plugin-vue": "5.2.3",
"@vitejs/plugin-vue-jsx": "4.1.2",
"@vitest/ui": "2.1.8",
"@vue/eslint-config-prettier": "10.1.0",
"@vue/eslint-config-typescript": "14.2.0",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21",
"depcheck": "1.4.7",
"eslint": "9.20.1",
"eslint-config-prettier": "10.1.2",
"eslint-plugin-prettier": "5.2.6",
"eslint-plugin-vue": "9.32.0",
"globals": "16.0.0",
"happy-dom": "17.1.0",
"husky": "8.0.3",
"lint-staged": "^15.2.0",
"postcss": "^8.4.38",
"lint-staged": "15.4.3",
"postcss": "8.5.4",
"postcss-px-to-viewport-8-with-include": "1.2.2",
"prettier": "^3.2.5",
"sass": "1.71.1",
"svg-sprite-loader": "^6.0.11",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.3",
"vite-bundle-analyzer": "0.9.4",
"prettier": "3.5.3",
"rollup-plugin-gzip": "4.0.1",
"sass": "1.86.3",
"svg-sprite-loader": "6.0.11",
"typescript": "5.8.3",
"unocss": "66.3.3",
"unplugin-auto-import": "19.1.2",
"unplugin-vue-components": "0.28.0",
"vite": "6.3.5",
"vite-bundle-analyzer": "0.16.0",
"vite-plugin-cdn2": "1.1.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-ejs": "1.7.0",
"vite-plugin-eslint": "1.8.1",
"vite-plugin-inspect": "^0.8.3",
"vite-plugin-mock-dev-server": "1.4.7",
"vite-plugin-svg-icons": "^2.0.1",
"vite-svg-loader": "^4.0.0",
"vite-tsconfig-paths": "4.3.2",
"vitest": "1.5.2",
"vue-tsc": "^2.0.13"
"vite-plugin-inspect": "0.8.4",
"vite-plugin-mock-dev-server": "1.8.3",
"vite-plugin-svg-icons": "2.0.1",
"vite-svg-loader": "5.1.0",
"vitest": "2.1.8",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.8"
},
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
"main": "index.ts",

13958
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -46,7 +46,7 @@ const AppAvatar = defineComponent({
const { getSigningCallback, avatarSize, spaceSize, $props, vertical } = this
return (
<NButton quaternary strong>
<NButton quaternary strong focusable={false}>
<NFlex align="center" size={spaceSize} vertical={vertical}>
<NAvatar
{...($props as AvatarProps)}
@ -54,7 +54,14 @@ const AppAvatar = defineComponent({
objectFit="cover"
round
size={avatarSize}
/>
>
{{
default: () =>
getSigningCallback.avatar
? null
: getSigningCallback?.name?.[0],
}}
</NAvatar>
{getSigningCallback?.name}
</NFlex>
</NButton>

View File

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

View File

@ -1,17 +1,10 @@
/**
*
*
*
* ,
*/
import { useStorage } from '@vueuse/core'
import { APP_CATCH_KEY } from '@/app-config'
const appLockScreen = useStorage(
APP_CATCH_KEY.isAppLockScreen,
false,
sessionStorage,
window.localStorage,
{
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 { 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({
name: 'LockScreen',
setup() {
const formInstRef = ref<FormInst | null>(null)
const inputInstRef = ref<InputInst | null>(null)
const [register, { validate }] = useForm()
const inputInstRef = useTemplateRef<InputInst | null>('inputInstRef')
const { setLockAppScreen } = useAppLockScreen()
const { updateSettingState } = useSettingActions()
@ -19,15 +25,17 @@ const LockScreen = defineComponent({
lockCondition: useCondition(),
})
/** 锁屏 */
const lockScreen = () => {
formInstRef.value?.validate((error) => {
if (!error) {
setLockAppScreen(true)
updateSettingState('lockScreenSwitch', true)
validate().then(() => {
setLockAppScreen(true)
updateSettingState('lockScreenSwitch', false)
setStorage(
APP_CATCH_KEY.appLockScreenPasswordKey,
encrypt(state.lockCondition.lockPassword),
'localStorage',
)
state.lockCondition = useCondition()
}
state.lockCondition = useCondition()
})
}
@ -40,40 +48,51 @@ const LockScreen = defineComponent({
return {
...toRefs(state),
lockScreen,
formInstRef,
register,
inputInstRef,
}
},
render() {
const { register } = this
return (
<div class="app-lock-screen__input">
<NForm
ref="formInstRef"
model={this.lockCondition}
rules={rules}
labelPlacement="left"
>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入锁屏密码"
clearable
showPasswordOn="click"
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
this.lockScreen()
}
}}
/>
</NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton>
</NForm>
<div class="app-lock-screen__content">
<div class="app-lock-screen__input">
<AppAvatar
avatarSize={52}
style="pointer-events: none;margin: 24px 0;"
vertical
/>
<RForm
ref="formInstRef"
model={this.lockCondition}
rules={rules}
labelPlacement="left"
onRegister={register}
>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入锁屏密码"
clearable
showPasswordOn="click"
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
this.lockScreen()
}
}}
autofocus
/>
</NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton>
</RForm>
</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 { RForm } from '@/components'
import dayjs from 'dayjs'
import { useSigningActions, useSettingActions } from '@/store'
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useDevice } from '@/hooks'
import type { FormInst, InputInst } from 'naive-ui'
import { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { removeStorage, decrypt, getStorage } from '@/utils'
export default defineComponent({
name: 'UnlockScreen',
setup() {
const formRef = ref<FormInst | null>(null)
const inputInstRef = ref<InputInst | null>(null)
const [register, { validate }] = useForm()
const { logout } = useSigningActions()
const { updateSettingState } = useSettingActions()
@ -22,13 +25,13 @@ export default defineComponent({
const HH_MM_FORMAT = 'HH:mm'
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 state = reactive({
lockCondition: useCondition(),
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),
DDD: dayjs().format(DDD_FORMAT),
})
@ -41,30 +44,55 @@ export default defineComponent({
state.DDD = dayjs().format(DDD_FORMAT)
}, 86_400_000)
/** 退出登陆并且回到登陆页 */
const toSigningFn = () => {
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
updateSettingState('lockScreenSwitch', false)
setTimeout(() => {
logout()
}, 100)
}
const backToSigning = () => {
window.$dialog.warning({
title: '警告',
content: '是否返回到登陆页?',
content: '是否返回到登陆页并且重新登录',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
logout()
setTimeout(() => {
updateSettingState('lockScreenSwitch', false)
})
},
negativeText: '重新登录',
onPositiveClick: toSigningFn,
})
}
/** 解锁 */
const unlockScreen = () => {
formRef.value?.validate((error) => {
if (!error) {
const catchPassword = getStorage<string>(
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)
updateSettingState('lockScreenSwitch', false)
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
state.lockCondition = useCondition()
} else {
window.$message.warning('密码错误,请重新输入')
}
})
}
@ -78,71 +106,84 @@ export default defineComponent({
...toRefs(state),
backToSigning,
unlockScreen,
formRef,
inputInstRef,
isTabletOrSmaller,
register,
}
},
render() {
const { isTabletOrSmaller } = this
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
const hmSplit = HH_MM.split(':')
const { unlockScreen, backToSigning } = this
const { unlockScreen, backToSigning, register } = this
return (
<div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content-wrapper">
<div
class={[
'app-lock-screen__unlock__content-bg__wrapper',
'app-lock-screen__unlock__content-bg',
isTabletOrSmaller
? 'app-lock-screen__unlock__content-bg--smaller'
: '',
]}
>
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
<div class="app-lock-screen__content app-lock-screen__content--full">
<div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content-wrapper">
<div
class={[
'app-lock-screen__unlock__content-bg__wrapper',
'app-lock-screen__unlock__content-bg',
isTabletOrSmaller
? 'app-lock-screen__unlock__content-bg--smaller'
: '',
]}
>
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
</div>
</div>
</div>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar avatarSize={52} style="pointer-events: none;" vertical />
</div>
<div class="app-lock-screen__unlock__content-input">
<NForm ref="formRef" model={this.lockCondition} rules={rules}>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
}
}}
/>
</NFormItem>
<NFlex justify="space-between">
<NButton type="primary" text onClick={backToSigning.bind(this)}>
</NButton>
<NButton type="primary" text onClick={unlockScreen.bind(this)}>
</NButton>
</NFlex>
</NForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-date">
{HH_MM}&nbsp;<span>{AM_PM}</span>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar
avatarSize={52}
style="pointer-events: none;"
vertical
/>
</div>
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>
<div class="app-lock-screen__unlock__content-input">
<RForm
onRegister={register}
model={this.lockCondition}
rules={rules}
>
<NFormItem path="lockPassword">
<NInput
autofocus
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
}
}}
/>
</NFormItem>
<NFlex justify="space-between">
<NButton
type="primary"
text
onClick={backToSigning.bind(this)}
>
</NButton>
<NButton
type="primary"
text
onClick={unlockScreen.bind(this)}
>
</NButton>
</NFlex>
</RForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>&nbsp;<span>{AM_PM}</span>
</div>
</div>
</div>
</div>

View File

@ -1,4 +1,10 @@
.app-lock-screen__content {
&.app-lock-screen__content--full {
width: 100%;
height: var(--html-height);
@include flexCenter;
}
& .app-lock-screen__input {
& button[class*='n-button'] {
width: 100%;
@ -30,7 +36,7 @@
width: 100%;
height: 100%;
@include flexCenter;
font-size: 320px;
font-size: 16.67rem;
gap: 80px;
z-index: 0;
@ -79,9 +85,23 @@
& .current-year,
& .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 LockScreen from './components/LockScreen'
import UnlockScreen from './components/UnlockScreen'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useSettingGetters, useSettingActions } from '@/store'
const AppLockScreen = defineComponent({
name: 'AppLockScreen',
setup() {
const { getLockAppScreen } = useAppLockScreen()
const { updateSettingState } = useSettingActions()
const { getLockScreenSwitch } = useSettingGetters()
const lockScreenSwitchRef = computed({
@ -28,12 +17,9 @@ const AppLockScreen = defineComponent({
return {
lockScreenSwitchRef,
getLockAppScreen,
}
},
render() {
const { getLockAppScreen } = this
return (
<RModal
v-model:show={this.lockScreenSwitchRef}
@ -42,12 +28,10 @@ const AppLockScreen = defineComponent({
autoFocus={false}
maskClosable={false}
closeOnEsc={false}
preset={!getLockAppScreen() ? 'dialog' : void 0}
preset="dialog"
title="锁定屏幕"
>
<div class="app-lock-screen__content">
{!getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
</div>
<LockScreen />
</RModal>
)
},

View File

@ -7,7 +7,8 @@ import {
getStorage,
} from '@/utils'
import { useSettingGetters } from '@/store'
import { APP_CATCH_KEY } from '@/app-config'
import { APP_CATCH_KEY, GLOBAL_CLASS_NAMES, APP_THEME } from '@/app-config'
import { useWindowSize } from '@vueuse/core'
import type { SettingState } from '@/store/modules/setting/types'
@ -15,40 +16,50 @@ export default defineComponent({
name: 'AppStyleProvider',
setup(_, { expose }) {
const { getAppTheme } = useSettingGetters()
const { height, width } = useWindowSize()
/** 同步主题色变量至 body, 如果未获取到缓存值则已默认值填充 */
// 同步主题色变量至 html如果未获取到缓存值则已默认值填充
const syncPrimaryColorToBody = () => {
// 默认主题色
const {
appPrimaryColor: { primaryColor, primaryFadeColor },
} = __APP_CFG__ // 默认主题色
const body = document.body
} = APP_THEME
// 主题色配置 class 名
const { rayTemplateThemePrimaryColor, rayTemplateThemePrimaryFadeColor } =
GLOBAL_CLASS_NAMES
const html = document.documentElement
// 获取缓存 naive ui 配置项
const primaryColorOverride = getStorage<SettingState>(
APP_CATCH_KEY.appPiniaSettingStore,
'localStorage',
) // 获取缓存 naive ui 配置项
)
if (primaryColorOverride) {
// 获取主色调
const p = get(
primaryColorOverride,
'primaryColorOverride.common.primaryColor',
primaryColor,
) // 获取主色调
const fp = colorToRgba(p, 0.38) // 将主色调任意颜色转换为 rgba 格式
)
// 将主色调任意颜色转换为 rgba 格式
const fp = colorToRgba(p, 0.85)
/** 设置全局主题色 css 变量 */
body.style.setProperty('--ray-theme-primary-color', p) // 主色调
body.style.setProperty(
'--ray-theme-primary-fade-color',
// 设置全局主题色 css 变量
html.style.setProperty(rayTemplateThemePrimaryColor, p) // 主色调
// 降低透明度后的主色调
html.style.setProperty(
rayTemplateThemePrimaryFadeColor,
fp || primaryFadeColor,
) // 降低透明度后的主色调
)
}
}
/** 隐藏加载动画 */
// 隐藏加载动画
const hiddenLoadingAnimation = () => {
/** pre-loading-animation 是默认 id */
const el = document.getElementById('pre-loading-animation')
// pre-loading-animation 是默认 id
const el = document.getElementById(GLOBAL_CLASS_NAMES.preLoadingAnimation)
if (el) {
setStyle(el, {
@ -57,38 +68,30 @@ export default defineComponent({
}
}
/** 切换主题时, 同步更新 body class 以便于进行自定义 css 配置 */
// 切换主题时,同步更新 html class 以便于进行自定义 css 配置
const updateGlobalThemeClass = (bool: boolean) => {
/**
*
* body class
*
* getAppTheme
*/
const body = document.body
const darkClassName = 'ray-template--dark' // 暗色类名
const lightClassName = 'ray-template--light' // 明亮色类名
const html = document.documentElement
const { darkClassName, lightClassName } = GLOBAL_CLASS_NAMES
bool
? removeClass(body, lightClassName)
: removeClass(body, darkClassName)
? removeClass(html, lightClassName)
: removeClass(html, darkClassName)
setClass(body, bool ? darkClassName : lightClassName)
setClass(html, bool ? darkClassName : lightClassName)
}
syncPrimaryColorToBody()
hiddenLoadingAnimation()
// 当切换主题时,更新 body 当前的注入 class
watch(
() => getAppTheme.value,
(ndata) => {
updateGlobalThemeClass(ndata)
},
{
immediate: true,
},
)
watchEffect(() => {
// 当切换主题时,更新 html 当前的注入 class
updateGlobalThemeClass(getAppTheme.value)
// 注入全局宽高尺寸
setStyle(document.documentElement, {
[GLOBAL_CLASS_NAMES.htmlHeight]: `${height.value}px`,
[GLOBAL_CLASS_NAMES.htmlWidth]: `${width.value}px`,
})
})
expose()
},

View File

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

View File

@ -26,7 +26,7 @@ export default defineComponent({
const { getWatermarkConfig, getWatermarkSwitch } = this
return getWatermarkSwitch ? (
<NWatermark cross fullscreen {...getWatermarkConfig} />
<NWatermark {...getWatermarkConfig} fullscreen />
) : null
},
})

View File

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

View File

@ -3,35 +3,36 @@ import type { AppTheme } from '@/types'
export const APP_THEME: AppTheme = {
/**
*
*
* RGBARGB
* @description
*
* RGBARGB
*/
appThemeColors: [
'#2d8cf0',
'#3f9eff',
'#ff42bc',
'#ee4f12',
'#a6e4f7',
'#dbcb02',
'#18A058',
'#18a058',
],
/** 系统主题色 */
// 系统主题色
appPrimaryColor: {
/** 主题色 */
// 主题色
primaryColor: '#2d8cf0',
/** 主题辅助色(用于整体 hover、active 等之类颜色) */
primaryFadeColor: 'rgba(45, 140, 240, 0.3)',
// 主题辅助色(用于整体 hover、active 等之类颜色)
primaryFadeColor: 'rgba(45, 140, 240, 0.85)',
},
/**
*
* naive-ui
* : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>
* @description
* naive-ui
* : <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>。
*
* :
* - 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>
*
* @example
@ -47,17 +48,24 @@ export const APP_THEME: AppTheme = {
* ```
*/
appNaiveUIThemeOverrides: {
dark: {},
light: {},
},
appNaiveUIThemeOverridesCommon: {
dark: {},
light: {},
dark: {
common: {
borderRadius: '4px',
baseColor: 'rgb(18, 18, 18)',
},
},
light: {
common: {
borderRadius: '4px',
baseColor: 'rgb(255, 255, 255)',
},
},
},
/**
*
* echart
* json xxx-dark.json
* @description
* echart
* json xxx-dark.json
*
* [](https://xiaodaigua-ray.github.io/ray-template-doc/ray-template-docs/advanced/echart-themes.html)
*/

View File

@ -8,4 +8,12 @@ export const APP_REGEX: Record<string, RegExp> = {
/** css 尺寸单位匹配 */
cssUnit:
/^\d+(\.\d+)?(px|em|rem|%|vw|vh|vmin|vmax|cm|mm|in|pt|pc|ch|ex|q|s|ms|deg|rad|turn|grad|hz|khz|dpi|dpcm|dppx|fr|auto)$/,
/**
*
* @description
* css auto, unset, fit-content, max-content, min-content, initial, inherit, revert, revert-layer, -webkit-fill-available,
* -webkit-max-content, -webkit-min-content, -webkit-revert, -webkit-revert-layer, -webkit-fill-available
*/
cssSize:
/^auto|unset|fit-content|max-content|min-content|initial|inherit|revert|revert-layer|[-\w]+-webkit-fill-available$/,
}

View File

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

View File

@ -16,7 +16,7 @@ import type {
* request instance ,
*/
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) {
// TODO: 根据 url 不同是否设置 token

View File

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

View File

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

View File

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

View File

@ -1,41 +0,0 @@
import { RCollapseGrid, RForm } from '@/components'
import { collapseGridProps, formProps } from '@/components'
import type { FormProps, GridProps } from 'naive-ui'
/**
*
* @description
* RCollapseGrid RForm
* RCollapseGrid 使 NFormItemGi, NGridItem
*/
export default defineComponent({
name: 'RCollapse',
props: Object.assign({}, formProps, {
...collapseGridProps,
open: {
type: Boolean,
default: true,
},
cols: {
type: Number,
default: 4,
},
bordered: {
type: Boolean,
default: true,
},
}),
render() {
const { $slots, $props } = this
const { labelPlacement, showFeedback, ...rest } = $props as FormProps &
GridProps
return (
<RForm {...rest} labelPlacement="top" showFeedback={false}>
<RCollapseGrid {...rest}>{$slots}</RCollapseGrid>
</RForm>
)
},
})

View File

@ -1,75 +0,0 @@
import type { TableProps, RTableInst } from '@/components'
import type { UsePaginationReturn } from '@/hooks'
import type { Recordable } from '@/types'
export type FormatRangeTime = {
/**
*
* @description
*
*/
key: string | number
/**
*
* @description
*
*/
target: [string | number, string | number]
}
/**
*
* @description
* Pagination
*/
export type TablePagination = Pick<
UsePaginationReturn[1],
'getItemCount' | 'getPage' | 'getPageSize'
>
export interface TableRequestConfig<Params = Recordable> {
/**
*
* @description
*
* @default undefined
*/
params?: Params
/**
*
* @description
*
* NDatePicker range
*
*
* @default undefined
*/
formatRangeTime?: FormatRangeTime[]
}
export type TableProProps = Omit<TableProps, 'pagination'>
export interface TableProInst extends Omit<RTableInst, 'getTableInstance'> {
/**
*
* @description
* pagination
*/
getTablePagination: () => TablePagination
/**
*
* @description
*
*/
runTableRequest: (extraConfig?: TableRequestConfig) => void
/**
*
* @param extraConfig
*
* @description
*
*/
getCurrentTableRequestParams: <T = Recordable>(
extraConfig?: TableRequestConfig<T>,
) => TableRequestConfig<T>['params'] & Recordable
}

View File

@ -1,4 +0,0 @@
import RCollapse from './RCollapse/Collapse'
export * from './RTablePro'
export { RCollapse }

View File

@ -1,42 +0,0 @@
import { NForm } from 'naive-ui'
import props from './props'
import { call } from '@/utils'
import { useTemplateRef } from 'vue'
import type { RFormInst } from './types'
import type { FormProps } from 'naive-ui'
export default defineComponent({
name: 'RForm',
props,
setup(props, { expose }) {
const formRef = useTemplateRef<RFormInst>('formRef')
onMounted(() => {
// 主动调用 register 方法,满足 useForm 方法正常调用
const { onRegister } = props
if (onRegister && formRef.value) {
call(onRegister, formRef.value)
}
})
expose()
return {
formRef,
}
},
render() {
const { $attrs, $props, $slots } = this
return (
<NForm {...$attrs} {...($props as FormProps)} ref="formRef">
{{
...$slots,
}}
</NForm>
)
},
})

View File

@ -1,114 +0,0 @@
import { cloneDeep } from 'lodash-es'
import type {
RFormInst,
FormValidateCallback,
ShouldRuleBeApplied,
RFormRules,
} from '../types'
import type { Recordable } from '@/types'
/**
*
* @description
* RForm
* 便
*
* @warning
* register 使
* 使 hooks register
* create hook使 nextTick
*
* @example
* defineComponent({
* setup() {
* const [register, { ...Hooks }] = useForm()
*
* return {
* register,
* ...Hooks,
* }
* },
* render() {
* const { register, ...Hooks } = this
*
* return <RForm onRegister={register} />
* },
* })
*/
const useForm = <T extends Recordable, R extends RFormRules>(
model?: T,
rules?: R,
) => {
const formRef = ref<RFormInst>()
const register = (inst: RFormInst) => {
if (inst) {
formRef.value = inst
}
}
const getFormInstance = () => {
if (!formRef.value) {
throw new Error(
'[useForm]: form instance is not ready yet. if you are using useForm, please make sure you have called register method in onRegister event.',
)
}
return formRef.value
}
/**
*
* @description
* Promise rejection FormValidationError[]
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#inline.vue
*/
const validate = (
callback?: FormValidateCallback,
shouldRuleBeApplied?: ShouldRuleBeApplied,
) => getFormInstance().validate.call(null, callback, shouldRuleBeApplied)
/**
*
* @description
*
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#Form-Methods
*/
const restoreValidation = () => getFormInstance().restoreValidation.call(null)
/**
*
* @description
*
*
* useForm model
*/
const formModel = () => cloneDeep(model) || ({} as T)
/**
*
* @description
*
*
* useForm rules
*/
const formRules = () => cloneDeep(rules) || ({} as R)
return [
register,
{
getFormInstance,
validate,
restoreValidation,
formModel,
formRules,
},
] as const
}
export type UseFormReturn = ReturnType<typeof useForm>
export default useForm

View File

@ -1,24 +0,0 @@
import { formProps } from 'naive-ui'
import type { MaybeArray } from '@/types'
import type { RFormInst } from './types'
const props = {
...formProps,
/**
*
* @description
* RForm
* useForm register 使便使 hooks
*
* @default null
*/
onRegister: {
type: [Function, Array] as PropType<
MaybeArray<(formInst: RFormInst) => void>
>,
default: null,
},
} as const
export default props

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

@ -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

@ -36,6 +36,7 @@ import {
import { RMoreDropdown } from '@/components'
import { useSettingGetters } from '@/store'
import { useTemplateRef } from 'vue'
import { USE_CHART_PROVIDER_KEY } from './config'
import type { WatchStopHandle } from 'vue'
import type { AnyFC } from '@/types'
@ -124,6 +125,7 @@ export default defineComponent({
const __catch = {
aria: props.showAria,
}
const chartProvideOptions = inject(USE_CHART_PROVIDER_KEY, {})
/**
*
@ -174,10 +176,19 @@ export default defineComponent({
* echartTheme 使
*/
const updateChartTheme = () => {
const { theme: providerTheme } = chartProvideOptions || {}
if (echartInstanceRef.value) {
destroyChart()
}
// 如果配置了全局配置主题,则忽略后面所有逻辑
if (providerTheme) {
renderChart(providerTheme)
return
}
if (props.theme === 'default') {
props.autoChangeTheme ? renderChart('dark') : renderChart('')

View File

@ -1,7 +1,7 @@
import type {
ChartThemeRawArray,
ChartThemeRawModules,
} from '@/components/RChart/src/types'
} from '@/components/base/RChart/src/types'
/**
*

View File

@ -1,6 +1,6 @@
import { useTheme } from '@/hooks'
import type { LoadingOptions } from '@/components/RChart/src/types'
import type { LoadingOptions } from '@/components/base/RChart/src/types'
/**
*

View File

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

View File

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

@ -0,0 +1,9 @@
import RFlow from './src/Flow'
import flowProps from './src/props'
import { useFlow } from './src/hooks'
import type { ExtractPublicPropTypes } from 'vue'
export type FlowProps = ExtractPublicPropTypes<typeof flowProps>
export { RFlow, flowProps, useFlow }

View File

@ -0,0 +1,155 @@
import './index.scss'
import '@logicflow/core/lib/style/index.css'
import { useTemplateRef } from 'vue'
import props from './props'
import { completeSize, call } from '@/utils'
import LogicFlow from '@logicflow/core'
import { omit } from 'lodash-es'
import type { FlowGraphData, G } from './types'
import type { WatchStopHandle } from 'vue'
// 是否首次注册插件
let isSetup = false
export default defineComponent({
name: 'RFlow',
props,
setup(props) {
// 流程图 dom 实例
const flowDomRef = useTemplateRef<HTMLElement>('flowDomRef')
// css 变量
const cssVars = computed(() => {
const { width, height } = props
const cssVar = {
'--r-flow-width': completeSize(width),
'--r-flow-height': completeSize(height),
}
return cssVar
})
// 流程图实例
const logicFlowInstRef = shallowRef<LogicFlow>()
// 需要禁用的流程图配置项
const readonlyOptions = {
nodeTextEdit: false,
edgeTextEdit: false,
textEdit: false,
}
// watchData 回调
let watchDataStop: WatchStopHandle
// 默认流程图数据
const defaultGraphData = {
nodes: [],
edges: [],
}
const cacheProps = {
readonly: props.readonly,
}
// 注册流程图插件
const registerExtension = () => {
if (!isSetup) {
props.use?.filter(Boolean).forEach((curr) => LogicFlow.use(curr))
isSetup = true
}
}
// 动态根据 readonly 配置项修改流程图配置
const updateFlowConfig = (bool: boolean) => {
if (!logicFlowInstRef.value) {
return
}
const ops = Object.entries(readonlyOptions).reduce(
(acc, [key]) => {
acc[key as keyof typeof acc] = !bool
return acc
},
{} as typeof readonlyOptions,
)
// 单独处理 isSilentMode 配置项
Object.assign(readonlyOptions, ops, {
isSilentMode: bool,
})
logicFlowInstRef.value.updateEditConfig(readonlyOptions)
}
/**
*
* @param graphData
*
* @description
*
*
* container 使
*/
const setupFlowRender = (graphData?: FlowGraphData) => {
registerExtension()
if (!flowDomRef.value) {
return
}
const { options, readonly } = props
// 初始化流程图实例
logicFlowInstRef.value = new LogicFlow({
container: unref(flowDomRef.value),
...omit(options, 'container'),
})
// 渲染
logicFlowInstRef.value.render((graphData || defaultGraphData) as G)
// 是否处于只读模式,如果是只读模式,则覆盖 options 配置项为 readonlyOptions
updateFlowConfig(readonly)
}
watchEffect(() => {
if (props.watchData) {
watchDataStop = watch(
() => props.data,
(ndata) => {
if (logicFlowInstRef.value) {
ndata && logicFlowInstRef.value.renderRawData(ndata as G)
} else {
setupFlowRender(ndata)
}
},
)
} else {
watchDataStop?.()
}
if (props.readonly !== cacheProps.readonly) {
updateFlowConfig(props.readonly)
cacheProps.readonly = props.readonly
}
})
onMounted(() => {
setupFlowRender()
const { onRegister } = props
if (onRegister && logicFlowInstRef.value) {
call(onRegister, logicFlowInstRef.value)
}
})
return {
flowDomRef,
cssVars,
}
},
render() {
const { cssVars } = this
return <div class="r-flow" style={[cssVars]} ref="flowDomRef"></div>
},
})

View File

@ -0,0 +1,17 @@
import type { FlowOptions } from './types'
/**
*
* @description
* RFlow
* 使 container使
*/
export const getDefaultFlowOptions = (): FlowOptions => {
return {
grid: true,
partial: false,
keyboard: {
enabled: true,
},
}
}

View File

@ -0,0 +1 @@
export { useFlow } from './useFlow'

View File

@ -0,0 +1,47 @@
import type LogicFlow from '@logicflow/core'
export const useFlow = () => {
let flowInst: LogicFlow
/**
*
* @param inst flow instance
*
* @description
* flow 使 useFlow hook
*/
const register = (inst: LogicFlow) => {
if (inst) {
flowInst = inst
}
}
/**
*
* @description
* flow
*
* onRegister
*
* @example
* const [register, { getFlowInstance }] = useFlow()
*
* const inst = getFlowInstance()
*/
const getFlowInstance = () => {
if (!flowInst) {
throw new Error(
'[useFlow]: flow instance is not ready yet. if you are using useFlow, please make sure you have called register method in onRegister event.',
)
}
return flowInst
}
return [
register,
{
getFlowInstance,
},
] as const
}

View File

@ -0,0 +1,10 @@
.r-flow {
width: var(--r-flow-width);
height: var(--r-flow-height);
}
.lf-text-input,
.lf-control-text,
.lf-menu-item {
color: initial;
}

View File

@ -0,0 +1,104 @@
import { getDefaultFlowOptions } from './constant'
import type { FlowGraphData, FlowOptions, ExtensionType } from './types'
import type LogicFlow from '@logicflow/core'
import type { MaybeArray } from '@/types'
const props = {
/**
*
* @description
*
*
*
* css
*
* @see https://site.logic-flow.cn/tutorial/extension/intro
*/
use: {
type: Array as PropType<ExtensionType[]>,
default: void 0,
},
/**
*
* @description
* data
*/
watchData: {
type: Boolean,
default: true,
},
/**
*
* @description
*
*
* options
*
* @default false
*/
readonly: {
type: Boolean,
default: false,
},
/**
*
* @description
*
*
* @default '100%'
*/
width: {
type: [String, Number] as PropType<string | number>,
default: '100%',
},
/**
*
* @description
*
*
* @default '100%'
*/
height: {
type: [String, Number] as PropType<string | number>,
default: '100%',
},
/**
*
* @description
*
*
* @default undefined
*/
data: {
type: Object as PropType<FlowGraphData>,
default: void 0,
},
/**
*
* @description
*
*
* @default undefined
*/
options: {
type: Object as PropType<FlowOptions>,
default: getDefaultFlowOptions(),
},
/**
*
* @description
* RFlow
* useFlow register 使便使 hooks
*
* @default undefined
*/
onRegister: {
type: [Function, Array] as PropType<
MaybeArray<(flowInst: LogicFlow) => void>
>,
default: void 0,
},
}
export default props

View File

@ -0,0 +1,61 @@
import type LogicFlow from '@logicflow/core'
import type { Recordable, SetRequired } from '@/types'
/**
*
* @description
* Omit
* Options
*
*
*/
type OptionsPickKeys =
| 'width'
| 'height'
| 'background'
| 'grid'
| 'partial'
| 'keyboard'
| 'style'
| 'edgeType'
| 'adjustEdge'
| 'textMode'
| 'edgeTextMode'
| 'nodeTextMode'
| 'allowRotate'
| 'allowResize'
| 'isSilentMode'
| 'stopScrollGraph'
| 'stopZoomGraph'
| 'stopMoveGraph'
| 'animation'
| 'history'
| 'outline'
| 'snapline'
| 'textEdit'
| 'guards'
| 'overlapMode'
| 'plugins'
| 'pluginsOptions'
| 'disabledPlugins'
| 'disabledTools'
| 'idGenerator'
| 'edgeGenerator'
| 'customTrajectory'
export type G = Parameters<LogicFlow['render']>[0]
export type NodeConfig = SetRequired<NonNullable<G['nodes']>[0], 'id'> &
Recordable
export type EdgeConfig = SetRequired<NonNullable<G['edges']>[0], 'id'> &
Recordable
export interface FlowGraphData {
nodes?: NodeConfig[]
edges?: EdgeConfig[]
}
export type FlowOptions = Pick<LogicFlow['options'], OptionsPickKeys>
export type ExtensionType = Parameters<typeof LogicFlow.use>[0]

View File

@ -0,0 +1,85 @@
import { NForm, NSpin } from 'naive-ui'
import props from './props'
import { call, unrefElement } from '@/utils'
import { useTemplateRef } from 'vue'
import { useEventListener } from '@vueuse/core'
import type { RFormInst } from './types'
import type { ShallowRef } from 'vue'
export default defineComponent({
name: 'RForm',
props,
setup(props, { expose }) {
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(() => {
// 主动调用 register 方法,满足 useForm 方法正常调用
const { onRegister } = props
if (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()
return {
formRef,
}
},
render() {
const { $attrs, $props, $slots } = this
const { loading, loadingDescription, ...restProps } = $props
return (
<NSpin
show={loading}
description={loadingDescription}
style={{
height: 'auto',
}}
>
<NForm {...$attrs} {...restProps} ref="formRef">
{{
...$slots,
}}
</NForm>
</NSpin>
)
},
})

View File

@ -0,0 +1,256 @@
import { cloneDeep } from 'lodash-es'
import type {
RFormInst,
FormValidateCallback,
ShouldRuleBeApplied,
RFormRules,
} from '../types'
import type { Recordable } from '@/types'
/**
*
* @description
* RForm
* 便
*
* @warning
* register 使
* 使 hooks register
* create hook使 nextTick
*
* @example
* defineComponent({
* setup() {
* const [register, { ...Hooks }] = useForm()
*
* return {
* register,
* ...Hooks,
* }
* },
* render() {
* const { register, ...Hooks } = this
*
* return <RForm onRegister={register} />
* },
* })
*/
const useForm = <
T extends Recordable = Recordable,
R extends RFormRules = RFormRules,
>(
model?: T | (() => T),
rules?: R | (() => R),
) => {
const formRef = shallowRef<RFormInst>()
const formModelRef = ref<T>()
const register = (inst: RFormInst) => {
if (inst) {
formRef.value = inst
}
}
const getFormInstance = () => {
if (!formRef.value) {
throw new Error(
'[useForm]: form instance is not ready yet. if you are using useForm, please make sure you have called register method in onRegister event.',
)
}
return formRef.value
}
// 初始化 formModelRef 的值,根据 model 的类型进行初始化
const initialFormModel = () => {
if (typeof model === 'function') {
formModelRef.value = model() ?? ({} as T)
} else {
formModelRef.value = cloneDeep(model) ?? ({} as T)
}
}
/**
*
* @description
* Promise rejection FormValidationError[]
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#inline.vue
*/
const validate = (
callback?: FormValidateCallback,
shouldRuleBeApplied?: ShouldRuleBeApplied,
) => getFormInstance().validate.call(null, callback, shouldRuleBeApplied)
/**
*
* @description
*
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#Form-Methods
*/
const restoreValidation = () => getFormInstance().restoreValidation.call(null)
/**
*
* @description
*
* 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 = (): T & Recordable => {
if (typeof model === 'function') {
return model()
}
return cloneDeep(model) || ({} as T)
}
/**
*
* @description
*
*
* useForm rules
*/
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 [
register,
{
getFormInstance,
validate,
restoreValidation,
formModel,
formRules,
reset,
validateTargetField,
formConditionRef: formModelRef as Ref<T>,
updateFormCondition,
},
] as const
}
export type UseFormReturn = ReturnType<typeof useForm>
export default useForm

View File

@ -0,0 +1,89 @@
import { formProps } from 'naive-ui'
import { omit } from 'lodash-es'
import type { MaybeArray, AnyFC } from '@/types'
import type { RFormInst } from './types'
const props = {
...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
* RForm
* useForm register 使便使 hooks
*
* @default null
*/
onRegister: {
type: [Function, Array] as PropType<
MaybeArray<(formInst: RFormInst) => void>
>,
default: null,
},
} as const
export default props

View File

@ -4,14 +4,12 @@ import { NModal } from 'naive-ui'
import props from './props'
import { completeSize, uuid } from '@/utils'
import { setupInteract } from './utils'
import {
FULLSCREEN_CARD_TYPE_CLASS,
R_MODAL_CLASS,
CSS_VARS_KEYS,
} from './constant'
import type interact from 'interactjs'
import type { ModalProps } from 'naive-ui'
export default defineComponent({
@ -24,57 +22,11 @@ export default defineComponent({
[CSS_VARS_KEYS['dialogWidth']]: completeSize(props.dialogWidth ?? 446),
}))
const uuidEl = uuid()
let intractable: null | ReturnType<typeof interact>
// 记录拖拽的位置
const position = {
x: 0,
y: 0,
}
// 当前是否为预设 card 类型并且设置了 fullscreen
const isFullscreenCardType = computed(
() => 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 {
cssVars,
isFullscreenCardType,

View File

@ -1,16 +1,13 @@
import { useModal as useNaiveModal, NScrollbar } from 'naive-ui'
import { setupInteract } from '../utils'
import { queryElements, setStyle, completeSize, setClass } from '@/utils'
import { R_MODAL_CLASS, CSS_VARS_KEYS } from '../constant'
import type { RModalProps } from '../types'
interface UseModalCreateOptions extends Omit<RModalProps, 'memo'> {}
const useModal = () => {
const { create: naiveCreate, destroyAll: naiveDestroyAll } = useNaiveModal()
const create = (options: UseModalCreateOptions) => {
const create = (options: RModalProps) => {
const { content, ...rest } = options
let contentNode = content
@ -23,11 +20,11 @@ const useModal = () => {
color: 'rgba(0, 0, 0, 0)',
colorHover: 'rgba(0, 0, 0, 0)',
},
trigger: 'none',
trigger: 'hover',
style: {
width: 'auto',
height:
'calc(100vh - 29px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))',
maxHeight:
'calc(var(--html-height) - 29px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))',
},
},
{
@ -37,7 +34,7 @@ const useModal = () => {
)
}
const { preset, dad, fullscreen, width, cardWidth, dialogWidth } = options
const { preset, fullscreen, width, cardWidth, dialogWidth } = options
const modalReactive = naiveCreate({
...rest,
content: contentNode,
@ -57,28 +54,8 @@ const useModal = () => {
return
}
// 是否启用拖拽
if (dad) {
setupInteract(modalElement, {
preset,
x: 0,
y: 0,
})
}
// preset 为 cardfullscreen 为 true 时,最大化 modal
if (fullscreen && preset === 'card') {
const cardContentElement =
modalElement.querySelector<HTMLElement>('.n-card__content')
if (cardContentElement) {
setStyle(cardContentElement, {
maxHeight: `calc(100vh - 9px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))`,
overflowY: 'hidden',
padding: '0',
})
}
setStyle(modalElement, {
width: '100%',
height: '100vh',

View File

@ -4,6 +4,11 @@
// 当设置全屏时启用滚动
& .n-card__content {
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 = {
...modalProps,
/**
*
* @description
*
*
* @default true
*/
memo: {
type: Boolean,
default: true,
},
/**
*
* @description
@ -58,18 +47,6 @@ const props = {
type: [String, Number] as PropType<string | number>,
default: 446,
},
/**
*
* @description
*
* header
*
* @default false
*/
dad: {
type: Boolean,
default: false,
},
}
export default props

View File

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

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