Compare commits

...

69 Commits
v4.7.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
XiaoDaiGua-Ray
2f42571b3b version: 5.0.0 2024-10-20 01:15:12 +08:00
XiaoDaiGua-Ray
73792144a8 fix: fix demo bug 2024-10-16 09:54:02 +08:00
XiaoDaiGua-Ray
11cbf8bca3 version: v4.9.7 2024-10-16 09:51:56 +08:00
XiaoDaiGua-Ray
f9473114e7
Merge pull request #27 from XiaoDaiGua-Ray/ray-template-electron
version: v4.9.6
2024-09-27 16:01:05 +08:00
XiaoDaiGua-Ray
7394e0bf30 version: v4.9.6 2024-09-27 15:59:01 +08:00
XiaoDaiGua-Ray
d12fcd18b6 version: v4.9.5 2024-09-18 14:41:15 +08:00
XiaoDaiGua-Ray
60ed09a0c5 version: v4.9.4 2024-08-31 14:51:22 +08:00
XiaoDaiGua-Ray
fa8d52601f version: v4.9.3 2024-08-23 17:45:19 +08:00
XiaoDaiGua-Ray
3f7e3722fd version: v4.9.2 2024-07-27 12:40:41 +08:00
XiaoDaiGua-Ray
ab6593f022 version: v4.9.1 2024-07-27 12:16:29 +08:00
XiaoDaiGua-Ray
d306ac8804 version: v4.9.0 2024-07-09 16:29:04 +08:00
XiaoDaiGua-Ray
8405cc5709 version: v4.8.9 2024-06-28 16:38:13 +08:00
XiaoDaiGua-Ray
6975af2368 version: v4.8.8 2024-06-24 10:25:48 +08:00
XiaoDaiGua-Ray
b0cd545c99 version: v4.8.7 2024-06-11 10:35:01 +08:00
XiaoDaiGua-Ray
7ca9663cb1 version: v4.8.6 2024-05-27 17:45:28 +08:00
XiaoDaiGua-Ray
85f0d43d7e fix: 一些bug修复 2024-05-27 17:40:02 +08:00
XiaoDaiGua-Ray
09315473a3 fix: 修复workflow错误 2024-05-17 15:33:12 +08:00
XiaoDaiGua-Ray
52dc6038da fix: 修复useTheme.spec.ts单测失败问题 2024-05-17 15:30:08 +08:00
XiaoDaiGua-Ray
27db2293f3 version: v4.8.5 2024-05-17 15:26:25 +08:00
XiaoDaiGua-Ray
5b035815eb version: v4.8.4 2024-05-09 17:29:28 +08:00
XiaoDaiGua-Ray
b06006b442 update: 添加macOS环境 2024-05-08 16:37:20 +08:00
XiaoDaiGua-Ray
557ab0467a version: v4.8.3 2024-05-04 18:31:57 +08:00
XiaoDaiGua-Ray
e5c16b3497 update: update workflows 2024-05-02 10:29:33 +08:00
XiaoDaiGua-Ray
f97b25e626 update: update engines.node version 2024-05-02 10:24:53 +08:00
XiaoDaiGua-Ray
8097296f8f fix: 修复workflow指定node版本错误语法 2024-05-02 10:22:46 +08:00
XiaoDaiGua-Ray
43df660ab6 update: 更新workflow配置,手动指定node版本 2024-05-02 10:16:33 +08:00
XiaoDaiGua-Ray
4e14f6a02d update: update the pnpm-lock.yaml file 2024-04-30 00:49:45 +08:00
XiaoDaiGua-Ray
472518af99 fix: 修复docs-deploy.yaml文件中的错误 2024-04-30 00:12:27 +08:00
XiaoDaiGua-Ray
d9ea6cbdf5 feat: 新增prettierignore文件忽略配置 2024-04-30 00:06:46 +08:00
XiaoDaiGua-Ray
01287743e7 update: 更新cicd配置文件node版本 2024-04-29 23:40:41 +08:00
XiaoDaiGua-Ray
41dce4920f fix: 修复baseURL配置错误问题 2024-04-29 23:33:39 +08:00
XiaoDaiGua-Ray
1ee3ce0500 version: v4.8.2 2024-04-29 23:29:11 +08:00
XiaoDaiGua-Ray
ae67b7b8b9 feat: 新增jsbarcode cdn配置 2024-04-14 23:52:00 +08:00
XiaoDaiGua-Ray
87d0a7a4af v4.8.1 2024-04-14 23:47:27 +08:00
XiaoDaiGua-Ray
683367cfd8
Merge pull request #25 from zhengxiongwei520/main
fix: 修复RSegment组件Popover warning提示
2024-04-14 23:03:32 +08:00
bearer
a6f7843ff9 fix: 修改warning提示 2024-04-14 22:50:00 +08:00
XiaoDaiGua-Ray
e95c06b009 version: v4.8.0 2024-04-13 15:24:51 +08:00
XiaoDaiGua-Ray
85f7f28dc4 refactor: 移除登陆时全屏加载动画的触发 2024-04-07 17:14:05 +08:00
XiaoDaiGua-Ray
7c429338d4 version: v4.7.5 2024-04-07 17:07:50 +08:00
XiaoDaiGua-Ray
a0f7763778 version: v4.7.4 2024-03-29 16:25:09 +08:00
XiaoDaiGua-Ray
03628890cb version: v4.7.3 2024-03-24 15:31:06 +08:00
XiaoDaiGua-Ray
3e36144cf9 docs: 更新README.md和README.zh-CN.md文件 2024-03-23 11:36:39 +08:00
XiaoDaiGua-Ray
5c52f2f88c fix: 修复PrintDomOptions类型错误问题 2024-03-23 11:29:41 +08:00
XiaoDaiGua-Ray
bed8432cda version: v4.7.2 2024-03-23 11:25:28 +08:00
XiaoDaiGua-Ray
c21df42187 feat: 移除getDependenciesVersion自定义方法 2024-03-17 15:36:10 +08:00
XiaoDaiGua-Ray
3bf297d81c version: v4.7.1 2024-03-17 15:25:49 +08:00
514 changed files with 24845 additions and 12990 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/echart-themes/**/*.json
*.md
src/icons/*.svg

View File

@ -1,198 +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',
},
],
},
}

12
.gitattributes vendored
View File

@ -1,2 +1,14 @@
# 将换行符设置为lf
* text eol=lf
# 将静态资源文件以二进制形式处理
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.svg binary
*.webp binary
*.mp4 binary
*.mov binary
*.avi binary
*.mp3 binary
*.wav binary

View File

@ -1,4 +1,3 @@
name: ray-template documents deploy
on:
@ -15,15 +14,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 18.x
- name: Install Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Install dependencies
@ -36,4 +35,4 @@ jobs:
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: dist
folder: dist/production-dist
folder: dist/production

View File

@ -2,16 +2,15 @@ on:
- push
- pull_request
jobs:
cache-and-install:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version: [ 18.x ]
os: [ ubuntu-latest, windows-latest, macos-latest ]
experimental: [ true ]
node-version: [22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [true]
steps:
- name: Checkout
@ -25,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

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ dist/
*.local
visualizer.*
.eslintcache
.history
# Editor directories and files
.idea

2
.nvmrc
View File

@ -1 +1 @@
v18.18.2
v22.12.0

View File

@ -9,3 +9,4 @@ yarn.*
visualizer.*
visualizer.html
.env.*
*-lock.yaml

View File

@ -8,7 +8,6 @@ module.exports = {
jsxSingleQuote: false, // `jsx` 不使用单引号, 而使用双引号
trailingComma: 'all', // 尾随逗号
bracketSpacing: true, // 大括号内的首尾需要空格
jsxBracketSameLine: false, // `jsx` 标签的反尖括号需要换行
arrowParens: 'always', // 箭头函数, 只有一个参数的时候, 也需要括号
rangeStart: 0, // 每个文件格式化的范围是文件的全部内容
rangeEnd: Infinity,
@ -16,5 +15,6 @@ module.exports = {
insertPragma: false, // 不需要自动在文件开头插入 `@prettier`
proseWrap: 'preserve', // 使用默认的折行标准
htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 `html` 要不要折行
endOfLine: 'lf', // 换行符使用 `lf`
endOfLine: 'lf', // 换行符使用 `lf`,
singleAttributePerLine: false,
}

12
.vscode/settings.json vendored
View File

@ -1,4 +1,5 @@
{
"editor.formatOnSave": true,
"i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
@ -19,21 +20,32 @@
"alias-skip.allowedsuffix": ["ts", "tsx"],
"alias-skip.rootpath": "package.json",
"cSpell.words": [
"baomitu",
"bezier",
"Cascader",
"Clickoutside",
"codabar",
"commitmsg",
"crossorigin",
"datetimerange",
"domtoimage",
"EDITMSG",
"iife",
"internalkey",
"jsbarcode",
"linebreak",
"logicflow",
"macarons",
"menutag",
"ndata",
"persistedstate",
"pharmacode",
"Popselect",
"precommit",
"siderbar",
"snapline",
"stylelint",
"unocss",
"WUJIE",
"zlevel"
]

View File

@ -1,4 +1,932 @@
# 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
## Feats
- `RTable` 组件现在默认 `attrs` 透传不会再透传至 `Table` 而是透传至容器 `Card`
- `AppAvatar` 组件获取用户信息由 `localStorage` 缓存获取变更为 `getSigningCallback` 响应式获取
- 移除 `SUPER_ADMIN` 默认值
- 移除 `APP_KEEP_ALIVE` 配置项,迁移至 `settingStore`
- 移除 `SIDE_BAR_LOGO_DEFAULT` 配置项,迁移至 `settingStore`
- 移除 `APP_MENU_CONFIG` 配置项,迁移至 `settingStore`
- 移除所有的 `ray-template` 相关注释信息
- `SettingDrawer` 新增更多配置项,允许通过配置入口配置更多的配置项
- 新增 `DatePicker`, `InputNumber` 样式,默认设置 `width``100%`
- 新增 `updateObjectValue` 方法,并且全局替换重复类似方法
- `useTheme` 新增 `syncSystemTheme` 方法,允许同步系统主题
- 新增 `ThemeSegment` 主题切换组件
- 移除 `watchOnce` 方法引入,使用原生 `watch` 方法替代
- 取消 `SettingDrawer` 自动聚焦可选中元素
## 4.9.7
从该版本开始,默认关闭了 `cdn` 构建生产环境;因为国内厂商真心不给力。
## Feats
- 更新 `vue` 版本至 `3.5.12`
- 更新 `@vueuse/core` 版本至 `11.1.0`
- 更新 `naive-ui` 版本至 `2.40.1`
- 更新 `MenuTag` 一些东西,移除了无用的 `ref` 注册,简化了代码;现在允许出现 `rootPath` 标签的关闭按钮了
- `useElementFullscreen` 新增 `currentWindowSize` 返回项,获取当前的窗口尺寸
- `RCollapseGrid` 组件默认开启 `bordered`
- `RCollapseGrid onUpdateValue` 方法更名为 `onUpdateOpen`
- `RTable` 组件默认开启 `wrapperBordered`
- `RTable` 组件默认开启 `bordered`
- 新增 `clearSigningCallback` 方法
- `vite.custom.config` 新增 `cdn` 配置项,是否启用 `cdn` 构建项目
- 配置 `cdn``false`,因为国内厂商更新资源速度有点慢,导致预览失败
- `Layout` 层注入 `--window-width`, `--window-height`, `css var` 属性
- 稳定 `Layout` 层的 `css var` 属性
## Fixes
- 修复 `MenuTag` 鼠标移入并且加右键菜单操作时,会导致关闭按钮图标消失的问题
- 修复 `useElementFullscreen` 方法在窗口尺寸更新时,全屏元素失败的问题
- 修复 `MenuTag` 注入 `MENU_TAG_DATA` 错误的问题
- 修复 `Layout Content` 默认高度获取失败问题
- 修复 `RCollapseGrid` 组件自定义 `collapse` 插槽折叠失败的问题
## 4.9.6
由于 `cdn` 厂商更新 `cdn` 资源太慢的缘故,所以目前 `vue` 的版本只有 `3.5.3` 会导致构建线上生产环境报错的问题。
## Feats
- 更新 `vue` 版本至 `3.5.8`
- `useDomToImage` 相关
- 使用 `html-to-image` 替换 `dom-to-image` 底层依赖库
- 同步补全 `html-to-image` 所有新特性至该方法
- 同步修改 `printDom` 方法
- 类型提示现在会更加友好,`create` 方法会根据 `imageType` 自动推导转换结果的类型
- 移除 `DomToImageResult` 类型
## Fixes
- 修复 `RCollapseGrid` 组件折叠失败的问题
## 4.9.5
天元突破,红莲螺岩。
兼容 `vue3.5` 版本的更新。
## Feats
- 更新脚手架依赖为主流依赖
- 更新 `vue` 版本至 `3.5.6`
- 更新 `vite` 版本至 `5.4.3`
- 更新 `pinia-plugin-persistedstate` 版本至 `4.0.1`,并且兼容破坏性更新改动
- `RChart` 组件相关
- 小重构该组件,移除多个 `echart` 缓存,现在有且仅有一个
- 减少 `watch` 监听项
- 使用 `useTemplateRef` 方法替代 `ref` 注册 `dom`
- 现在预设 `card` 时,`chart` 图会更加的醒目一些
- 优化 `demo` 展示
- 现在会拦截 `aria` 属性,现在仅允许通过 `showAria` 配置项管理无障碍模式
- 优化无障碍模式渲染,现在不会重新渲染整个图表,而是通过 `setOptions` 方式更新图表
- `useChart` 方法相关
- `isDispose` 方法更名为 `isDisposed`
## Fixes
- 修复 `useChart` 方法相关方法中 `dispose` 方法执行不生效的问题
- 修复 `RChart``loading` 不能跟随主题变化的问题
## 4.9.4
## Feats
- `cdn` 相关的一些配置
- 将 `cdn` 源更新为 `baomitu`,为了避免因为墙的影响导致加载缓慢的问题
- 新增一些插件的 `cdn` 配置
- `useTable`, `useTablePro` 中的 `print` 方法新增传递 `options` 配置项,配置打印,允许配置 `dom-to-image``print-js`,详情可以查看 `PrintDomOptions` 类型说明。
- `RCollapse` 组件新增 `collapse` 插槽,允许自定义【展开】与【收起】状态
## Fixes
- 修复 `useTablePro``print` 方法丢失的问题
## 4.9.3
## Feats
- 更新 `vue` 版本至 `3.4.38`
- 更新 `vite` 版本至 `5.4.1`
- 调整 `RCollapseGrid` 支持 `actionAlign` 配置型,配置按钮垂直方向,默认为 `end`
- `MenuTag` 组件
- 调整 `MenuTag` 滚动条样式,现在将它隐藏了
- 优化关闭按钮样式
- `RTable` 新增 `renderWrapperHeader` 配置项,配置外层容器 `header` 是否渲染
- `postcss` 配置 `not dead`,忽略兼容已经无需兼容的浏览器
- `RChart` 组件 `setOptions` 方法配置项默认不启用 `merge` 模式
- 调整 `header` 的样式,增加了一点点间隙
- `useDevice` 新增 `observer` 配置项,可以自定义观察回调
- 新增 `components` 包,助力简化业务开发
- 新增 `RTablePro` 组件,大幅简化中后台带有过滤请求条件的表格显示业务
- 新增 `RCollapse` 组件,允许折叠过滤条件
## Fixes
- 移除 `postcss-px-to-viewport-8-plugin` 插件,使用 `postcss-px-to-viewport-8-with-include` 替换,修复 `include` 失效问题
- 修复 `useElementFullscreen` 在退出时,没有正确的回滚 `zIndex` 的问题
- 修复 `RChart` 配置 `setChartOptions` 不生效的问题
## 4.9.2
## Feats
- `useDevice` 支持 `observer` 配置项
- `postcss` 自动尺寸转换插件配置更新
## Fixes
- 修复菜单在小尺寸屏幕并且处于折叠状态,导致显示不完整的问题
## 4.9.1
更新核心依赖版本为主流版本。
## Feats
- 更新 `axios` 版本至 `1.7.2`
- 更新 `vue-hooks-plus` 版本至 `2.2.1`
- 更新 `naive-ui` 版本至 `2.39.0`
- 更新 `vue` 版本至 `3.4.34`
- 更新 `vite` 版本至 `5.3.5`
- 更新 `@vitejs/plugin-vue` 版本至 `5.1.0`
- 更新 `@vitejs/plugin-vue-jsx` 版本至 `4.0.0`
- 调整解锁锁屏头像样式
- `RModal` 在设置为拖拽时,如果未设置头(也就是 `title`)属性,则会失效
## Fixes
- 修复 `__APP_CFG__` 类型丢失问题
- 修复 `RTable` 设置 `tool``false` 时,单独设置 `striped`, `bordered` 时不生效的问题
## 4.9.0
主要修复了一个歧义问题,就是新开页面输入 `url`,或者是 `window.open` 打开当前系统的页面时,会导致初始化异常的问题。这是因为以前的 `appMenu`, `appSigning` 相关的缓存都是防止与 `sessionStorage`,但是该缓存并不能共享,所以导致了这个问题。在该版本中将该缓存调整为了 `localStorage`
## Feats
- 移除 `vite-plugin-imp` 插件
- 更新 `vue` 版本至 `3.4.31`
- 更新 `vite` 版本至 `5.3.3`
- 将 `appPiniaMenuStore`, `appPiniaSigningStore` 缓存由 `sessionStorage` 更改为 `localStorage` 缓存
- 现在在模拟退出的时候,会清理 `appPiniaMenuStore`, `appPiniaSigningStore``localStorage` 缓存
- 将 `route` 待提取字段单独抽离,统一维护为 `pickRouteRecordNormalizedConstant`
## Fixes
- 修复 `RTable` 自定义 `tool` 时抛出的优化警告问题
- 修复在复制 `url` 打开新标签页输入网址后(包括新开页面),会导致项目初始化异常的问题
## 4.8.9
## Fixes
- 修复 `useCheckedRowKeys``getRows` 方法在异步分页获取数据的时候,会导致切换页面后勾选项丢失问题
## 4.8.8
## Feats
- 更新 `vite` 版本至 `5.3.1`
- 更新 `vue` 版本至 `3.4.30`
- `FetchErrorFunction` 类型在你自定义请求拦截器插件处理错误请求的时候,可能会用上
## Fixes
- 修复 `useCheckedRowKeys``getRows` 方法类型丢失问题
## 4.8.7
## Feats
- 更新 `vite` 版本至 `5.2.12`
- 更新 `vue` 版本至 `3.4.27`
- 更新 `vue-hooks-plus` 版本至 `2.2.0`
- 更新 `@types/lodash-es` 版本至 `4.17.12`
- `FetchFunction` 移除,使用 `AxiosResponseInterceptor`, `AxiosRequestInterceptor` 替代插件类型推导
- `echarts macarons theme` 优化
- `RChart` 组件的默认 `loading` 效果支持自动跟随主题切换
- `SIDE_BAR_LOGO` 更名为 `SIDE_BAR_LOGO_DEFAULT`,现在支持在 `setting store` 中动态更新,以满足更广泛的需求
## Fixes
- 修复 `primaryColorOverride` 配置项不生效问题
- 修复面包屑在 `sameLevel` 的情况下,可能会被覆盖问题
## 4.8.6
## Fixes
- 修复动态路由的情况下,始终会被跳转到 `404` 页面的问题
- 修复 `RIcon` 组件 `color` 配置项无效的问题
- 修复 `svg-icon` 页面可能会在更新 `icon` 后出现报错的问题
- 修复 `updateDocumentTitle` 方法可能会更新标题出错的问题
- 修复菜单的折叠问题
## 4.8.5
## Feats
- 更新 `vue-router` 版本至 `4.3.2`
- 更新 `vue-hooks-plus` 版本至 `2.0.3`
- `usePagination` 方法
- 新增 `resetPagination` 方法,用于重置分页器
- `RBarcode` 组件,默认启用 `watchText` 配置项,用于监听 `text` 内容变化
- `RMoreDropdown` 组件,新增 `icon` 自定义配置图标,依赖 `RIcon` 组件实现
- `buildOptions` 方法现在会自动根据 `mode` 进行自动构建生成包了
- `RTable hooks`
- `UseCheckedRowKeysOptions`
- 新增 `table column` 参数,自动获取当前表格列配置是否启用了选项模式(单选、多选)
- `selectKey` 方法现在在单选模式下,只会选择一条数据,最后一次选择的会覆盖上次的
- `useTable`
- 新增 `print` 方法
- `useTheme` 方法
- `changeDarkTheme` 更名为 `darkTheme`
- `changeLightTheme` 更名为 `lightTheme`
- `getAppTheme` 方法返回值做了准确的 `ts` 签名,并且新增 `themeLabelI18n` 字段
## Fixes
- 修复 `vitest` 设置 `threads` 导致报错 `Module did not self-register...` 的问题
- 修复 `vue-router warning` 抛出 `can no longer be used directly inside...` 的警告问题
- 修复 `pre-commit` 错误的 `vue-tsc` 检查报错
- `signing logout` 方法现在会正确的清理 `menuTagOptions`
## 4.8.4
由于 `node canvas` 本身的特性(环境问题很多),故在 `v4.8.4` 版本予以移除 `RQRCode` 组件,使用 `vue3-next-qrcode` 替代。所有的使用方法保持一致。
## Feats
- 新增 `package.json``lint` 命令,用于自动修复 `eslint, prettier` 问题
```sh
pnpm lint
```
- `useModal` 方法在 `preset = card` 并且设置了 `fullscreen` 时,现在会自动启用滚动条
- `pre-commit` 新增 `vue-tsc` 检查
## 4.8.3
## Feats
- 更新 `vue-i18n` 版本至 `9.13.1`
- 更新 `naive-ui` 版本至 `2.38.2`
- 更新 `vite` 版本至 `5.2.11`
- 更新 `vue` 版本至 `3.4.26`
## Fixes
- 同步修复 `useModal` 组件在 `preset = card` 时,不能正确显示内容的问题
## 4.8.2
破坏性更新!请仔细阅读更新日志!
更新默认 `node` 版本为 `v20.12.0`
并且对于历史一些模块的分包进行了优化,现在更加语意化与清晰。
重构 `axios` 拦截器,使用插件式设计思想,更加强大的拓展性、可维护性!
## Feats
- 更新默认 `node` 版本为 `v20.12.0`
- `RBarcode` 相关
- 新增 `onSuccess`, `onError`, `onFinally` 三个渲染回调
- 新增 `watchText` 配置项,当 `text` 内容变化后,主动更新条形码,默认不启用
- 更新 `vue-tsc` 版本至 `2.0.11`
- `hooks`
- `template`
- 新增 `useContentScroll` 方法,用于滚动 `LayoutContent` 内容区域
- 新增 `useSiderScroll` 方法,用于滚动 `LayoutSider` 侧边栏区域,也就是菜单区域
- `web`
- `usePagination` 类型修复,所有 `get` 方法都会准确的返回对应的类型
- `SiderBarLogo` 组件
- 支持未配置 `icon` 时,允许截取 `title` 第一个字符作为占位
- `router`
- `router types` 包,补充类型,获得更好的类型提示与配置提示
- `combine-raw-route-modules` 方法现在会检查路由配置的 `name`, `path` 属性是否出现重复,如果重复,会直接抛出异常中断流程
- 移除 `name` 配置项作为必选项
- 标记 `components` 配置项为 `deprecated` 描述项
- 增强 `AppRouteRecordRaw` 类型,现在当你显式配置了 `meta.keepAlive``true` 的时候,会提示你 `name` 必填
- `app-config`
- 提供 `MessageProver` 配置
- 限制最大消息数量为 5 条
- 默认不启用手动关闭消息功能
- `RTable` 新增 `cardProps` 配置项,配置外层容器。
- 更新 `vue` 版本至 `3.4.25`
- 更新 `vite` 版本至 `5.2.10`
- 更新 `vite-bundle-analyzer` 版本至 `0.9.4`,新增汇总模式
- 移除 `naive-ui` 自动导入 `hooks`
- `Search` 组件
- 优化分包逻辑
- 现在允许在搜索菜单初始位置进行上下键切换
- 优化搜索菜单样式
- 新增 `positionSelectedMenuItem` 方法,用于定位选中菜单项
- `hooks`
- `template`
- 新增 `useContentScroll` 方法,用于滚动 `LayoutContent` 内容区域
- 新增 `useSiderScroll` 方法,用于滚动 `LayoutSider` 侧边栏区域,也就是菜单区域
- 重写 `AppAvatar` 组件
- `axios`
- `inject` 包重命名为 `axios-interceptor`,并且拆分分包逻辑,现在以插件形式管理拦截器逻辑与注入逻辑
- 新增 `padding-line-between-statements` 规范,强制语句之间的空行
## Fixes
- 修复 `SIDE_BAR_LOGO` 未配置 `icon` 时,侧边栏不显示的问题
- 修复 `RTable` 在配置 `style.height` 时,样式会错乱的问题
- 修复 `MenuTag` 在初始化位置时不能准确滚动到当前标签页的问题
## 4.8.1
## Feats
- 新增 `RBarcode` 条形码组件,基于 `JsBarcode` 二次封装
## Fixes
- 修复 `RSegment` 分段器设置 `popover` 后警告的问题
## 4.8.0
全局破坏性更新。移除了很多包、方法,请谨慎更新。
## Feats
- 移除无意义依赖包
- `RTable` 组件
- 强制约束使用 `useTable` 方法操作实例,移除 `expose` 暴露
- 新增 `useCheckedRowKeys` 方法,用于操作表格选中行,该方法仅适用于选中行操作(多选、单选)
- `RForm`, `RChart` 强制约束使用对应 `useForm`, `useChart` 方法操作实例,移除 `expose` 暴露
- 优化 `usePagination` 方法修改 `paginationRef` 值类型,使用 `Ref` 签名类型
- `eslint` 规则新增禁用被标记弃用方法
- 移除 `omit`, `pick` 方法,使用 `lodash-es` 包替代
- 新增 `RSegment` 分段器组件
> 由于是基于 [NTabs](https://www.naiveui.com/zh-CN/dark/components/tabs) 组件二开所以也继承了该组件的一些特性bug
- `svg icon` 支持分包管理
- `vite-helper` 包中所有方法进行拆分,约定按照功能模块进行拆分。如果该方法属于 `vite` 插件,按照 `xx-xx-plugin` 方式命名。
- 统一所有模块的文件命名
- `class`, `hooks` 方法统一为小驼峰命名
> 示例: `MyClass`, `useMyHooks`
- `component` 的文件夹与组件名称统一为大驼峰命名
> 示例: `MyComponent`, `ChildComponent`
- `utils`, `custom function` 统一为蛇形命名
> 示例: `custom-function`, `custom-utils`
- 新增 `v-ripple` 水波纹指令
- 新增 `v-lazy-show` 惰性 `v-show` 指令
- `GlobalSearchButton` 组件样式优化
- `useElementFullscreen` 方法
- 移除缓存 `transition` 样式还原方式,使用 `options.transition` 方式配置,默认为 `all 0.3s var(--r-bezier)`
- 优化 `LayoutContent` 网页最大化动画效果
- `global-variable` 新增 `getVariable` 方法,允许解构获取全局响应式变量
- 移除 `SiderBar` 组件的 `tip` 提示
- 补充了一些注释
## Fixes
- 修复 `RTable C` 组件对于 `columns type` 项无法有效兼容问题
## 4.7.5
## Feats
- 更新 `vite` 版本至 `5.2.8`
- `appConfig` 配置项
- 新增 `LAYOUT_CONTENT_SPIN_WHEN_ROUTE_CHANGE` 配置项用于配置路由切换时是否显示内容区域LayoutContent加载中状态
- 新增 `APP_GLOBAL_LOADING` 配置项,用于配置全局加载状态内容
- `setVariable`, `updateSettingState` 方法,使用 `Object.hasOwn` 方法替代 `Reflect.has` 方法
- `spin` 包整合至 `app-components`
- `RayLink` 更名为 `AppShareLink`
- `echart-themes` 包整合至 `app-config`
- 优化 `GlobalSearch` 底部样式
- `error`
- 整合至 `views`
- `PageResult` 组件新增 `goBack` 功能
- `usePagination` 方法
- 重写该方法返回值
- 调整 `MenuTag` 标签页尺寸为 `small`
- `RChart` 组件
- 新增 `onFinally` 配置项,用于配置图表渲染完成后的回调
- 新增 `watchOptionsThrottleWait` 配置项,可以配置 `watchOptions` 触发频率,当你需要频繁更新图表数据时,可以配置该项,避免频繁渲染图表
- 移除 `throttleWait` 配置项
- `__test__`
- 新增 `qr-code.spec.ts` 单元测试模块
- 新增 `modal.spec.ts` 单元测试模块
- 针对 `eslint` 部分规则进行调整
- 补充代码注释
```ts
import { usePagination } from '@/hooks'
const [
paginationRef,
{ updatePage, updatePageSize, getItemCount, setItemCount },
] = usePagination(
() => {
// do something...
},
{
// ...options
},
)
```
## Fixes
- 修复 `useDayjs.spec` 单测模块测试失败问题
## 4.7.4
对于 `RTable`, `RForm`, `RChart` 组件都新增了对应的 `useTable`, `useForm`, `useChart` 方法;让你在业务开发中抛弃注册 `ref` 实例方式调用组件方法。
补充拓展了 `useModal` 方法,支持 `dad`, `fullscreen` 等拓展配置项。
```ts
import { useTable, useForm } from '@/components'
const [registerTable, { getTableInstance }] = useTable()
const [registerForm, { getFormInstance }] = useForm()
// 做点什么...
```
## Feats
- 更新 `vite` 版本至 `5.2.6`
- `useDevice` 方法支持自定义 `media` 配置项,用于配置自定义媒体查询尺寸
- `RTable` 组件
- 新增 `tool` 配置项,配置是否显示工具栏
- 优化工具栏渲染逻辑
- 新增 `useTable` 方法,用于便捷调用表格方法
> 该方法比起常见的 `ref` 注册,然后 `tableRef.value.xxx` 的方法获取表格方法更为简洁一点。但是也值得注意的是,需要手动调用一次 `register` 方法,否则会报错;还有值得注意的是,需要注意表格方法的调用时机,需要等待表格注册完成后才能正常调用。如果需要在 `Parent Create` 阶段调用,可以尝试 `nextTick` 包裹一层。
```tsx
import { RTable } from '@/components'
import { useTable } from '@/components'
defineComponent({
setup() {
const [
register,
{ getTableInstance, clearFilters, clearSorter, scrollTo, filters, sort },
] = useTable()
const columns = [
{
title: 'No',
key: 'no',
},
{
title: 'Title',
key: 'title',
},
]
const data = [
{
no: 1,
title: 'title',
},
]
return {
register,
getTableInstance,
clearFilters,
clearSorter,
scrollTo,
filters,
sort,
columns,
data,
}
},
render() {
const { columns, data } = this
const { register } = this
return (
<RTable columns={columns} data={data} register={register.bind(this)} />
)
},
})
```
- `RForm` 组件
- 新增组件,所有行为、方法与 `NForm` 保持一致
- `useForm` 方法,使用方法与 `useTable` 几乎一致
- `canUseDom`, `isBrowser` 方法统一为函数导出
- `RModal` 组件新增 `useModal` 方法
- 新增 `useModal` 方法,允许拓展配置 `dad`, `fullscreen` 等配置项。但是由于 `useModal` 生成组件的特殊性,不支持 `memo` 属性配置,其余配置项维持一致
> 该方法在当前版本存在一个 bug`preset = card` 时,不能正确的显示 content查看相应的 [issues](https://github.com/tusen-ai/naive-ui/issues/5746)。
- 重写部分代码,优化组件逻辑,补全 `ts` 类型
- `RChart`
- 新增 `useChart` 方法,使用方法与 `useTable` 几乎一致
- 新增 `usePagination` 方法与 `usePagination.spec` 单元测试模块
```ts
import { usePagination } from '@/hooks'
const {
updatePage,
updatePageSize,
getItemCount,
setItemCount,
getPage,
setPage,
getPageSize,
setPageSize,
getPagination,
getCallback,
} = usePagination(
() => {
// do something...
},
{
// ...options
},
)
```
## 4.7.3
补全 `hooks` 包下的单测模块。
## Feats
- 重命名 `store` 包下的 `type.ts``types.ts`
- `useElementFullscreen`
- 优化全屏尺寸逻辑,现在会监听浏览器 `width`, `height` 变化,自动调整全屏尺寸
## Fixes
- 修复 `useElementFullscreen` 方法在退出全屏时,不能恢复原有 `transition` 样式问题
## 4.7.2
新增 `vitest` 测试框架。
重写了一些 `utils`, `hooks` 包的方法,并且编写了对应的单测模块。
## Feats
- 集成 `vitest` 测试框架,并且对于 `utils`, `hooks` 包方法编写了对应的单测模块
> 使用方法请查看 [vitest](https://cn.vitest.dev/)。
```sh
# 新增测试单元模块
1. 在 `__test__` 目录下创建测试文件
2. 添加对应的单测模块
3. 编写对应的单测逻辑
# 值得注意的是
1. 测试文件必须在 `__test__` 目录下
2. 测试文件必须以 `xxx.spec.ts` 或者 `xxx.spec.tsx` 结尾,否则不生效
3. 必须手动补全导入待测试方法或者组件,可以查看现有的测试文件
# 运行测试
pnpm test
# 运行测试 ui 界面
pnpm test:ui
# 最重要需要值得注意的地方
一旦被导入方法或者组件文件中,有报错,那么会导致整个文件的测试方法在执行 `pnpm test`, `pnpm test:ui` 时都报错。
但是单独测试该文件时,不会报错,只有在执行 `pnpm test`, `pnpm test:ui` 时才会报错。
# 最后
未来会逐步完善测试用例,以及编写更多的测试单元模块,包括全局组件。
```
- `basic` 包相关
- 重构 `equalRouterPath` 方法,现在允许忽略带参数的路径比较
- `omit`, `pick` 方法不在对 `null`, `undefined` 传参抛出警告;该方法现在支持多参数传递
- `hooks` 包相关
- `useDayjs`
- 优化注释
- `getStartAndEndOfDay` 方法新增 `formatEndOfDay`
- `element` 包相关
- `colorToRgba`
- 现在支持解析 `#fff`, `#ffffff`, `#ffffffaa` 格式的颜色
- 重写该方法
- `precision` 包相关
- `Options` 类型重构为 `CurrencyOptions`
- `format` 方法新增 `currency` 配置项,移除第二个参数,合并在配置项中配置输出格式
- `distribute` 方法新增配置项CurrencyOptions
- 现在 `@/hooks` 包下方法都将构建在一个包中输出,不在做拆分
## Fixes
- `utils` 包相关
- 修复 `arrayBufferToBase64Image` 方法总是返回 `null` 的问题
- 修复 `queryElements` 方法 `defaultElement` 配置项不能正确的返回默认值问题
- 修复 `autoPrefixStyle` 方法不能返回样式本身问题
## 4.7.1
## Feats
- 更新 `vite` 版本至 `5.1.6`
- `vite-plugin-cdn2` 相关
- 更新 `vite-plugin-cdn2` 版本至 `1.1.0`
- 更新 `vite.plugin.config.ts` 关于 `cdn2` 配置
- 新增 `cdnResolve` 方法,自定义 `resolve`
- 更新主流浏览器版本号升级
- `RChart` 组件相关
- 新增 `intersectionObserver` 配置项,用于配置是否使用 `IntersectionObserver` 监听图表渲染
> 但是该配置项不支持动态修改,只能在初始化时配置
- 优化组件的注释,并且补充了一些注释
- 新增 `intersectionObserver` 配置项,手动指定 `IntersectionObserver` 需要监听的元素
- `observer` 更名为 `autoResizeObserverTarget`
- 补充 `chart` 示例页面
- 优化亮色主题下锁屏样式
## Fixes
- 修复 `vite-plugin-cdn2` 插件构建 `echarts` 失败问题,具体查看该 [issue](https://github.com/nonzzz/vite-plugin-cdn/issues/42)
- `RChart` 组件相关
- 修复卸载组件不能完全清理 `inst` 问题
- 修复组件不能正常触发初始化动画问题
- 修正 `isDispose` 方法返回值含义,现在返回 `true` 代表已经卸载,`false` 代表未卸载
## 4.7.0
@ -248,7 +1176,7 @@ remove('your key', 'all')
- 新增 `extra` 配置项,用于配置标记
```ts
import { t } from '@/hooks'
import { t } from '@/hooks/web/useI18n'
import { LAYOUT } from '@/router/constant'
import type { AppRouteRecordRaw } from '@/router/types'

View File

@ -48,20 +48,19 @@ A `completely free`, `efficient`, `feature complete` and based on vite5. x & ts(
- `Multi-terminal adaptation:` support pc, phone, pad
- `Documentation:` complete documentation
- `Mock data:` built-in Mock data solution
- `Axios request:` secondary encapsulation of axios library, support: cancel, jitter, automatic repeat cancellation and other functions
- `Axios request:` the plug-in design is used to encapsulate the axios library interceptor twice, which makes the interceptor more flexible
- `SVG:` built-in svg icon solution
- `Hooks:` based on the template characteristics of the encapsulated hooks to make it easier to use some functions of the template
- `TypeScript:` provide a complete type
- `Vitest:` built-in vitest test solution
## 👀 Preview
- [Preview](https://xiaodaigua-ray.github.io/ray-template/#/)
- [Preview(Acceleration address)](https://ray-template.yunkuangao.com/#/)
## 📌 Documentation
- [Documentation](https://xiaodaigua-ray.github.io/ray-template-doc/)
- [Documentation(Acceleration address)](https://ray-template.yunkuangao.com/ray-template-doc/)
## 🔋 Change Log
@ -80,6 +79,7 @@ A `completely free`, `efficient`, `feature complete` and based on vite5. x & ts(
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs basic syntax
- [Pinia](https://pinia.vuejs.org/zh/introduction.html) - state manager pinia usage
- [TSX](https://github.com/vuejs/babel-plugin-jsx/blob/main/packages/babel-plugin-jsx/README-zh_CN.md) - tsx basic syntax
- [Vitest](https://cn.vitest.dev/guide/) - vitest basic use
## 📦 Setup
@ -88,9 +88,6 @@ A `completely free`, `efficient`, `feature complete` and based on vite5. x & ts(
```sh
# github
git clone https://github.com/XiaoDaiGua-Ray/ray-template.git
# If your download speed is very slow, you can switch to the proxy address below
git clone https://mirror.ghproxy.com/https://github.com/XiaoDaiGua-Ray/ray-template.git
```
### Pull dependencies
@ -99,6 +96,13 @@ git clone https://mirror.ghproxy.com/https://github.com/XiaoDaiGua-Ray/ray-templ
pnpm i
```
### Test project
```sh
pnpm test
```
### Startup project
```sh

View File

@ -48,20 +48,19 @@
- `多端适配:`支持 pc, phone, pad
- `文档:`完善的文档
- `Mock 数据:`内置 Mock 数据方案
- `Axios 请求:`二次封装 axios 库,支持:取消、防抖、自动重复取消等功能
- `Axios 请求:`采用插件式设计二次封装 axios 库拦截器,让拦截器更加灵活
- `SVG`内置 svg icon 解决方案
- `Hooks`基于模板特性封装的 hooks 让你更加方便的使用模板一些功能
- `TypeScript`提供完整的类型
- `Vitest`内置 vitest 测试方案
## 👀 预览地址
- [点击预览](https://xiaodaigua-ray.github.io/ray-template/#/)
- [点击预览(加速地址)](https://ray-template.yunkuangao.com/#/)
## 📌 文档地址
- [文档](https://xiaodaigua-ray.github.io/ray-template-doc/)
- [文档(加速地址)](https://ray-template.yunkuangao.com/ray-template-doc/)
## 🔋 更新日志
@ -80,6 +79,7 @@
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
- [Pinia](https://pinia.vuejs.org/zh/introduction.html) - 状态管理器 pinia 使用
- [TSX](https://github.com/vuejs/babel-plugin-jsx/blob/main/packages/babel-plugin-jsx/README-zh_CN.md) - tsx 基本语法
- [Vitest](https://cn.vitest.dev/guide/) - vitest 基本使用
## 📦 起步
@ -88,9 +88,6 @@
```sh
# github
git clone https://github.com/XiaoDaiGua-Ray/ray-template.git
# 如果你的下载速度很慢,可以切换到下面的代理地址
git clone https://mirror.ghproxy.com/https://github.com/XiaoDaiGua-Ray/ray-template.git
```
### 拉取依赖
@ -99,6 +96,13 @@ git clone https://mirror.ghproxy.com/https://github.com/XiaoDaiGua-Ray/ray-templ
pnpm i
```
### 测试项目
```sh
pnpm test
```
### 启动项目
```sh

View File

@ -0,0 +1,16 @@
import { prefixCacheKey } from '../../src/utils/app/prefix-cache-key'
describe('prefixCacheKey', () => {
it('should return the key with the default prefix', () => {
const key = 'signing'
expect(prefixCacheKey(key)).toBe(key)
})
it('should return the key with the custom prefix', () => {
const key = 'signing'
const customPrefix = 'ray-'
expect(prefixCacheKey(key, { customPrefix })).toBe(customPrefix + key)
})
})

View File

@ -0,0 +1,18 @@
import { arrayBufferToBase64Image } from '../../src/utils/basic'
describe('arrayBufferToBase64Image', () => {
const arrayBuffer = new ArrayBuffer(8)
const base64ImagePrefix = 'data:image/png;base64,'
it('should convert array buffer to base64 image', () => {
const base64Image = arrayBufferToBase64Image(arrayBuffer)
expect(base64Image).toBe(`${base64ImagePrefix}AAAAAAAAAAA=`)
})
it('should convert array buffer to base64 image with prefix', () => {
const base64Image = arrayBufferToBase64Image(arrayBuffer)
expect(base64Image.startsWith(base64ImagePrefix)).toBe(true)
})
})

View File

@ -0,0 +1,27 @@
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)
})
it('should call the callback function when the function throws an error', () => {
let callbackFnExecuted = 1
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}
callWithAsyncErrorHandling(fn, callbackFn)
expect(callbackFnExecuted).toBe(2)
})
})

View File

@ -0,0 +1,27 @@
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)
})
it('should call the callback function when the function throws an error', () => {
let callbackFnExecuted = 1
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}
callWithErrorHandling(fn, callbackFn)
expect(callbackFnExecuted).toBe(2)
})
})

View File

@ -0,0 +1,7 @@
import { detectOperatingSystem } from '../../src/utils/basic'
describe('detectOperatingSystem', () => {
it('should return Unknown', () => {
expect(detectOperatingSystem()).toBe('Unknown')
})
})

View File

@ -0,0 +1,33 @@
import { downloadAnyFile } from '../../src/utils/basic'
describe('downloadAnyFile', () => {
it('should download data when data is a string', () => {
const data = 'test data'
const fileName = 'test.txt'
expect(downloadAnyFile(data, fileName)).resolves.toBeUndefined()
})
// it('should download data when data is a ArrayBuffer', () => {
// const data = new ArrayBuffer(8)
// const fileName = 'test.txt'
// expect(downloadAnyFile(data, fileName)).resolves.toBeUndefined()
// })
// it('should download data when data is a Blob', () => {
// const data = new Blob(['hello', 'world'], {
// type: 'text/plain',
// })
// const fileName = 'test.txt'
// expect(downloadAnyFile(data, fileName)).resolves.toBeUndefined()
// })
// it('should download data when data is a File', () => {
// const data = new File(['hello', 'world'], 'test.txt')
// const fileName = 'test.txt'
// expect(downloadAnyFile(data, fileName)).resolves.toBeUndefined()
// })
})

View File

@ -0,0 +1,12 @@
import { downloadBase64File } from '../../src/utils/basic'
describe('downloadBase64File', () => {
const base64 =
'data:image/png;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAE'
it('download base64 to file', () => {
const result = downloadBase64File(base64, 'test.png')
expect(result).toBe(void 0)
})
})

View File

@ -0,0 +1,17 @@
import { equalRouterPath } from '../../src/utils/basic'
describe('equalRouterPath', () => {
it('compare paths with parameters', () => {
const p1 = '/a?b=1'
const p2 = '/a?b=2'
expect(equalRouterPath(p1, p2)).toBe(true)
})
it('compare paths', () => {
const p1 = '/a'
const p2 = '/a/'
expect(equalRouterPath(p1, p2)).toBe(true)
})
})

View File

@ -0,0 +1,21 @@
import { getAppEnvironment } from '../../src/utils/basic'
describe('getAppEnvironment', () => {
it('should return MODE is test', () => {
const { MODE } = getAppEnvironment()
expect(MODE).toBe('test')
})
it('SSR should be false', () => {
const { SSR } = getAppEnvironment()
expect(SSR).toBe(false)
})
it('deconstruction value should be undefined', () => {
const { UNDEFINED_MODE } = getAppEnvironment()
expect(UNDEFINED_MODE).toBe(void 0)
})
})

View File

@ -0,0 +1,33 @@
import { isAsyncFunction } from '../../src/utils/basic'
describe('isAsyncFunction', () => {
it('should return true if the function is async', () => {
const asyncFn = async () => {}
expect(isAsyncFunction(asyncFn)).toBe(true)
})
it('should return false if the function is not async', () => {
const syncFn = () => {}
expect(isAsyncFunction(syncFn)).toBe(false)
})
it('should return false if the function is not a function', () => {
const notFn = 'not a function'
expect(isAsyncFunction(notFn)).toBe(false)
})
it('should return false if the function is a class', () => {
class MyClass {}
expect(isAsyncFunction(MyClass)).toBe(false)
})
it('should return false if the function is a Promise', () => {
const promise = Promise.resolve('')
expect(isAsyncFunction(promise)).toBe(false)
})
})

View File

@ -0,0 +1,33 @@
import { isPromise } from '../../src/utils/basic'
describe('isPromise', () => {
it('should return true if the value is a Promise', () => {
const promise = Promise.resolve('')
expect(isPromise(promise)).toBe(true)
})
it('should return false if the value is not a Promise', () => {
const notPromise = 'not a Promise'
expect(isPromise(notPromise)).toBe(false)
})
it('should return false if the value is a class', () => {
class MyClass {}
expect(isPromise(MyClass)).toBe(false)
})
it('should return false if the value is a function', () => {
const fn = () => {}
expect(isPromise(fn)).toBe(false)
})
it('should return true if the value is an async function', () => {
const asyncFn = async () => {}
expect(isPromise(asyncFn)).toBe(true)
})
})

View File

@ -0,0 +1,48 @@
import { isValueType } from '../../src/utils/basic'
describe('isValueType', () => {
it('should return true for string', () => {
expect(isValueType<string>('string', 'String')).toBe(true)
})
it('should return true for number', () => {
expect(isValueType<number>(123, 'Number')).toBe(true)
})
it('should return true for array', () => {
expect(isValueType<unknown[]>([], 'Array')).toBe(true)
})
it('should return true for null', () => {
expect(isValueType<null>(null, 'Null')).toBe(true)
})
it('should return true for undefined', () => {
expect(isValueType<undefined>(void 0, 'Undefined')).toBe(true)
})
it('should return true for object', () => {
expect(isValueType<object>({}, 'Object')).toBe(true)
})
it('should return true for Map', () => {
expect(isValueType<Map<unknown, unknown>>(new Map(), 'Map')).toBe(true)
})
it('should return true for Set', () => {
expect(isValueType<Set<unknown>>(new Set(), 'Set')).toBe(true)
})
it('should return true for Date', () => {
expect(isValueType<Date>(new Date(), 'Date')).toBe(true)
})
it('should return true for RegExp', () => {
expect(isValueType<RegExp>(/a/i, 'RegExp')).toBe(true)
})
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

@ -0,0 +1,20 @@
import { uuid } from '../../src/utils/basic'
describe('uuid', () => {
it('should return String', () => {
expectTypeOf(uuid()).toEqualTypeOf<string>()
})
it('the return value should be unique', () => {
const uuid1 = uuid()
const uuid2 = uuid()
expect(uuid1).not.toBe(uuid2)
})
it('should return a string with length 36', () => {
const uid = uuid(36)
expect(uid.length).toBe(36)
})
})

117
__test__/cache/index.spec.ts vendored Normal file
View File

@ -0,0 +1,117 @@
import {
hasStorage,
setStorage,
getStorage,
removeStorage,
} from '../../src/utils/cache'
describe('cache utils', () => {
const __DEMO__KEY = '__DEMO__KEY'
const __DEMO__VALUE = '__DEMO__VALUE'
const __PRE__KEY = '__PRE__KEY'
it('use setStorage set cache in localStorage and sessionStorage', () => {
setStorage(__DEMO__KEY, __DEMO__VALUE, 'sessionStorage')
setStorage(__DEMO__KEY, __DEMO__VALUE, 'localStorage')
expect(hasStorage(__DEMO__KEY, 'sessionStorage')).toBe(true)
expect(hasStorage(__DEMO__KEY, 'localStorage')).toBe(true)
})
it('use getStorage get cache', () => {
expect(getStorage(__DEMO__KEY, 'sessionStorage')).toBe(__DEMO__VALUE)
expect(getStorage(__DEMO__KEY, 'localStorage')).toBe(__DEMO__VALUE)
})
it('use removeStorage remove cache', () => {
removeStorage(__DEMO__KEY, 'sessionStorage')
removeStorage(__DEMO__KEY, 'localStorage')
expect(hasStorage(__DEMO__KEY, 'sessionStorage')).toBe(false)
expect(hasStorage(__DEMO__KEY, 'localStorage')).toBe(false)
})
it('use removeStorage remove all localStorage and sessionStorage cache', () => {
setStorage(__DEMO__KEY, __DEMO__VALUE, 'sessionStorage')
setStorage(__DEMO__KEY, __DEMO__VALUE, 'localStorage')
removeStorage('__all_sessionStorage__', 'sessionStorage')
removeStorage('__all_localStorage__', 'localStorage')
expect(hasStorage(__DEMO__KEY, 'sessionStorage')).toBe(false)
expect(hasStorage(__DEMO__KEY, 'localStorage')).toBe(false)
})
it('use removeStorage remove all cache', () => {
setStorage(__DEMO__KEY, __DEMO__VALUE, 'sessionStorage')
setStorage(__DEMO__KEY, __DEMO__VALUE, 'localStorage')
removeStorage('__all__', 'all')
expect(hasStorage(__DEMO__KEY, 'sessionStorage')).toBe(false)
expect(hasStorage(__DEMO__KEY, 'localStorage')).toBe(false)
})
it('setStorage with prefix', () => {
setStorage(__DEMO__KEY, __DEMO__VALUE, 'sessionStorage', {
prefix: true,
prefixKey: __PRE__KEY,
})
setStorage(__DEMO__KEY, __DEMO__VALUE, 'localStorage', {
prefix: true,
prefixKey: __PRE__KEY,
})
expect(
hasStorage(__DEMO__KEY, 'sessionStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(true)
expect(
hasStorage(__DEMO__KEY, 'localStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(true)
})
it('getStorage with prefix', () => {
expect(
getStorage(__DEMO__KEY, 'sessionStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(__DEMO__VALUE)
expect(
getStorage(__DEMO__KEY, 'localStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(__DEMO__VALUE)
})
it('removeStorage with prefix', () => {
removeStorage(__DEMO__KEY, 'sessionStorage', {
prefix: true,
prefixKey: __PRE__KEY,
})
removeStorage(__DEMO__KEY, 'localStorage', {
prefix: true,
prefixKey: __PRE__KEY,
})
expect(
hasStorage(__DEMO__KEY, 'sessionStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(false)
expect(
hasStorage(__DEMO__KEY, 'localStorage', {
prefix: true,
prefixKey: __PRE__KEY,
}),
).toBe(false)
})
})

View File

@ -0,0 +1,36 @@
import { RModal } from '../../src/components/base/RModal/index'
import { mount } from '@vue/test-utils'
describe('RModal', () => {
it('should execute the onAfterEnter callback', () => {
mount(RModal, {
props: {
show: true,
onAfterEnter: () => {
assert(true)
},
},
slots: {
default: h('div', 'Hello World'),
},
})
})
it('should render a modal', async () => {
const wrapper = mount(RModal, {
props: {
show: true,
},
slots: {
default: h('div', 'Hello World'),
},
})
const classStr = 'n-modal-container'
const modal = document.body.querySelector(`.${classStr}`)
const modalClassList = Array.from(modal?.classList || [])
expect(modalClassList.length).not.toBe(0)
expect(modalClassList.includes(classStr)).toBe(true)
})
})

View File

@ -0,0 +1,49 @@
import { printDom } from '../../src/utils/dom'
import { mount } from '@vue/test-utils'
import renderHook from '../utils/renderHook'
// happy-dom 官方有一个 bug无法使用 canvas.toDataURL 方法。所以该模块单测暂时无法通过
describe('printDom', () => {
// let count = 1
// const domRef = ref<HTMLElement>()
// const canvas = document.createElement('canvas')
// canvas.width = 100
// canvas.height = 100
// console.log('canvas.toDataURL result', canvas.toDataURL)
// const wrapper = mount(
// defineComponent({
// setup() {
// const print = () => {
// count = 2
// printDom(canvas, {
// domToImageOptions: {
// created: () => {
// count = 2
// },
// },
// })
// }
// return {
// domRef,
// print,
// }
// },
// render() {
// const { print } = this
// return (
// <>
// <div ref="domRef">print html</div>
// <button onClick={print.bind(this)}>print</button>
// </>
// )
// },
// }),
// )
// it('print dom', () => {
// const button = wrapper.find('button')
// button.trigger('click')
// expect(count).toBe(2)
// })
it('print dom', () => {})
})

View File

@ -0,0 +1,19 @@
import { autoPrefixStyle } from '../../src/utils/element'
describe('autoPrefixStyle', () => {
it('should be defined', () => {
expect(autoPrefixStyle).toBeDefined()
})
it('should complete css prefix', () => {
const result = autoPrefixStyle('transform')
expect(result).toEqual({
webkitTransform: 'transform',
mozTransform: 'transform',
msTransform: 'transform',
oTransform: 'transform',
transform: 'transform',
})
})
})

View File

@ -0,0 +1,68 @@
import { setClass, hasClass, removeClass } from '../../src/utils/element'
import createRefElement from '../utils/createRefElement'
describe('setClass', () => {
const wrapper = createRefElement()
const CLASS_NAME = 'test'
const CLASS_NAME_2 = 'test2'
it('set ref element class', () => {
setClass(wrapper.element, CLASS_NAME)
const classList = Array.from(wrapper.element.classList)
expect(classList.includes(CLASS_NAME)).toBe(true)
})
it('set ref element class with multiple class names', () => {
setClass(wrapper.element, `${CLASS_NAME} ${CLASS_NAME_2}`)
const classList = Array.from(wrapper.element.classList)
expect(classList.includes(CLASS_NAME)).toBe(true)
expect(classList.includes(CLASS_NAME_2)).toBe(true)
})
it('set ref element class with multiple class names use array params', () => {
setClass(wrapper.element, [CLASS_NAME, CLASS_NAME_2])
const classList = Array.from(wrapper.element.classList)
expect(classList.includes(CLASS_NAME)).toBe(true)
expect(classList.includes(CLASS_NAME_2)).toBe(true)
})
it('get ref element class', () => {
setClass(wrapper.element, CLASS_NAME)
const hasClassResult = hasClass(wrapper.element, CLASS_NAME)
expect(hasClassResult.value).toBe(true)
})
it('get ref element class with multiple class names', () => {
setClass(wrapper.element, `${CLASS_NAME} ${CLASS_NAME_2}`)
const hasClassResult = hasClass(wrapper.element, CLASS_NAME)
expect(hasClassResult.value).toBe(true)
})
it('get ref element class with multiple class names use array params', () => {
setClass(wrapper.element, [CLASS_NAME, CLASS_NAME_2])
const hasClassResult = hasClass(wrapper.element, CLASS_NAME)
expect(hasClassResult.value).toBe(true)
})
it('remove ref element class', () => {
setClass(wrapper.element, CLASS_NAME)
removeClass(wrapper.element, CLASS_NAME)
const classList = Array.from(wrapper.element.classList)
expect(classList.includes(CLASS_NAME)).toBe(false)
})
})

View File

@ -0,0 +1,23 @@
import { colorToRgba } from '../../src/utils/element'
describe('colorToRgba', () => {
it('should be defined', () => {
expect(colorToRgba).toBeDefined()
})
it('should return rgba color', () => {
expect(colorToRgba('rgb(255, 255, 255)', 0.5)).toBe(
'rgba(255, 255, 255, 0.5)',
)
expect(colorToRgba('rgba(255, 255, 255, 0.5)', 0.5)).toBe(
'rgba(255, 255, 255, 0.5)',
)
expect(colorToRgba('#fff', 0.1)).toBe('rgba(255, 255, 255, 0.1)')
expect(colorToRgba('#000000', 0.1)).toBe('rgba(0, 0, 0, 0.1)')
expect(colorToRgba('#fffffafa', 0.1)).toBe('rgba(255, 255, 250, 0.98)')
})
it('should return input color', () => {
expect(colorToRgba('hi')).toBe('hi')
})
})

View File

@ -0,0 +1,17 @@
import { completeSize } from '../../src/utils/element'
describe('completeSize', () => {
it('should be defined', () => {
expect(completeSize).toBeDefined()
})
it('should return size', () => {
expect(completeSize('100px')).toBe('100px')
expect(completeSize('100%')).toBe('100%')
expect(completeSize('100vw')).toBe('100vw')
})
it('should return default size', () => {
expect(completeSize(0)).toBe('0px')
})
})

View File

@ -0,0 +1,52 @@
import { queryElements } from '../../src/utils/element'
describe('queryElements', () => {
const div = document.createElement('div')
const CLASS_NAME = 'demo'
const ATTR_KEY = 'attr_key'
const ATTR_VALUE = 'attr_value'
it('should be defined', () => {
expect(queryElements).toBeDefined()
})
it('should return empty array', () => {
const el = queryElements('.demo')
expect(el?.length).toBe(0)
})
it('should return element list', () => {
div.parentNode?.removeChild(div)
div.classList.add(CLASS_NAME)
document.body.appendChild(div)
const el = queryElements('.demo')
expect(el?.length).toBe(1)
})
it('should return default element', () => {
div.parentNode?.removeChild(div)
const el = queryElements('.demo', {
defaultElement: document.body,
})
expect(el?.length).toBe(1)
})
it('should return element list by attr', () => {
div.parentNode?.removeChild(div)
div.setAttribute(ATTR_KEY, ATTR_VALUE)
document.body.appendChild(div)
const el = queryElements(`attr:${ATTR_KEY}`)
const el2 = queryElements(`attr:${ATTR_KEY}=${ATTR_VALUE}`)
expect(el?.length).toBe(1)
expect(el2?.length).toBe(1)
})
})

View File

@ -0,0 +1,71 @@
import { setStyle, removeStyle } from '../../src/utils/element'
import createRefElement from '../utils/createRefElement'
describe('setStyle', () => {
const div = document.createElement('div')
const removeKeys = ['width', 'height']
const wrapper = createRefElement()
document.body.appendChild(div)
it('should be defined', () => {
expect(setStyle).toBeDefined()
})
it('should set style', () => {
removeStyle(div, removeKeys)
setStyle(div, {
width: '100px',
height: '100px',
})
expect(div.style.width).toBe('100px')
expect(div.style.height).toBe('100px')
})
it('should set style with string', () => {
removeStyle(div, removeKeys)
setStyle(div, 'width: 100px; height: 100px;')
expect(div.style.width).toBe('100px')
expect(div.style.height).toBe('100px')
})
it('should set style with string array', () => {
removeStyle(div, removeKeys)
setStyle(div, ['width: 100px', 'height: 100px'])
expect(div.style.width).toBe('100px')
expect(div.style.height).toBe('100px')
})
it('should set style with css variable', () => {
removeStyle(div, ['--width', '--height'])
setStyle(div, {
'--width': '100px',
'--height': '100px',
})
expect(div.style.getPropertyValue('--width')).toBe('100px')
expect(div.style.getPropertyValue('--height')).toBe('100px')
})
it('should set style to ref element', () => {
const element = wrapper.vm.domRef as HTMLElement
const style = element.style
removeStyle(element, removeKeys)
setStyle(element, {
width: '100px',
height: '100px',
})
expect(style.width).toBe('100px')
expect(style.height).toBe('100px')
})
})

View File

@ -0,0 +1,18 @@
import { useAppNavigation } from '../../src/hooks/template/useAppNavigation'
import { useMenuGetters } from '../../src/store'
import setupMiniApp from '../utils/setupMiniApp'
describe('useAppNavigation', async () => {
await setupMiniApp()
const { navigationTo } = useAppNavigation()
const { getMenuOptions } = useMenuGetters()
it('navigationTo', () => {
const [dashboard] = getMenuOptions.value
expect(navigationTo(dashboard.fullPath)).toBeUndefined()
expect(navigationTo(dashboard)).toBeUndefined()
expect(navigationTo(0)).toBeUndefined()
})
})

View File

@ -0,0 +1,51 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useAppRoot } from '../../src/hooks/template/useAppRoot'
describe('useAppRoot', async () => {
await setupMiniApp()
const { setRootRoute } = useAppRoot()
it(`should return '/test' and 'test'`, () => {
setRootRoute({
path: '/test',
name: 'test',
})
const { getRootPath, getRootName } = useAppRoot()
expect(getRootPath.value).toBe('/test')
expect(getRootName.value).toBe('test')
})
it(`should be returned a object like: {path: /test2, name: test2}`, () => {
const baseRootRoute = {
path: '/test2',
name: 'test2',
}
setRootRoute(baseRootRoute)
const { getRootRoute } = useAppRoot()
expect(getRootRoute.value).toEqual(baseRootRoute)
})
it('should update root route when setRootRoute is called', () => {
const baseRootRoute = {
path: '/test3',
name: 'test3',
}
setRootRoute({
path: '/test3',
name: 'test3',
})
const { getRootPath, getRootName, getRootRoute } = useAppRoot()
expect(getRootPath.value).toBe('/test3')
expect(getRootName.value).toBe('test3')
expect(getRootRoute.value).toEqual(baseRootRoute)
})
})

View File

@ -0,0 +1,67 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useBadge } from '../../src/hooks/template/useBadge'
import { useMenuGetters } from '../../src/store'
import type { AppMenuExtraOptions } from '../../src/router/types'
describe('useBadge', async () => {
await setupMiniApp()
const { show, hidden, update } = useBadge()
const { getMenuOptions } = useMenuGetters()
const baseBadge = (extra: AppMenuExtraOptions) =>
Object.assign(
{},
{
label: 'new',
type: 'error',
} as AppMenuExtraOptions,
extra,
)
it('should hide badge', () => {
const [dashboard] = getMenuOptions.value
update(
dashboard,
baseBadge({
show: false,
label: 'new',
}),
)
hidden(dashboard)
expect(dashboard.meta.extra?.show).toBe(false)
})
it('should show badge', () => {
const [dashboard] = getMenuOptions.value
update(
dashboard,
baseBadge({
show: true,
label: 'new',
}),
)
show(dashboard)
expect(dashboard.meta.extra?.show).toBe(true)
})
it('should show badge with new label', () => {
const [dashboard] = getMenuOptions.value
const label = 'update new'
update(
dashboard,
baseBadge({
show: true,
label,
}),
)
expect(dashboard.meta.extra?.label).toBe(label)
})
})

View File

@ -0,0 +1,50 @@
import { useContextmenuCoordinate } from '../../src/hooks/components/useContextmenuCoordinate'
import renderHook from '../utils/renderHook'
import createRefElement from '../utils/createRefElement'
describe('useContextmenuCoordinate', () => {
const wrapperRef = createRefElement()
const [result] = renderHook(() =>
useContextmenuCoordinate(wrapperRef.element),
)
it('should be defined', () => {
expect(useContextmenuCoordinate).toBeDefined()
})
it('should update show value to true when contextmenu event is triggered', async () => {
wrapperRef.element.dispatchEvent(new MouseEvent('contextmenu'))
await nextTick()
expect(result.show.value).toBe(true)
})
it('should update show value when calling updateShow method', async () => {
result.updateShow(false)
await nextTick()
expect(result.show.value).toBe(false)
result.updateShow(true)
await nextTick()
expect(result.show.value).toBe(true)
})
it('should get the clientX and clientY value when contextmenu event is triggered', async () => {
const event = new MouseEvent('contextmenu', {
clientX: 100,
clientY: 200,
})
wrapperRef.element.dispatchEvent(event)
await nextTick()
expect(result.x.value).toBe(100)
expect(result.y.value).toBe(200)
})
})

View File

@ -0,0 +1,101 @@
import { useDayjs } from '../../src/hooks/web/useDayjs'
import dayjs from 'dayjs'
describe('useDayjs', () => {
const {
locale,
getStartAndEndOfDay,
format,
isDayjs,
daysDiff,
isDateInRange,
} = useDayjs()
it('check whether the locale method runs properly', () => {
const m = {
locale,
}
const localSpy = vi.spyOn(m, 'locale')
m.locale('en-US')
m.locale('zh-CN')
expect(localSpy).toHaveBeenCalledTimes(2)
})
it('gets Returns the current date, start time, and end time of the current date ', () => {
const formatOptions = {
format: 'YYYY/M/DD HH:mm:ss',
}
const formatOptions2 = {
format: 'YYYY/M/DD',
}
const {
today,
startOfDay,
endOfDay,
formatToday,
formatStartOfDay,
formatEndOfDay,
} = getStartAndEndOfDay(formatOptions)
const _today = dayjs(new Date()).format(formatOptions2.format)
const _startOfDay = dayjs(new Date().setHours(0, 0, 0, 0)).format(
formatOptions.format,
)
const _endOfDay = dayjs(new Date().setHours(23, 59, 59, 999)).format(
formatOptions.format,
)
expect(format(today, formatOptions2)).toBe(_today)
expect(format(startOfDay, formatOptions)).toBe(_startOfDay)
expect(format(endOfDay, formatOptions)).toBe(_endOfDay)
expect(format(formatToday, formatOptions2)).toBe(_today)
expect(formatStartOfDay).toBe(_startOfDay)
expect(formatEndOfDay).toBe(_endOfDay)
})
it('check format method', () => {
const formatOptions1 = {
format: 'YYYY/M/DD HH:mm:ss',
}
const formatOptions2 = {
format: 'YYYY/M/DD',
}
const formatOptions3 = {
format: 'YYYY-MM-DD HH:mm:ss',
}
const formatOptions4 = {
format: 'YYYY-MM-DD',
}
const date = new Date('2022-01-11 00:00:00')
expect(format(date, formatOptions1)).toBe('2022/1/11 00:00:00')
expect(format(date, formatOptions2)).toBe('2022/1/11')
expect(format(date, formatOptions3)).toBe('2022-01-11 00:00:00')
expect(format(date, formatOptions4)).toBe('2022-01-11')
})
it('check isDayjs object', () => {
const { today } = getStartAndEndOfDay()
expect(isDayjs(new Date())).toBe(false)
expect(isDayjs(today)).toBe(true)
})
it('check daysDiff method', () => {
expect(daysDiff('2022-01-11', '2022-01-12')).toBe(1)
expect(daysDiff('2021-01-11', '2022-01-12')).toBe(366)
expect(daysDiff('2023-01-11', '2022-01-12')).toBe(-364)
})
it('check isDateInRange method', () => {
const range = {
start: '2023-01-15',
end: '2023-01-20',
}
expect(isDateInRange('2023-01-16', range)).toBe(true)
expect(isDateInRange('2023-01-15', range)).toBe(false)
expect(isDateInRange('2023-01-20', range)).toBe(false)
})
})

View File

@ -0,0 +1,116 @@
import { useDevice } from '../../src/hooks/web/useDevice'
describe('useDevice', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener')
const matchMediaSpy = vi
.spyOn(window, 'matchMedia')
.mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}))
beforeEach(() => {
addEventListenerSpy.mockClear()
matchMediaSpy.mockClear()
})
afterAll(() => {
addEventListenerSpy.mockRestore()
matchMediaSpy.mockRestore()
})
it('should be defined', () => {
expect(useDevice).toBeDefined()
})
it('should work', () => {
const { width, height } = useDevice({
initialWidth: 100,
initialHeight: 200,
})
expect(width.value).toBe(window.innerWidth)
expect(height.value).toBe(window.innerHeight)
})
it('should exclude scrollbar', () => {
const { width, height } = useDevice({
initialWidth: 100,
initialHeight: 200,
includeScrollbar: false,
})
expect(width.value).toBe(window.document.documentElement.clientWidth)
expect(height.value).toBe(window.document.documentElement.clientHeight)
})
it('sets handler for window resize event', async () => {
useDevice({
initialWidth: 100,
initialHeight: 200,
listenOrientation: false,
})
await nextTick()
expect(addEventListenerSpy).toHaveBeenCalledOnce()
const call = addEventListenerSpy.mock.calls[0]
expect(call[0]).toEqual('resize')
expect(call[2]).toEqual({
passive: true,
})
})
it('sets handler for window.matchMedia("(orientation: portrait)") change event', async () => {
useDevice({
initialWidth: 100,
initialHeight: 200,
})
await nextTick()
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
expect(matchMediaSpy).toHaveBeenCalledTimes(1)
const call = matchMediaSpy.mock.calls[0]
expect(call[0]).toEqual('(orientation: portrait)')
})
it('should update width and height on window resize', async () => {
const { width, height } = useDevice({
initialWidth: 100,
initialHeight: 200,
})
window.innerWidth = 300
window.innerHeight = 400
window.dispatchEvent(new Event('resize'))
await nextTick()
expect(width.value).toBe(300)
expect(height.value).toBe(400)
})
it('should update isTabletOrSmaller on window resize', async () => {
const { isTabletOrSmaller } = useDevice()
window.innerWidth = 300
window.dispatchEvent(new Event('resize'))
await nextTick()
expect(isTabletOrSmaller.value).toBe(true)
})
})

View File

@ -0,0 +1,54 @@
import { useElementFullscreen } from '../../src/hooks/web/useElementFullscreen'
describe('useElementFullscreen', async () => {
const div = document.createElement('div')
const transition = 'all 0.3s var(--r-bezier)'
const __ID__ = '__ID__'
div.setAttribute('id', __ID__)
document.body.appendChild(div)
const resetDivStyle = () => {
const element = document.getElementById(__ID__)
if (element) {
element.style.transition = ''
}
}
const { enter, exit, toggleFullscreen } = useElementFullscreen(div)
it('should enter fullscreen', async () => {
resetDivStyle()
enter()
await nextTick()
expect(div.style.transition).toBe(transition)
})
it('should exit fullscreen', async () => {
resetDivStyle()
exit()
await nextTick()
expect(div.style.transition).toBe('')
})
it('should toggle fullscreen', async () => {
resetDivStyle()
enter()
enter() // 为了兼容测试环境,故而调用两次
await nextTick()
expect(div.style.transition).toBe(transition)
toggleFullscreen()
await nextTick()
expect(!div.style.transition).not.toBe(true)
})
})

View File

@ -0,0 +1,66 @@
import { usePagination } from '../../src/hooks/web/usePagination'
describe('usePagination', () => {
let count = 0
const defaultOptions = {
itemCount: 200,
page: 1,
pageSize: 10,
}
const [
_,
{
getItemCount,
getCallback,
getPage,
getPageSize,
getPagination,
setItemCount,
setPage,
setPageSize,
},
] = usePagination(() => {
count++
}, defaultOptions)
it('should get current itemCount', () => {
setItemCount(200)
expect(getItemCount()).toBe(200)
setItemCount(100)
expect(getItemCount()).toBe(100)
})
it('should get current page', () => {
setPage(1)
expect(getPage()).toBe(1)
})
it('should get current pageSize', () => {
setPageSize(10)
expect(getPageSize()).toBe(10)
})
it('should get current pagination', () => {
setItemCount(200)
expect(getPagination()).toMatchObject(defaultOptions)
})
it('should update count when page or pageSize changes', () => {
count = 0
setPage(2)
expect(count).toBe(1)
setPageSize(20)
expect(count).toBe(2)
})
})

View File

@ -0,0 +1,167 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useSiderBar } from '../../src/hooks/template/useSiderBar'
import { useMenuGetters, useMenuActions } from '../../src/store'
import { useVueRouter } from '../../src/hooks/web/useVueRouter'
import type { AppMenuOption, MenuTagOptions } from '../../src/types/modules/app'
describe('useSiderBar', async () => {
await setupMiniApp()
const { setMenuTagOptions, resolveOption } = useMenuActions()
const {
close,
closeAll,
closeRight,
closeLeft,
closeOther,
getCurrentTagIndex,
checkCloseRight,
checkCloseLeft,
} = useSiderBar()
const updateMenuTagOptions = () => {
const { router } = useVueRouter()
const routes = router.getRoutes() as unknown as AppMenuOption[]
routes.forEach((curr) =>
setMenuTagOptions(
resolveOption({
...curr,
fullPath: curr.path,
}),
true,
),
)
}
it('should close target tag', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [dashboard] = getMenuOptions.value
const beforeIndex = getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
close(dashboard.fullPath)
await nextTick()
const afterIndex = getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
expect(beforeIndex).not.toBe(afterIndex)
})
it('should close all tags', async () => {
updateMenuTagOptions()
const { getMenuTagOptions } = useMenuGetters()
closeAll()
await nextTick()
const afterLength = getMenuTagOptions.value.length
expect(afterLength).toBe(1)
})
it('should close right tags', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [dashboard] = getMenuOptions.value
const beforeIndex = getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
expect(!!getMenuTagOptions.value[beforeIndex + 1]).toBe(true)
closeRight(dashboard.fullPath)
await nextTick()
const afterIndex = getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
expect(getMenuTagOptions.value[afterIndex + 1]).toBe(void 0)
})
it('should close left tags', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [dashboard] = getMenuOptions.value
closeLeft(dashboard.fullPath)
await nextTick()
const afterIndex = getMenuTagOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
expect(getMenuTagOptions.value[afterIndex - 1]).toBe(void 0)
})
it('should get current tag index', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [dashboard] = getMenuOptions.value
const index = getMenuOptions.value.findIndex(
(curr) => curr.fullPath === dashboard.fullPath,
)
expect(getCurrentTagIndex()).toBe(index)
})
it('should close other tags', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [dashboard] = getMenuOptions.value
closeOther(dashboard.fullPath)
await nextTick()
expect(getMenuTagOptions.value[0].fullPath).toBe(dashboard.fullPath)
expect(getMenuTagOptions.value.length).toBe(1)
})
it('check menuTagOptions left or right can close', async () => {
updateMenuTagOptions()
const { getMenuOptions, getMenuTagOptions } = useMenuGetters()
const [f, s] = getMenuOptions.value
closeRight(f.fullPath)
await nextTick()
const canClose = checkCloseRight(f.fullPath)
expect(canClose).toBe(false)
updateMenuTagOptions()
closeLeft(f.fullPath)
await nextTick()
const canCloseLeft = checkCloseLeft(f.fullPath)
expect(canCloseLeft).toBe(false)
updateMenuTagOptions()
expect(checkCloseRight(s.fullPath)).toBe(true)
expect(checkCloseLeft(s.fullPath)).toBe(true)
})
})

View File

@ -0,0 +1,38 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useSpinning } from '../../src/hooks/template/useSpinning'
import { setVariable, getVariableToRefs } from '../../src/global-variable'
describe('useSpinning', async () => {
await setupMiniApp()
const { reload, openSpin, closeSpin } = useSpinning()
const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad')
it('should open spinning', () => {
openSpin()
expect(globalMainLayoutLoad.value).toBe(true)
})
it('should close spinning', () => {
openSpin()
expect(globalMainLayoutLoad.value).toBe(true)
closeSpin()
expect(globalMainLayoutLoad.value).toBe(true)
})
it('should reload', () => {
const wait = 1000
reload(wait)
expect(globalMainLayoutLoad.value).toBe(false)
setTimeout(() => {
expect(globalMainLayoutLoad.value).toBe(true)
}, wait)
})
})

View File

@ -0,0 +1,46 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useTheme } from '../../src/hooks/template/useTheme'
describe('useTheme', async () => {
await setupMiniApp()
const { darkTheme, lightTheme, toggleTheme, getAppTheme } = useTheme()
it('should change to dark theme', () => {
darkTheme()
expect(getAppTheme().theme).toBe(true)
})
it('should change to light theme', () => {
lightTheme()
expect(getAppTheme().theme).toBe(false)
})
it('should toggle theme', () => {
lightTheme()
expect(getAppTheme().theme).toBe(false)
toggleTheme()
expect(getAppTheme().theme).toBe(true)
})
it('should return current theme', () => {
darkTheme()
const { theme: _darkTheme, themeLabel: _darkThemeLabel } = getAppTheme()
expect(_darkTheme).toBe(true)
expect(_darkThemeLabel).toBe('Dark')
lightTheme()
const { theme: __lightTheme, themeLabel: __lightThemeLabel } = getAppTheme()
expect(__lightTheme).toBe(false)
expect(__lightThemeLabel).toBe('Light')
})
})

View File

@ -0,0 +1,15 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useVueRouter } from '../../src/hooks/web/useVueRouter'
describe('useVueRouter', async () => {
await setupMiniApp()
const { router } = useVueRouter()
it('should get push and replace methods', () => {
const { push, replace } = router
assert.isFunction(push)
assert.isFunction(replace)
})
})

View File

@ -0,0 +1,33 @@
import setupMiniApp from '../utils/setupMiniApp'
import { useWatermark } from '../../src/hooks/template/useWatermark'
import { useSettingGetters } from '../../src/store'
describe('useWatermark', async () => {
await setupMiniApp()
const { setWatermarkContent, showWatermark, hiddenWatermark } = useWatermark()
it('should set watermark content', () => {
const { getWatermarkConfig } = useSettingGetters()
const watermarkConfig = getWatermarkConfig.value
const content = 'Ray Template Yes!'
setWatermarkContent(content)
expect(watermarkConfig.content).toBe(content)
})
it('should update watermark', () => {
showWatermark()
const { getWatermarkSwitch: show } = useSettingGetters()
expect(show.value).toBe(true)
hiddenWatermark()
const { getWatermarkSwitch: hidden } = useSettingGetters()
expect(hidden.value).toBe(false)
})
})

View File

@ -0,0 +1,82 @@
import {
isCurrency,
format,
add,
subtract,
multiply,
divide,
distribute,
} from '../../src/utils/precision'
describe('precision', () => {
it('check value is currency object', () => {
expect(isCurrency(1)).toBeFalsy()
expect(isCurrency('1')).toBeFalsy()
expect(isCurrency({})).toBeFalsy()
expect(isCurrency({ s: 1 })).toBeFalsy()
expect(isCurrency(add(1, 1))).toBeTruthy()
})
it('format value', () => {
expect(format(1)).toBe(1)
expect(
format(1.1, {
type: 'number',
}),
).toBe(1.1)
expect(
format(1.11, {
type: 'string',
precision: 2,
}),
).toBe('1.11')
expect(format(add(1, 1))).toBe(2)
expect(format(add(0.1, 0.2))).toBe(0.3)
})
it('add value', () => {
expect(format(add(1, 1))).toBe(2)
expect(format(add(0.1, 0.2))).toBe(0.3)
expect(format(add(0.1, 0.2, 0.3))).toBe(0.6)
expect(format(add(0.1, 0.2, 0.3, 0.4))).toBe(1)
expect(format(add(0.1, 0.2, 0.3, 0.4, 0.5))).toBe(1.5)
})
it('subtract value', () => {
expect(format(subtract(1, 1))).toBe(0)
expect(format(subtract(0.3, 0.2))).toBe(0.1)
expect(format(subtract(0.6, 0.3, 0.2))).toBe(0.1)
expect(format(subtract(1, 0.5, 0.4, 0.3, 0.2))).toBe(-0.4)
})
it('multiply value', () => {
expect(format(multiply(1, 1))).toBe(1)
expect(format(multiply(0.1, 0.2))).toBe(0.02)
expect(format(multiply(0.1, 0.2, 0.3))).toBe(0.006)
expect(format(multiply(0.1, 0.2, 0.3, 0.4))).toBe(0.0024)
expect(format(multiply(0.1, 0.2, 0.3, 0.4, 0.5))).toBe(0.0012)
})
it('divide value', () => {
expect(format(divide(1, 1))).toBe(1)
expect(format(divide(0.1, 0.2))).toBe(0.5)
expect(
format(divide(0.1, 0.2, 0.3), {
precision: 2,
}),
).toBe(1.67)
})
it('distribute value', () => {
expect(distribute(1, 1)).toEqual([1])
expect(distribute(1, 0)).toEqual([1])
expect(distribute(0, 3)).toEqual([0, 0, 0])
expect(distribute(10, 3)).toEqual([3.33333334, 3.33333333, 3.33333333])
expect(
distribute(20, 3, {
precision: 4,
}),
).toEqual([6.6667, 6.6667, 6.6666])
expect(distribute(add(20, 1), 3)).toEqual([7, 7, 7])
})
})

View File

@ -0,0 +1,16 @@
/**
*
* @description
* DOM
*
* DOM true false
*/
const canUseDom = () => {
return !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
)
}
export default canUseDom

View File

@ -0,0 +1,36 @@
import { mount } from '@vue/test-utils'
/**
*
* @param slots
*
* @description
* ref domRef
*
*
* @example
* const wrapper = createRefElement({ default: () => <div>hello</div> })
*
* 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({
setup() {
const domRef = ref<HTMLElement>()
return {
domRef,
}
},
render() {
return <div ref="domRef">{{ ...slots }}</div>
},
}),
)
return wrapper
}
export default createRefElement

View File

@ -0,0 +1,15 @@
/**
*
* @description
*
*
* true false
*/
const isBrowser = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
)
export default isBrowser

View File

@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createApp, defineComponent } from 'vue'
import type { App } from 'vue'
export default function renderHook<R = any>(
renderFC: () => R,
): [
R,
App<Element>,
{
act?: (fn: () => void) => void
},
] {
let result: any
let act: ((fn: () => void) => void) | undefined
const app = createApp(
defineComponent({
setup() {
result = renderFC()
act = (fn: () => void) => {
fn()
}
return () => {}
},
}),
)
app.mount(document.createElement('div'))
return [result, app, { act }]
}

View File

@ -0,0 +1,27 @@
import { setupStore } from '../../src/store'
import { setupRouter } from '../../src/router'
import { setupI18n } from '../../src/locales'
import renderHook from '../utils/renderHook'
/**
*
* @description
* mini ray template
* storerouteri18n
*
* @example
* const { app } = await setupMiniApp()
*/
const setupMiniApp = async () => {
const [_, app] = renderHook(() => {})
setupStore(app)
setupRouter(app)
await setupI18n(app)
return {
app,
}
}
export default setupMiniApp

17
__test__/utils/sleep.ts Normal file
View File

@ -0,0 +1,17 @@
/**
*
* @param timer
*
* @description
*
*
* @example
* await sleep(1000)
*/
const sleep = (timer: number) => {
return new Promise((resolve) => {
setTimeout(resolve, timer)
})
}
export default sleep

27
__test__/vue/call.spec.ts Normal file
View File

@ -0,0 +1,27 @@
import { call } from '../../src/utils/vue/call'
describe('call', () => {
it('should be executed once', () => {
const fn = vi.fn()
call(() => fn())
expect(fn).toHaveBeenCalledTimes(1)
})
it('should be executed with an argument', () => {
const fn = vi.fn()
call((a: number) => fn(a), 1)
expect(fn).toHaveBeenCalledWith(1)
})
it('should be executed with multiple arguments', () => {
const fn = vi.fn()
call((a: number, b: number) => fn(a, b), 1, 2)
expect(fn).toHaveBeenCalledWith(1, 2)
})
})

View File

@ -0,0 +1,7 @@
import { effectDispose } from '../../src/utils/vue/effect-dispose'
describe('effectDispose', () => {
it('should return false if getCurrentScope is null', () => {
expect(effectDispose(() => {})).toBe(false)
})
})

View File

@ -0,0 +1,14 @@
import { renderNode } from '../../src/utils/vue/render-node'
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()
expect(text).toBe('hello world')
})
})

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

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -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,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-08-11
*
* @workspace ray-template
*
* @remark
*/
import Mock from 'mockjs'
/**

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-08-11
*
* @workspace ray-template
*
* @remark
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
export function array(length: number) {

View File

@ -1,20 +1,22 @@
{
"name": "ray-template",
"private": false,
"version": "4.7.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",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vue-tsc --noEmit && vite build --mode test",
"dev-build": "vue-tsc --noEmit && vite build --mode development",
"report": "vite build --mode report",
"prepare": "husky install"
"prepare": "husky install",
"test": "vitest",
"test:ui": "vitest --ui",
"lint": "vue-tsc --noEmit && eslint --fix && prettier --write \"**/*.{ts,tsx,json,.vue}\""
},
"husky": {
"hooks": {
@ -23,87 +25,89 @@
}
},
"lint-staged": {
"*.{js,json}": [
"*.{ts,tsx,json}": [
"prettier --write"
],
"*.ts?(x)": [
"eslint src",
"prettier --parser=typescript --write"
"*.{ts,tsx,vue}": [
"eslint --fix"
]
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"awesome-qr": "2.1.5-rc.0",
"axios": "^1.6.7",
"@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.1.1",
"crypto-js": "4.2.0",
"currency.js": "^2.0.4",
"dayjs": "^1.11.10",
"dom-to-image": "2.6.0",
"echarts": "^5.5.0",
"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.38.1",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"print-js": "^1.6.0",
"vue": "^3.4.21",
"vue-demi": "0.14.6",
"vue-hooks-plus": "1.8.8",
"vue-i18n": "^9.9.0",
"vue-router": "^4.2.5"
"vue": "^3.5.17",
"vue-demi": "0.14.10",
"vue-hooks-plus": "2.4.0",
"vue-i18n": "^9.13.1",
"vue-router": "^4.5.1",
"vue3-next-qrcode": "3.0.2"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/eslint-parser": "^7.23.3",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@interactjs/types": "1.10.21",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@types/crypto-js": "^4.1.1",
"@types/dom-to-image": "2.6.7",
"@types/lodash-es": "^4.17.11",
"@types/mockjs": "1.0.7",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue-hooks-plus/resolvers": "1.2.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"autoprefixer": "^10.4.15",
"depcheck": "^1.4.5",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.18.1",
"@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.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.1.0",
"postcss": "^8.4.31",
"postcss-px-to-viewport-8-plugin": "1.2.3",
"prettier": "^3.2.5",
"sass": "1.71.1",
"svg-sprite-loader": "^6.0.11",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.5",
"vite-bundle-analyzer": "0.8.1",
"vite-plugin-cdn2": "0.15.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.7.0",
"lint-staged": "15.4.3",
"postcss": "8.5.4",
"postcss-px-to-viewport-8-with-include": "1.2.2",
"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-ejs": "1.7.0",
"vite-plugin-eslint": "1.8.1",
"vite-plugin-imp": "^2.4.0",
"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",
"vue-tsc": "^1.8.27"
"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",

13991
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -8,31 +8,32 @@ module.exports = {
'ff > 31',
'ie >= 8',
'last 10 versions',
'not dead',
],
grid: true,
},
// 为了适配 postcss8.x 版本的转换库
'postcss-px-to-viewport-8-plugin': {
inlinePxToViewport: true,
/** 视窗的宽度(设计稿的宽度) */
'postcss-px-to-viewport-8-with-include': {
// 横屏时使用的视口宽度
landscapeWidth: 1920,
// 视窗的宽度(设计稿的宽度)
viewportWidth: 1920,
/** 视窗的高度(设计稿高度, 一般无需指定) */
viewportHeight: 1080,
/** 指定 px 转换为视窗单位值的小数位数 */
// 指定 px 转换为视窗单位值的小数位数
unitPrecision: 3,
/** 指定需要转换成的视窗单位 */
viewportUnit: 'rem',
/** 制定字体转换单位 */
fontViewportUnit: 'rem',
/** 指定不转换为视窗单位的类 */
// 指定需要转换成的视窗单位
viewportUnit: 'vw',
// 制定字体转换单位
fontViewportUnit: 'vw',
// 指定不转换为视窗单位的类
selectorBlackList: ['.ignore'],
/** 小于或等于 1px 不转换为视窗单位 */
// 小于或等于 1px 不转换为视窗单位
minPixelValue: 1,
/** 允许在媒体查询中转换 px */
// 允许在媒体查询中转换 px
mediaQuery: false,
// exclude: /(\/|\\)(node_modules)(\/|\\)/, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: [/^src[/\\].*\.(vue|tsx|jsx|ts(?!d))$/],
preserve: true,
// 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
exclude: /node_modules/,
// 指定一个空的文件夹,避免影响到无需转换的文件
include: [],
},
},
}

View File

@ -3,9 +3,11 @@ import AppNaiveGlobalProvider from '@/app-components/provider/AppNaiveGlobalProv
import AppStyleProvider from '@/app-components/provider/AppStyleProvider'
import AppLockScreen from '@/app-components/app/AppLockScreen'
import AppWatermarkProvider from '@/app-components/provider/AppWatermarkProvider'
import AppGlobalSpin from '@/spin'
import AppGlobalSpin from '@/app-components/app/AppGlobalSpin'
import AppVersionProvider from '@/app-components/provider/AppVersionProvider'
import { APP_GLOBAL_LOADING } from '@/app-config'
export default defineComponent({
name: 'App',
render() {
@ -18,7 +20,7 @@ export default defineComponent({
<AppGlobalSpin>
{{
default: () => <RouterView />,
description: () => 'loading...',
description: () => APP_GLOBAL_LOADING,
}}
</AppGlobalSpin>
</AppNaiveGlobalProvider>

View File

@ -9,6 +9,6 @@
当你需要在做一些定制化操作的时候,可以尝试在这个包里做一些事情。
后在 `main.ts` 中导入并且调用即可。
后在 `main.ts` 中导入并且调用即可。
> 出于一些考虑,并没有做自动化导入调用,所以需要自己手动来。(好吧,其实就是我懒--

View File

@ -1,2 +1,16 @@
export * from './valid/validAppRootPath'
export * from './valid/validLocal'
import { validAppRootPath } from './valid/valid-app-root-path'
import { validLocal } from './valid/valid-local'
/**
*
* @description
* ray-template
*/
export const setupRayTemplateCore = async () => {
if (!__DEV__) {
return
}
await validAppRootPath()
await validLocal()
}

View File

@ -11,10 +11,6 @@ import { useVueRouter } from '@/hooks'
* getRoutes
*/
export const validAppRootPath = async () => {
if (!__DEV__) {
return
}
const { getAppRootRoute } = useSettingGetters()
const {
router: { getRoutes },

View File

@ -92,10 +92,6 @@ const validDefaultDayjsLocal = () => {
* localConfig
*/
export const validLocal = async () => {
if (!__DEV__) {
return
}
validSystemDefaultLocal()
validSystemFallbackLocale()
validDayjsLocalMap()

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-03-31
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -39,7 +28,7 @@ interface JSONPlaceholder {
*
* @returns
*
* @medthod get
* @method get
*/
export const getWeather = (city: string) => {
return request<AxiosTestResponse>({

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-31
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -17,71 +6,65 @@
* session catch
*/
import './index.scss'
import { NAvatar, NButton, NFlex } from 'naive-ui'
import { NAvatar, NFlex } from 'naive-ui'
import { avatarProps, flexProps } from 'naive-ui'
import { APP_CATCH_KEY } from '@/app-config'
import { getStorage } from '@/utils'
import { avatarProps } from 'naive-ui'
import { useSigningGetters } from '@/store'
import type { PropType } from 'vue'
import type { AvatarProps, SpaceProps } from 'naive-ui'
import type { SigningCallback } from '@/store/modules/signing/type'
import type { AvatarProps, FlexProps } from 'naive-ui'
const AppAvatar = defineComponent({
name: 'AppAvatar',
props: {
...avatarProps,
...flexProps,
cursor: {
type: String,
default: 'auto',
},
spaceSize: {
type: [String, Number] as PropType<SpaceProps['size']>,
type: [String, Number, Array] as PropType<FlexProps['size']>,
default: 'medium',
},
avatarSize: {
type: [String, Number] as PropType<AvatarProps['size']>,
default: 'medium',
},
vertical: {
type: Boolean,
default: false,
},
setup(props) {
const signing = getStorage<SigningCallback>(APP_CATCH_KEY.signing)
const cssVars = computed(() => {
const vars = {
'--app-avatar-cursor': props.cursor,
}
return vars
})
},
setup() {
const { getSigningCallback } = useSigningGetters()
return {
signing,
cssVars,
getSigningCallback,
}
},
render() {
const { signing, cssVars, spaceSize, avatarSize, $props } = this
const { getSigningCallback, avatarSize, spaceSize, $props, vertical } = this
return (
<NFlex
class="app-avatar"
{...this.$props}
style={cssVars}
size={spaceSize}
>
<NButton quaternary strong focusable={false}>
<NFlex align="center" size={spaceSize} vertical={vertical}>
<NAvatar
// eslint-disable-next-line prettier/prettier, @typescript-eslint/no-explicit-any
{...($props as any)}
src={signing?.avatar}
{...($props as AvatarProps)}
src={getSigningCallback?.avatar}
objectFit="cover"
round
size={avatarSize}
/>
<div class="app-avatar__name">{signing?.name}</div>
>
{{
default: () =>
getSigningCallback.avatar
? null
: getSigningCallback?.name?.[0],
}}
</NAvatar>
{getSigningCallback?.name}
</NFlex>
</NButton>
)
},
})

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-01-18
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -31,6 +20,8 @@ import { NSpin } from 'naive-ui'
import { spinProps } from 'naive-ui'
import { getVariableToRefs } from '@/global-variable'
import type { SpinProps } from 'naive-ui'
const GlobalSpin = defineComponent({
name: 'GlobalSpin',
props: {
@ -50,11 +41,14 @@ const GlobalSpin = defineComponent({
render() {
return (
<NSpin
{...this.$props}
{...(this.$props as SpinProps)}
show={this.spinValue}
themeOverrides={this.overrides}
style="height: var(--html-height)"
>
{{ ...this.$slots }}
{{
...this.$slots,
}}
</NSpin>
)
},

View File

@ -1,28 +1,10 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
*
* ,
*/
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,30 +1,22 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
/** 锁屏界面 */
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()
@ -33,15 +25,17 @@ const LockScreen = defineComponent({
lockCondition: useCondition(),
})
/** 锁屏 */
const lockScreen = () => {
formInstRef.value?.validate((error) => {
if (!error) {
validate().then(() => {
setLockAppScreen(true)
updateSettingState('lockScreenSwitch', true)
updateSettingState('lockScreenSwitch', false)
setStorage(
APP_CATCH_KEY.appLockScreenPasswordKey,
encrypt(state.lockCondition.lockPassword),
'localStorage',
)
state.lockCondition = useCondition()
}
})
}
@ -54,19 +48,27 @@ const LockScreen = defineComponent({
return {
...toRefs(state),
lockScreen,
formInstRef,
register,
inputInstRef,
}
},
render() {
const { register } = this
return (
<div class="app-lock-screen__content">
<div class="app-lock-screen__input">
<AppAvatar vertical align="center" avatarSize={52} />
<NForm
<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
@ -83,12 +85,14 @@ const LockScreen = defineComponent({
this.lockScreen()
}
}}
autofocus
/>
</NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton>
</NForm>
</RForm>
</div>
</div>
)
},

View File

@ -1,32 +1,22 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
import '../../index.scss'
/** 解锁界面 */
import { NInput, NForm, NFormItem, NButton, NFlex } from 'naive-ui'
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()
@ -35,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),
})
@ -54,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('密码错误,请重新输入')
}
})
}
@ -91,22 +106,24 @@ 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__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'
@ -116,20 +133,28 @@ export default defineComponent({
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
</div>
</div>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar vertical align="center" avatarSize={52} />
<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}>
<RForm
onRegister={register}
model={this.lockCondition}
rules={rules}
>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
autofocus
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
@ -138,21 +163,27 @@ export default defineComponent({
/>
</NFormItem>
<NFlex justify="space-between">
<NButton type="primary" text onClick={backToSigning.bind(this)}>
<NButton
type="primary"
text
onClick={backToSigning.bind(this)}
>
</NButton>
<NButton type="primary" text onClick={unlockScreen.bind(this)}>
<NButton
type="primary"
text
onClick={unlockScreen.bind(this)}
>
</NButton>
</NFlex>
</NForm>
</RForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-date">
{HH_MM}&nbsp;<span>{AM_PM}</span>
</div>
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>
{YY_MM_DD}&nbsp;<span>{DDD}</span>&nbsp;<span>{AM_PM}</span>
</div>
</div>
</div>
</div>

View File

@ -1,10 +1,16 @@
.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"] {
& button[class*='n-button'] {
width: 100%;
}
& form[class*="n-form"] {
& form[class*='n-form'] {
margin: 24px 0px;
}
}
@ -12,17 +18,25 @@
& .app-lock-screen__unlock {
.app-lock-screen__unlock__content {
position: relative;
width: 100%;
height: 100%;
@include flexCenter;
flex-direction: column;
& .app-lock-screen__unlock__content-wrapper {
position: fixed;
inset: 0px;
& .app-lock-screen__unlock__content-bg__wrapper {
width: 100%;
height: 100%;
background-color: rgb(16, 16, 20);
}
& .app-lock-screen__unlock__content-bg {
position: absolute;
width: 100%;
height: 100%;
@include flexCenter;
font-size: 220px;
font-size: 16.67rem;
gap: 80px;
z-index: 0;
@ -46,11 +60,12 @@
filter: blur(4px);
}
}
}
& .app-lock-screen__unlock__content-avatar {
margin-top: 5px;
color: #bababa;
font-weight: 500;
font-weight: bolder;
z-index: 1;
}
@ -70,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,33 +1,11 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-13
*
* @workspace ray-template
*
* @remark
*/
/**
*
* ,
*
*/
import './index.scss'
import { NModal } from 'naive-ui'
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({
@ -39,27 +17,22 @@ const AppLockScreen = defineComponent({
return {
lockScreenSwitchRef,
getLockAppScreen,
}
},
render() {
const { getLockAppScreen } = this
return (
<NModal
<RModal
v-model:show={this.lockScreenSwitchRef}
transformOrigin="center"
show
autoFocus={false}
maskClosable={false}
closeOnEsc={false}
preset={!getLockAppScreen() ? 'dialog' : void 0}
preset="dialog"
title="锁定屏幕"
>
<div class="app-lock-screen__content">
{!getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
</div>
</NModal>
<LockScreen />
</RModal>
)
},
})

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
import type { InputInst } from 'naive-ui'
import type { Ref } from 'vue'

View File

@ -1,8 +1,3 @@
/**
*
*
*/
import { NAvatar, NTooltip, NFlex } from 'naive-ui'
interface AvatarOptions {
@ -13,21 +8,9 @@ interface AvatarOptions {
}
export default defineComponent({
name: 'RayLink',
name: 'AppShareLink',
setup() {
const avatarOptions: AvatarOptions[] = [
// {
// key: 'yunhome',
// src: 'https://yunkuangao.me/',
// tooltip: '云之家',
// icon: 'https://r2chevereto.yka.moe/avatar.jpeg',
// },
// {
// key: 'yun-cloud-images',
// src: 'https://yunkuangao.com/',
// tooltip: '云图床',
// icon: 'https://r2chevereto.yka.moe/avatar.jpeg',
// },
{
key: 'ray-js-note',
src: 'https://note.youdao.com/s/ObWEe2BB',

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-14
*
* @workspace ray-template
*
* @remark
*/
/**
*
* naive ui
@ -31,6 +20,7 @@ import {
import { getNaiveLocales } from '@/locales/utils'
import { useSettingGetters } from '@/store'
import { MESSAGE_PROVIDER } from '@/app-config'
export default defineComponent({
name: 'GlobalProvider',
@ -92,7 +82,7 @@ export default defineComponent({
dateLocale={localePackage.dateLocal}
>
<NLoadingBarProvider>
<NMessageProvider>
<NMessageProvider {...MESSAGE_PROVIDER}>
<NDialogProvider>
<NModalProvider>
<NNotificationProvider>

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-07-21
*
* @workspace ray-template
*
* @remark
*/
/**
*
*

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-07-08
*
* @workspace ray-template
*
* @remark
*/
import { get } from 'lodash-es'
import {
setClass,
@ -18,48 +7,59 @@ 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/type'
import type { SettingState } from '@/store/modules/setting/types'
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, {
@ -68,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

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2024-01-01
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -39,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 {
@ -72,7 +53,7 @@ export default defineComponent({
title="发现新版本"
content="当前版本已更新,点击确认加载新版本~"
zIndex={999999999}
dad
draggable
positiveText="确认"
negativeText="取消"
onPositiveClick={logout}

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-10-21
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -37,7 +26,7 @@ export default defineComponent({
const { getWatermarkConfig, getWatermarkSwitch } = this
return getWatermarkSwitch ? (
<NWatermark cross fullscreen {...getWatermarkConfig} />
<NWatermark {...getWatermarkConfig} fullscreen />
) : null
},
})

View File

@ -0,0 +1,147 @@
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
* MessageProver
* Message
*
* @see https://www.naiveui.com/zh-CN/dark/components/message#MessageProvider-Props
*/
export const MESSAGE_PROVIDER: MessageProviderProps = {
max: 5,
closable: false,
}
/**
*
* @description
* spin
*
*
* v4.7.5
*/
export const LAYOUT_CONTENT_SPIN_WHEN_ROUTE_CHANGE = false
/**
*
* @description
* Spin
*/
export const APP_GLOBAL_LOADING = 'loading'
/**
*
*
* title
*/
export const PRE_LOADING_CONFIG: PreloadingConfig = {
title: 'Ray Template',
tagColor: '#ff6700',
titleColor: '#2d8cf0',
}
/**
*
* @description
* key
* key 使 getStorage setStorage key
* APP_CATCH_KEY 使
*
* cache.ts
*
*
*
* @example
* export const APP_CATCH_KEY_PREFIX = 'ray-template:'
*
* key: ray-template:signing
*/
export const APP_CATCH_KEY_PREFIX = ''
/**
*
* @description
* key
*
* :
* - signing: 登陆信息缓存 key
* - localeLanguage: 国际化默认缓存 key
* - token: token key
* - appMenuKey: 菜单缓存 key
* - appPiniaSettingStore: pinia setting store key
* - appPiniaKeepAliveStore: pinia keep alive store key
* - appPiniaMenuStore: pinia menu store key
* - appPiniaSigningStore: pinia signing store key
* - appVersionProvider: 版本信息缓存 key
* - appMenuTagOptions: 标签页菜单列表
* - appLockScreenPasswordKey: 锁屏密码缓存 key
*/
export const APP_CATCH_KEY = {
signing: 'signing',
localeLanguage: 'localeLanguage',
token: 'token',
appMenuKey: 'menuKey',
appPiniaSettingStore: 'piniaSettingStore',
appPiniaKeepAliveStore: 'piniaKeepAliveStore',
appPiniaMenuStore: 'piniaMenuStore',
appPiniaSigningStore: 'piniaSigningStore',
appVersionProvider: 'appVersionProvider',
isAppLockScreen: 'isAppLockScreen',
appGlobalSearchOptions: 'appGlobalSearchOptions',
appMenuTagOptions: 'appMenuTagOptions',
appLockScreenPasswordKey: 'appLockScreenPasswordKey',
} as const
/**
*
* @description
*
* transform
* 例如: transform-fade-bottom
*/
export const CONTENT_TRANSITION_OPTIONS = [
{
label: '无',
value: 'none',
},
{
label: '缩放出现',
value: 'scale',
},
{
label: '缩放消退',
value: 'fade-scale',
},
{
label: '滑入出现',
value: 'fade-slide',
},
{
label: '淡入消退',
value: 'opacity',
},
{
label: '底部消退',
value: 'fade-bottom',
},
]

View File

@ -1,157 +0,0 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-23
*
* @workspace ray-template
*
* @remark
*/
/** 系统配置 */
import type { LayoutSideBarLogo, PreloadingConfig } from '@/types'
import type { AppMenuConfig, AppKeepAlive } from '@/types'
/**
*
*
*
* :
* - setupKeepAlive: 是否启用系统页面缓存, false
* - keepAliveExclude: 排除哪些页面不缓存
* - maxKeepAliveLength: 最大缓存页面数量
*/
export const APP_KEEP_ALIVE: Readonly<AppKeepAlive> = {
setupKeepAlive: true,
keepAliveExclude: [],
maxKeepAliveLength: 5,
}
/**
*
*
* title
*/
export const PRE_LOADING_CONFIG: PreloadingConfig = {
title: 'Ray Template',
tagColor: '#ff6700',
titleColor: '#2d8cf0',
}
/**
*
* icon: LOGO , `RIcon` ()
* title: LOGO
* url: 点击跳转地址, ,
* jumpType: 跳转类型(station: 项目内跳转, outsideStation: 新页面打开)
*
* , LOGO
*/
export const SIDE_BAR_LOGO: LayoutSideBarLogo | undefined = {
icon: 'ray',
title: 'Ray Template',
url: '/dashboard',
jumpType: 'station',
}
/**
*
*
*
* menuCollapsedWidth menuCollapsedMode width
*
* menuCollapsedMode:
* - transform: 边栏将只会移动它的位置而不会改变宽度
* - width: Sider
* menuCollapsedIconSize
* menuCollapsedIndent
* menuAccordion
*/
export const APP_MENU_CONFIG: Readonly<AppMenuConfig> = {
menuCollapsedWidth: 64,
menuCollapsedMode: 'width',
menuCollapsedIconSize: 22,
menuCollapsedIndent: 24,
menuAccordion: false,
}
/**
*
* key
* key 使 getStorage setStorage key
* APP_CATCH_KEY 使
*
* cache.ts
*
*
*
* @example
* export const APP_CATCH_KEY_PREFIX = 'ray-template:'
*
* 'ray-template:signing' // 会自动拼接为
*/
export const APP_CATCH_KEY_PREFIX = ''
/**
*
* key
*
* :
* - signing: 登陆信息缓存 key
* - localeLanguage: 国际化默认缓存 key
* - token: token key
* - appMenuKey: 菜单缓存 key
* - appPiniaSettingStore: pinia setting store key
* - appPiniaKeepAliveStore: pinia keep alive store key
* - appPiniaMenuStore: pinia menu store key
* - appPiniaSigningStore: pinia signing store key
* - appVersionProvider: 版本信息缓存 key
*/
export const APP_CATCH_KEY = {
signing: 'signing',
localeLanguage: 'localeLanguage',
token: 'token',
appMenuKey: 'menuKey',
appPiniaSettingStore: 'piniaSettingStore',
appPiniaKeepAliveStore: 'piniaKeepAliveStore',
appPiniaMenuStore: 'piniaMenuStore',
appPiniaSigningStore: 'piniaSigningStore',
appVersionProvider: 'appVersionProvider',
isAppLockScreen: 'isAppLockScreen',
appGlobalSearchOptions: 'appGlobalSearchOptions',
} as const
/**
*
*
* `transform`
* : `transform-fade-bottom`
*/
export const CONTENT_TRANSITION_OPTIONS = [
{
label: '无',
value: 'none',
},
{
label: '缩放出现',
value: 'scale',
},
{
label: '缩放消退',
value: 'fade-scale',
},
{
label: '滑入出现',
value: 'fade-slide',
},
{
label: '淡入消退',
value: 'opacity',
},
{
label: '底部消退',
value: 'fade-bottom',
},
]

View File

@ -1,50 +1,38 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-19
*
* @workspace ray-template
*
* @remark
*/
/** 系统颜色风格配置入口 */
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
@ -60,17 +48,24 @@ export const APP_THEME: AppTheme = {
* ```
*/
appNaiveUIThemeOverrides: {
dark: {},
light: {},
dark: {
common: {
borderRadius: '4px',
baseColor: 'rgb(18, 18, 18)',
},
},
light: {
common: {
borderRadius: '4px',
baseColor: 'rgb(255, 255, 255)',
},
},
appNaiveUIThemeOverridesCommon: {
dark: {},
light: {},
},
/**
*
* 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

@ -7,7 +7,7 @@
1. 配置、选择主题
2. 点击下载主题
3. 选择 json 类型,然后复制
4. 在 src/echart-themes 包中创建对应的 json 文件,文件名为主题名称
4. 在 src/app-config/echart-themes 包中创建对应的 json 文件,文件名为主题名称
## 注意

View File

@ -7,7 +7,24 @@
"#f7c5a0",
"#d4a4eb",
"#d2f5a6",
"#76f2f2"
"#76f2f2",
"#9b8bba",
"#e098c7",
"#8fd3e8",
"#71669e",
"#cc70af",
"#7cb4cc",
"#7EC4FF",
"#5FCBB0",
"#49C4BF",
"#F0C9CA",
"#34DC90",
"#3295E0",
"#EAB62E",
"#76C3F3",
"#2DC2C0",
"#FCC43F",
"#84CFFF"
],
"backgroundColor": "transparent",
"textStyle": {},
@ -297,6 +314,9 @@
"legend": {
"textStyle": {
"color": "#999999"
},
"pageTextStyle": {
"color": "#999999"
}
},
"tooltip": {

View File

@ -19,7 +19,13 @@
"#c9ab00",
"#7eb00a",
"#6f5553",
"#c14089"
"#c14089",
"#516b91",
"#59c4e6",
"#edafda",
"#93b7e3",
"#a5e7f0",
"#cbb0e3"
],
"backgroundColor": "transparent",
"textStyle": {},

View File

@ -1,6 +1,6 @@
export * from './appConfig'
export * from './designConfig'
export * from './localConfig'
export * from './regexConfig'
export * from './requestConfig'
export * from './routerConfig'
export * from './app-config'
export * from './design-config'
export * from './local-config'
export * from './regex-config'
export * from './request-config'
export * from './router-config'

View File

@ -1,16 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-19
*
* @workspace ray-template
*
* @remark
*/
/** 国际化相关配置 */
import type { TemplateLocale, LocalOptions, DayjsLocalMap } from '@/types'
import type { ValueOf } from '@/types'

View File

@ -0,0 +1,19 @@
/**
*
*
* ,
*/
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

@ -1,22 +0,0 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-12
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
* ,
*/
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)$/,
}

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-02
*
* @workspace ray-template
*
* @remark
*/
import type { AxiosConfig } from '@/types'
/** axios 相关配置 */

View File

@ -1,34 +1,37 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-19
*
* @workspace ray-template
*
* @remark
*/
/** vue-router 相关配置入口 */
import type { LayoutInst } from 'naive-ui'
import type { Ref } from 'vue'
/**
*
* ref
* @description
* shallowRef
*
* , 使(scrollTo)
*
*
* , , dom
* @example
* ```ts
* nextTick().then(() => {
* LAYOUT_CONTENT_REF.value?.scrollTo()
* })
* ```
*/
export const LAYOUT_CONTENT_REF = ref<LayoutInst | null>(null)
export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> =
shallowRef<LayoutInst | null>(null)
/**
*
* @description
* shallowRef
*
*
* 使使 nextTick() dom
* @example
* nextTick().then(() => {
* LAYOUT_SIDER_REF.value?.scrollTo({ top: 0 })
* })
*/
export const LAYOUT_SIDER_REF: Readonly<Ref<LayoutInst | null>> =
shallowRef<LayoutInst | null>(null)
export const SETUP_ROUTER_ACTION = {
/** 是否启用路由切换时顶部加载条 */
@ -39,6 +42,7 @@ export const SETUP_ROUTER_ACTION = {
/**
*
* @description
* ()
*
*
@ -50,7 +54,8 @@ export const WHITE_ROUTES: string[] = ['RLogin', 'ErrorPage', 'RayTemplateDoc']
/**
*
* @description
*
* ,
*/
export const SUPER_ADMIN: (string | number)[] = ['admin']
export const SUPER_ADMIN: (string | number)[] = []

View File

@ -1,14 +1,3 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-10-23
*
* @workspace ray-template
*
* @remark
*/
import { useAxiosInterceptor } from '@/axios/utils/interceptor'
import implement from './provider'

View File

@ -0,0 +1,30 @@
import { axiosCanceler } from '@/axios/utils/interceptor'
import type { AxiosRequestInterceptor, FetchErrorFunction } from '@/axios/types'
/**
*
* @param ins
* @param mode
*
* @description
*
*/
const injectRequestCanceler: AxiosRequestInterceptor = (ins, mode) => {
axiosCanceler.removePendingRequest(ins) // 检查是否存在重复请求, 若存在则取消已发的请求
axiosCanceler.addPendingRequest(ins) // 把当前的请求信息添加到 pendingRequest 表中
}
/**
*
* @param error
* @param mode
*
* @description
*
*/
const requestErrorCanceler: FetchErrorFunction = (error, mode) => {
axiosCanceler.removePendingRequest(error) // 移除请求拦截器
}
export { injectRequestCanceler, requestErrorCanceler }

View File

@ -0,0 +1,42 @@
import { appendRequestHeaders } from '@/axios/utils/append-request-headers'
import { APP_CATCH_KEY } from '@/app-config'
import { getStorage } from '@/utils'
import type {
RequestInterceptorConfig,
AxiosRequestInterceptor,
} from '@/axios/types'
/**
*
* token token key value
*
*
*
* request instance ,
*/
const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => {
const token = getStorage<string | null>(APP_CATCH_KEY.token, 'localStorage')
if (ins.url) {
// TODO: 根据 url 不同是否设置 token
}
return {
key: 'X-TOKEN',
value: token,
}
}
/** 注入请求头信息 */
const injectRequestHeaders: AxiosRequestInterceptor = (ins, mode) => {
appendRequestHeaders(ins, [
requestHeaderToken(ins, mode),
{
key: 'Demo-Header-Key',
value: 'Demo Header Value',
},
])
}
export { injectRequestHeaders }

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