Compare commits

..

No commits in common. "main" and "v4.7.2" have entirely different histories.
main ... v4.7.2

490 changed files with 12688 additions and 22393 deletions

18
.eslintignore Normal file
View File

@ -0,0 +1,18 @@
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

198
.eslintrc.cjs Normal file
View File

@ -0,0 +1,198 @@
/* 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,14 +1,2 @@
# 将换行符设置为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,3 +1,4 @@
name: ray-template documents deploy
on:
@ -14,15 +15,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 22.x
- name: Install Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 22.x
node-version: 18.x
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
version: 8
run_install: false
- name: Install dependencies
@ -35,4 +36,4 @@ jobs:
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: dist
folder: dist/production
folder: dist/production-dist

View File

@ -2,15 +2,16 @@ on:
- push
- pull_request
jobs:
cache-and-install:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version: [22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [true]
node-version: [ 18.x ]
os: [ ubuntu-latest, windows-latest, macos-latest ]
experimental: [ true ]
steps:
- name: Checkout
@ -24,7 +25,7 @@ jobs:
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
version: 8
run_install: false
- name: Get pnpm store directory

1
.gitignore vendored
View File

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

2
.nvmrc
View File

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

View File

@ -8,5 +8,4 @@ yarn.*
.prettierrc.*
visualizer.*
visualizer.html
.env.*
*-lock.yaml
.env.*

View File

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

12
.vscode/settings.json vendored
View File

@ -1,5 +1,4 @@
{
"editor.formatOnSave": true,
"i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
@ -20,32 +19,21 @@
"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,843 +1,4 @@
## 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` 样式问题
# CHANGE LOG
## 4.7.2

View File

@ -48,7 +48,7 @@ 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:` the plug-in design is used to encapsulate the axios library interceptor twice, which makes the interceptor more flexible
- `Axios request:` secondary encapsulation of axios library, support: cancel, jitter, automatic repeat cancellation and other functions
- `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
@ -57,10 +57,12 @@ A `completely free`, `efficient`, `feature complete` and based on vite5. x & ts(
## 👀 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
@ -88,6 +90,9 @@ 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

View File

@ -48,7 +48,7 @@
- `多端适配:`支持 pc, phone, pad
- `文档:`完善的文档
- `Mock 数据:`内置 Mock 数据方案
- `Axios 请求:`采用插件式设计二次封装 axios 库拦截器,让拦截器更加灵活
- `Axios 请求:`二次封装 axios 库,支持:取消、防抖、自动重复取消等功能
- `SVG`内置 svg icon 解决方案
- `Hooks`基于模板特性封装的 hooks 让你更加方便的使用模板一些功能
- `TypeScript`提供完整的类型
@ -57,10 +57,12 @@
## 👀 预览地址
- [点击预览](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/)
## 🔋 更新日志
@ -88,6 +90,9 @@
```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
```
### 拉取依赖

View File

@ -1,4 +1,4 @@
import { prefixCacheKey } from '../../src/utils/app/prefix-cache-key'
import { prefixCacheKey } from '../../src/utils/app/prefixCacheKey'
describe('prefixCacheKey', () => {
it('should return the key with the default prefix', () => {

View File

@ -3,7 +3,6 @@ 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)
@ -15,7 +14,6 @@ describe('callWithAsyncErrorHandling', () => {
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}

View File

@ -3,7 +3,6 @@ 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)
@ -15,7 +14,6 @@ describe('callWithErrorHandling', () => {
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}

View File

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

View File

@ -0,0 +1,39 @@
import { omit } from '../../src/utils/basic'
describe('omit', () => {
it('should omit key from object', () => {
const obj = { a: 1, b: 2, c: 3 }
const result = omit(obj, 'b')
expect(result).toEqual({ a: 1, c: 3 })
})
it('should omit key from the array argument', () => {
const obj = { a: 1, b: 2, c: 3 }
const result = omit(obj, ['a', 'c'])
expect(result).toEqual({ b: 2 })
})
it('should return empty object if no keys are provided', () => {
const obj = { a: 1, b: 2, c: 3 }
const result = omit(obj, Object.keys(obj))
expect(result).toEqual({})
})
it('should return empty object if object is empty', () => {
const obj = {}
const result = omit(obj, 'a', 'b')
expect(result).toEqual({})
})
it('an empty object should be returned if null or undefined is passed', () => {
const result1 = omit(null)
const result2 = omit(void 0)
expect(result1).toEqual({})
expect(result2).toEqual({})
})
})

View File

@ -0,0 +1,25 @@
import { pick } from '../../src/utils/basic'
describe('pick', () => {
it('should pick keys from object', () => {
const obj = { a: 1, b: 2, c: 3 }
const result = pick(obj, 'a', 'c')
expect(result).toEqual({ a: 1, c: 3 })
})
it('should pick keys from the array argument', () => {
const obj = { a: 1, b: 2, c: 3 }
const result = pick(obj, ['a', 'c'])
expect(result).toEqual({ a: 1, c: 3 })
})
it('an empty object should be returned if null or undefined is passed', () => {
const result1 = pick(null)
const result2 = pick(void 0)
expect(result1).toEqual({})
expect(result2).toEqual({})
})
})

View File

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

View File

@ -1,36 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,67 +0,0 @@
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

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

View File

@ -1,5 +1,4 @@
import { useDayjs } from '../../src/hooks/web/useDayjs'
import dayjs from 'dayjs'
describe('useDayjs', () => {
const {
@ -17,8 +16,8 @@ describe('useDayjs', () => {
}
const localSpy = vi.spyOn(m, 'locale')
m.locale('en-US')
m.locale('zh-CN')
m.locale('en')
m.locale('zh-cn')
expect(localSpy).toHaveBeenCalledTimes(2)
})
@ -38,13 +37,13 @@ describe('useDayjs', () => {
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,
)
const _today = new Date().toLocaleDateString()
const _startOfDay = new Date(
new Date().setHours(0, 0, 0, 0),
).toLocaleString()
const _endOfDay = new Date(
new Date().setHours(23, 59, 59, 999),
).toLocaleString()
expect(format(today, formatOptions2)).toBe(_today)
expect(format(startOfDay, formatOptions)).toBe(_startOfDay)

View File

@ -1,54 +0,0 @@
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

@ -1,66 +0,0 @@
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

@ -1,167 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,46 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -12,5 +12,4 @@ const canUseDom = () => {
window.document.createElement
)
}
export default canUseDom

View File

@ -1,19 +1,7 @@
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
import type { Slot } from 'vue'
const createRefElement = (slots?: Record<string, Function>) => {
const wrapper = mount(
defineComponent({

View File

@ -5,11 +5,9 @@
*
* true false
*/
const isBrowser = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
)
const isBrowser = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
)
export default isBrowser

View File

@ -1,27 +0,0 @@
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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
import { renderNode } from '../../src/utils/vue/render-node'
import { renderNode } from '../../src/utils/vue/renderNode'
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()

View File

@ -1,365 +0,0 @@
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,27 +15,6 @@
--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 {
@ -44,9 +23,13 @@
right: 0;
top: 0;
bottom: 0;
background-color: #ffffff;
color: var(--preloading-title-color);
text-align: center;
background-color: var(--global-loading-bg-color);
}
.ray-template--dark #pre-loading-animation {
background-color: #2a3146;
}
#pre-loading-animation .pre-loading-animation__wrapper {
@ -112,18 +95,6 @@
}
}
</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,3 +1,14 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-08-11
*
* @workspace ray-template
*
* @remark
*/
import Mock from 'mockjs'
/**

View File

@ -1,3 +1,14 @@
/**
*
* @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,11 +1,11 @@
{
"name": "ray-template",
"private": false,
"version": "5.2.2",
"version": "4.7.2",
"type": "module",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"pnpm": ">=9.0.0"
"node": "^18.0.0 || >=20.0.0",
"pnpm": ">=8.0.0"
},
"scripts": {
"dev": "vite",
@ -15,8 +15,7 @@
"report": "vite build --mode report",
"prepare": "husky install",
"test": "vitest",
"test:ui": "vitest --ui",
"lint": "vue-tsc --noEmit && eslint --fix && prettier --write \"**/*.{ts,tsx,json,.vue}\""
"test:ui": "vitest --ui"
},
"husky": {
"hooks": {
@ -25,89 +24,92 @@
}
},
"lint-staged": {
"*.{ts,tsx,json}": [
"*.{js,json}": [
"prettier --write"
],
"*.{ts,tsx,vue}": [
"eslint --fix"
"*.ts?(x)": [
"eslint src",
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@logicflow/core": "2.0.10",
"@logicflow/extension": "2.0.14",
"@vueuse/core": "^13.1.0",
"axios": "^1.9.0",
"@vueuse/core": "^10.9.0",
"awesome-qr": "2.1.5-rc.0",
"axios": "^1.6.7",
"clipboard": "^2.0.11",
"crypto-js": "4.2.0",
"crypto-js": "^4.1.1",
"currency.js": "^2.0.4",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"html-to-image": "1.11.13",
"interactjs": "1.10.27",
"jsbarcode": "3.11.6",
"dayjs": "^1.11.10",
"dom-to-image": "2.6.0",
"echarts": "^5.5.0",
"interactjs": "1.10.26",
"lodash-es": "^4.17.21",
"mockjs": "1.1.0",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"naive-ui": "^2.38.1",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"print-js": "^1.6.0",
"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"
"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"
},
"devDependencies": {
"@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",
"@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",
"@vitest/ui": "1.4.0",
"@vue-hooks-plus/resolvers": "1.2.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "2.4.3",
"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",
"happy-dom": "14.3.1",
"husky": "8.0.3",
"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",
"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.6",
"vite-bundle-analyzer": "0.8.1",
"vite-plugin-cdn2": "1.1.0",
"vite-plugin-ejs": "1.7.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "1.8.1",
"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"
"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",
"vite-tsconfig-paths": "4.3.2",
"vitest": "1.4.0",
"vue-tsc": "^1.8.27"
},
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
"main": "index.ts",

14115
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,16 +1,2 @@
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()
}
export * from './valid/validAppRootPath'
export * from './valid/validLocal'

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,28 @@
/**
*
* @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,
window.localStorage,
sessionStorage,
{
mergeDefaults: true,
},

View File

@ -1,22 +1,30 @@
import { NInput, NFormItem, NButton } from 'naive-ui'
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
/** 锁屏界面 */
import { NInput, NForm, 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 { useSettingActions } from '@/store'
import { useTemplateRef } from 'vue'
import { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { setStorage, encrypt } from '@/utils'
import { useSettingGetters, useSettingActions } from '@/store'
import type { InputInst } from 'naive-ui'
import type { FormInst, InputInst } from 'naive-ui'
const LockScreen = defineComponent({
name: 'LockScreen',
setup() {
const [register, { validate }] = useForm()
const inputInstRef = useTemplateRef<InputInst | null>('inputInstRef')
const formInstRef = ref<FormInst | null>(null)
const inputInstRef = ref<InputInst | null>(null)
const { setLockAppScreen } = useAppLockScreen()
const { updateSettingState } = useSettingActions()
@ -25,17 +33,15 @@ const LockScreen = defineComponent({
lockCondition: useCondition(),
})
/** 锁屏 */
const lockScreen = () => {
validate().then(() => {
setLockAppScreen(true)
updateSettingState('lockScreenSwitch', false)
setStorage(
APP_CATCH_KEY.appLockScreenPasswordKey,
encrypt(state.lockCondition.lockPassword),
'localStorage',
)
formInstRef.value?.validate((error) => {
if (!error) {
setLockAppScreen(true)
updateSettingState('lockScreenSwitch', true)
state.lockCondition = useCondition()
state.lockCondition = useCondition()
}
})
}
@ -48,51 +54,41 @@ const LockScreen = defineComponent({
return {
...toRefs(state),
lockScreen,
register,
formInstRef,
inputInstRef,
}
},
render() {
const { register } = this
return (
<div class="app-lock-screen__content">
<div class="app-lock-screen__input">
<AppAvatar
avatarSize={52}
style="pointer-events: none;margin: 24px 0;"
vertical
/>
<RForm
ref="formInstRef"
model={this.lockCondition}
rules={rules}
labelPlacement="left"
onRegister={register}
>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入锁屏密码"
clearable
showPasswordOn="click"
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
this.lockScreen()
}
}}
autofocus
/>
</NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton>
</RForm>
</div>
<div class="app-lock-screen__input">
<AppAvatar vertical align="center" avatarSize={52} />
<NForm
ref="formInstRef"
model={this.lockCondition}
rules={rules}
labelPlacement="left"
>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入锁屏密码"
clearable
showPasswordOn="click"
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
this.lockScreen()
}
}}
/>
</NFormItem>
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
</NButton>
</NForm>
</div>
)
},

View File

@ -1,22 +1,32 @@
import '../../index.scss'
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
import { NInput, NFormItem, NButton, NFlex } from 'naive-ui'
/** 解锁界面 */
import { NInput, NForm, 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 { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { removeStorage, decrypt, getStorage } from '@/utils'
import type { FormInst, InputInst } from 'naive-ui'
export default defineComponent({
name: 'UnlockScreen',
setup() {
const [register, { validate }] = useForm()
const formRef = ref<FormInst | null>(null)
const inputInstRef = ref<InputInst | null>(null)
const { logout } = useSigningActions()
const { updateSettingState } = useSettingActions()
@ -25,13 +35,13 @@ export default defineComponent({
const HH_MM_FORMAT = 'HH:mm'
const AM_PM_FORMAT = 'A'
const YY_MM_DD_FORMAT = 'YYYY-MM-DD'
const YY_MM_DD_FORMAT = 'YY年MM月DD日'
const DDD_FORMAT = 'ddd'
const state = reactive({
lockCondition: useCondition(),
HH_MM: dayjs().format(HH_MM_FORMAT),
AM_PM: dayjs().format(AM_PM_FORMAT),
AM_PM: dayjs().locale('en').format(AM_PM_FORMAT),
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
DDD: dayjs().format(DDD_FORMAT),
})
@ -44,55 +54,30 @@ 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: toSigningFn,
negativeText: '取消',
onPositiveClick: () => {
logout()
setTimeout(() => {
updateSettingState('lockScreenSwitch', false)
})
},
})
}
/** 解锁 */
const unlockScreen = () => {
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) {
formRef.value?.validate((error) => {
if (!error) {
setLockAppScreen(false)
updateSettingState('lockScreenSwitch', false)
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
state.lockCondition = useCondition()
} else {
window.$message.warning('密码错误,请重新输入')
}
})
}
@ -106,84 +91,71 @@ 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, register } = this
const { unlockScreen, backToSigning } = 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'
: '',
]}
>
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
</div>
<div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content-wrapper">
<div
class={[
'app-lock-screen__unlock__content-bg__wrapper',
'app-lock-screen__unlock__content-bg',
isTabletOrSmaller
? 'app-lock-screen__unlock__content-bg--smaller'
: '',
]}
>
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
</div>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar
avatarSize={52}
style="pointer-events: none;"
vertical
/>
</div>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar vertical align="center" avatarSize={52} />
</div>
<div class="app-lock-screen__unlock__content-input">
<NForm ref="formRef" model={this.lockCondition} rules={rules}>
<NFormItem path="lockPassword">
<NInput
ref="inputInstRef"
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
maxlength={12}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
}
}}
/>
</NFormItem>
<NFlex justify="space-between">
<NButton type="primary" text onClick={backToSigning.bind(this)}>
</NButton>
<NButton type="primary" text onClick={unlockScreen.bind(this)}>
</NButton>
</NFlex>
</NForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-date">
{HH_MM}&nbsp;<span>{AM_PM}</span>
</div>
<div class="app-lock-screen__unlock__content-input">
<RForm
onRegister={register}
model={this.lockCondition}
rules={rules}
>
<NFormItem path="lockPassword">
<NInput
autofocus
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
}
}}
/>
</NFormItem>
<NFlex justify="space-between">
<NButton
type="primary"
text
onClick={backToSigning.bind(this)}
>
</NButton>
<NButton
type="primary"
text
onClick={unlockScreen.bind(this)}
>
</NButton>
</NFlex>
</RForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>&nbsp;<span>{AM_PM}</span>
</div>
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>
</div>
</div>
</div>

View File

@ -1,10 +1,4 @@
.app-lock-screen__content {
&.app-lock-screen__content--full {
width: 100%;
height: var(--html-height);
@include flexCenter;
}
& .app-lock-screen__input {
& button[class*='n-button'] {
width: 100%;
@ -36,7 +30,7 @@
width: 100%;
height: 100%;
@include flexCenter;
font-size: 16.67rem;
font-size: 320px;
gap: 80px;
z-index: 0;
@ -85,23 +79,9 @@
& .current-year,
& .current-date span {
font-size: 1.875rem;
line-height: 2.25rem;
font-size: 1.5rem;
}
}
}
}
}
.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,11 +1,33 @@
import { RModal } from '@/components'
import LockScreen from './components/LockScreen'
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-13
*
* @workspace ray-template
*
* @remark
*/
/**
*
* ,
*
*/
import './index.scss'
import { NModal } from 'naive-ui'
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({
@ -17,22 +39,27 @@ const AppLockScreen = defineComponent({
return {
lockScreenSwitchRef,
getLockAppScreen,
}
},
render() {
const { getLockAppScreen } = this
return (
<RModal
<NModal
v-model:show={this.lockScreenSwitchRef}
transformOrigin="center"
show
autoFocus={false}
maskClosable={false}
closeOnEsc={false}
preset="dialog"
preset={!getLockAppScreen() ? 'dialog' : void 0}
title="锁定屏幕"
>
<LockScreen />
</RModal>
<div class="app-lock-screen__content">
{!getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
</div>
</NModal>
)
},
})

View File

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

View File

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

View File

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

View File

@ -1,3 +1,14 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2024-01-01
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -28,10 +39,18 @@ export default defineComponent({
if (version !== cacheVersion) {
modalShow.value = true
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
setStorage<string>(
APP_CATCH_KEY.appVersionProvider,
version,
'localStorage',
)
}
} else {
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
setStorage<string>(
APP_CATCH_KEY.appVersionProvider,
version,
'localStorage',
)
}
return {
@ -53,7 +72,7 @@ export default defineComponent({
title="发现新版本"
content="当前版本已更新,点击确认加载新版本~"
zIndex={999999999}
draggable
dad
positiveText="确认"
negativeText="取消"
onPositiveClick={logout}

View File

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

View File

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

157
src/app-config/appConfig.ts Normal file
View File

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

View File

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

View File

@ -1,3 +1,16 @@
/**
*
* @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

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

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

View File

@ -1,37 +1,34 @@
/**
*
* @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'
/**
*
* @description
* shallowRef
* ref
*
* , 使(scrollTo)
*
*
* , , dom
* @example
* ```ts
* nextTick().then(() => {
* LAYOUT_CONTENT_REF.value?.scrollTo()
* })
* ```
*/
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 LAYOUT_CONTENT_REF = ref<LayoutInst | null>(null)
export const SETUP_ROUTER_ACTION = {
/** 是否启用路由切换时顶部加载条 */
@ -42,7 +39,6 @@ export const SETUP_ROUTER_ACTION = {
/**
*
* @description
* ()
*
*
@ -54,8 +50,7 @@ export const WHITE_ROUTES: string[] = ['RLogin', 'ErrorPage', 'RayTemplateDoc']
/**
*
* @description
*
* ,
*/
export const SUPER_ADMIN: (string | number)[] = []
export const SUPER_ADMIN: (string | number)[] = ['admin']

View File

@ -1,30 +0,0 @@
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

@ -1,42 +0,0 @@
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 }

View File

@ -1,28 +0,0 @@
/**
*
*
* ,
* ,
*
* , 便
*
* injectRequestCanceler requestErrorCanceler axios request interceptor
*/
import { injectRequestCanceler, requestErrorCanceler } from './plugins/cancel'
import { injectRequestHeaders } from './plugins/request-headers'
/**
*
*
*
*/
export default {
// 请求正常
implementRequestInterceptorArray: [
injectRequestHeaders,
injectRequestCanceler,
],
// 请求错误
implementRequestInterceptorErrorArray: [requestErrorCanceler],
}

View File

@ -1,32 +0,0 @@
import { axiosCanceler } from '@/axios/utils/interceptor'
import type {
AxiosResponseInterceptor,
FetchErrorFunction,
} from '@/axios/types'
/**
*
* @param ins
* @param mode
*
* @description
*
*/
const injectResponseCanceler: AxiosResponseInterceptor = (ins, mode) => {
axiosCanceler.removePendingRequest(ins)
}
/**
*
* @param error
* @param mode
*
* @description
*
*/
const responseErrorCanceler: FetchErrorFunction = (error, mode) => {
axiosCanceler.removePendingRequest(error)
}
export { injectResponseCanceler, responseErrorCanceler }

View File

@ -1,24 +0,0 @@
/**
*
*
* ,
* ,
*
* , 便
*
* injectResponseCanceler responseErrorCanceler axios response interceptor
*/
import { injectResponseCanceler, responseErrorCanceler } from './plugins/cancel'
/**
*
*
*
*/
export default {
// 响应正常
implementResponseInterceptorArray: [injectResponseCanceler],
// 响应错误
implementResponseInterceptorErrorArray: [responseErrorCanceler],
}

View File

@ -1,3 +1,14 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-07-11
*
* @workspace ray-template
*
* @remark
*/
/**
*
* vue-hook-plus axios
@ -40,14 +51,14 @@ function useRequest<
fetchOptions: AppRawRequestConfig<Response>,
option?: UseRequestOptions<Response, HookPlusParams, HookPlusPlugin>,
) {
const fn = () => {
const fc = () => {
const cb = request<Response>(fetchOptions)
return cb
}
const hooks = useHookPlusRequest<Response, HookPlusParams>(
fn,
fc,
Object.assign({}, option),
)

View File

@ -1,3 +1,14 @@
/**
*
* @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,105 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-06
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
* ,
* ,
*
* , 便
*
* injectRequestCanceler requestErrorCanceler axios request interceptor
*/
import { axiosCanceler } from '@/axios/utils/interceptor'
import { appendRequestHeaders } from '@/axios/utils/axiosCopilot'
import { APP_CATCH_KEY } from '@/app-config'
import { getStorage } from '@/utils'
import type {
RequestInterceptorConfig,
FetchFunction,
FetchErrorFunction,
} from '@/axios/types'
import type { Recordable } from '@/types'
/**
*
* token token key value
*
*
*
* request instance ,
*/
const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => {
const token = getStorage<string>(APP_CATCH_KEY.token)
if (ins.url) {
// TODO: 根据 url 不同是否设置 token
}
return {
key: 'X-TOKEN',
value: token,
}
}
/** 注入请求头信息 */
const injectRequestHeaders: FetchFunction = (ins, mode) => {
appendRequestHeaders(ins, [
requestHeaderToken(ins, mode),
{
key: 'Demo-Header-Key',
value: 'Demo Header Value',
},
])
}
/**
*
* @param ins
* @param mode
*
* @description
*
*/
const injectRequestCanceler: FetchFunction = (ins, mode) => {
axiosCanceler.removePendingRequest(ins) // 检查是否存在重复请求, 若存在则取消已发的请求
axiosCanceler.addPendingRequest(ins) // 把当前的请求信息添加到 pendingRequest 表中
}
/**
*
* @param error
* @param mode
*
* @description
*
*/
const requestErrorCanceler: FetchErrorFunction = (error, mode) => {
axiosCanceler.removePendingRequest(error) // 移除请求拦截器
}
/**
*
*
*
*/
export default {
// 请求正常
implementRequestInterceptorArray: [
injectRequestHeaders,
injectRequestCanceler,
],
// 请求错误
implementRequestInterceptorErrorArray: [requestErrorCanceler],
}

View File

@ -1,3 +1,14 @@
/**
*
* @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,66 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-06
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
* ,
* ,
*
* , 便
*
* injectResponseCanceler responseErrorCanceler axios response interceptor
*/
import { axiosCanceler } from '@/axios/utils/interceptor'
import type {
ResponseInterceptorConfig,
FetchFunction,
FetchErrorFunction,
} from '@/axios/types'
import type { Recordable } from '@/types'
/**
*
* @param ins
* @param mode
*
* @description
*
*/
const injectResponseCanceler: FetchFunction = (ins, mode) => {
axiosCanceler.removePendingRequest(ins)
}
/**
*
* @param error
* @param mode
*
* @description
*
*/
const responseErrorCanceler: FetchErrorFunction = (error, mode) => {
axiosCanceler.removePendingRequest(error)
}
/**
*
*
*
*/
export default {
// 响应正常
implementResponseInterceptorArray: [injectResponseCanceler],
// 响应错误
implementResponseInterceptorErrorArray: [responseErrorCanceler],
}

View File

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

View File

@ -7,8 +7,6 @@ import type {
Axios,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig,
AxiosRequestHeaders,
} from 'axios'
import type { AnyFC } from '@/types'
@ -26,45 +24,12 @@ export interface RequestHeaderOptions {
}
export interface CancelConfig {
/**
*
* @description
*
*/
cancel?: boolean
}
export interface AppRawRequestConfig<T = any> extends AxiosRequestConfig<T> {
/**
*
* @description
*
*/
cancelConfig?: CancelConfig
/**
*
* @description
*
*/
__CANCELER_TAG_RAY_TEMPLATE__?: '__CANCELER_TAG_RAY_TEMPLATE__'
/**
*
* @description
*
*
*
* @default true
*/
responseErrorMessage?: boolean
/**
*
* @description
*
*
*
* @default false
*/
responseSuccessMessage?: boolean
}
export interface CancelerParams<T = any, D = any>
@ -145,8 +110,18 @@ export interface ErrorImplementQueue {
implementResponseInterceptorErrorArray: AnyFC[]
}
export type FetchFunction = <T = any, K = any>(
ins: RequestInterceptorConfig<T> & ResponseInterceptorConfig<T, K>,
mode: string,
) => void
export type FetchType = 'ok' | 'error'
export type FetchErrorFunction<T = any, D = any> = (
error: AxiosError<T, D>,
mode: string,
) => void
export interface AxiosFetchInstance {
requestInstance: RequestInterceptorConfig | null
responseInstance: ResponseInterceptorConfig | null
@ -156,54 +131,3 @@ export interface AxiosFetchError<T = any> {
requestError: T | null
responseError: T | null
}
interface AxiosResponseConfigInterceptor<T = any>
extends AppRawRequestConfig<T> {
headers: AxiosRequestHeaders
}
type AxiosResponseOmit<T = any, D = any> = Omit<AxiosResponse<T, D>, 'config'>
type AxiosResponseInterceptorIns<T = any, D = any> = AxiosResponseOmit<T, D> & {
config: AxiosResponseConfigInterceptor<T>
}
/**
*
* @param ins current response instance
* @param mode current environment mode
*
* @description
* axios response interceptor.
*/
export type AxiosResponseInterceptor<T = any, K = any> = (
ins: AxiosResponseInterceptorIns<T, K>,
mode: string,
) => void
/**
*
* @param ins current request instance
* @param mode current environment mode
*
* @description
* axios request interceptor.
*/
export type AxiosRequestInterceptor<T = any> = (
ins: RequestInterceptorConfig<T>,
mode: string,
) => void
/**
*
* @param error current request error instance
* @param mode current environment mode
*
* @description
* axios request and response error interceptor.
* if you need create plugin with error interceptor, you can use this function type.
*/
export type FetchErrorFunction<T = any, D = any> = (
error: AxiosError<T, D>,
mode: string,
) => void

View File

@ -1,3 +1,14 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-02-27
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
@ -7,17 +18,6 @@
import type { AppRawRequestConfig, CancelerParams } from '@/axios/types'
/**
*
* @class RequestCanceler
*
* @description
* signal
* generateRequestKey key
*
* cancelConfig.cancel true
* __CANCELER_TAG_RAY_TEMPLATE__
*/
export default class RequestCanceler {
private pendingRequest: Map<string, AbortController>
@ -25,19 +25,7 @@ export default class RequestCanceler {
this.pendingRequest = new Map<string, AbortController>()
}
/**
*
* @param config config
*
* @description
* signal
*
* cancelConfig false signal
* cancelConfig true signal
*
* @example
* const bool = isAppending(config) // true or false
*/
/** 是否需要加入取消请求表中 */
private isAppending(config: AppRawRequestConfig | CancelerParams) {
return config.cancelConfig?.cancel ?? true
}
@ -45,12 +33,9 @@ export default class RequestCanceler {
/**
*
* @param config config
* @returns key
*
* @description
* key
*
* @example
* const key = generateRequestKey(config) // string
* @remark config request key
*/
private generateRequestKey(config: AppRawRequestConfig | CancelerParams) {
const { method, url } = config
@ -67,15 +52,7 @@ export default class RequestCanceler {
*
* @param config axios request config
*
* @description
* pendingRequest map
* signal
*
* cancelConfig.cancel false
* __CANCELER_TAG_RAY_TEMPLATE__
*
* @example
* addPendingRequest(config)
* @remark signal ,
*/
addPendingRequest(config: AppRawRequestConfig | CancelerParams) {
if (this.isAppending(config)) {
@ -100,11 +77,7 @@ export default class RequestCanceler {
*
* @param config axios request config
*
* @description
* pendingRequest map
*
* @example
* removePendingRequest(config)
* @remark , map generateRequestKey value
*/
removePendingRequest(config: AppRawRequestConfig | CancelerParams) {
const requestKey = this.generateRequestKey(config)
@ -115,17 +88,7 @@ export default class RequestCanceler {
}
}
/**
*
* @description
* pendingRequest map
*
*
* cancelConfig.cancel false
*
* @example
* cancelAllRequest()
*/
/** 取消所有请求 */
cancelAllRequest() {
this.pendingRequest.forEach((curr) => {
curr.abort()

View File

@ -1,31 +0,0 @@
import type { RawAxiosRequestHeaders, AxiosRequestConfig } from 'axios'
import type { RequestHeaderOptions } from '../types'
/**
*
* @param instance axios instance
* @param options axios headers options
*
* @description
* axios
*
* @example
* appendRequestHeaders(inst, [
* {
* key: 'Demo-Header-Key',
* value: 'Demo Header Value',
* }
* ])
*/
export const appendRequestHeaders = <T = unknown>(
instance: AxiosRequestConfig<T>,
options: RequestHeaderOptions[],
) => {
if (instance) {
const requestHeaders = instance.headers as RawAxiosRequestHeaders
options.forEach((curr) => {
requestHeaders[curr.key] = curr.value
})
}
}

View File

@ -1,3 +1,16 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-02
*
* @workspace ray-template
*
* @remark
*/
/** axios 拦截器工具 */
import type { RawAxiosRequestHeaders, AxiosRequestConfig } from 'axios'
import type { RequestHeaderOptions } from '../types'

View File

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

View File

@ -1,12 +1,11 @@
import RChart from './src'
import chartProps from './src/props'
import useChart from './src/hooks/useChart'
import type { ExtractPublicPropTypes } from 'vue'
import type * as RChartType from './src/types'
import type { UseChartReturn } from './src/hooks/useChart'
export type ChartProps = ExtractPublicPropTypes<typeof chartProps>
export type { RChartType, UseChartReturn }
export type { RChartType }
export { RChart, chartProps, useChart }
export { RChart, chartProps }

View File

@ -26,17 +26,11 @@ import { NCard } from 'naive-ui'
import props from './props'
import { throttle } from 'lodash-es'
import { completeSize, downloadBase64File, call, renderNode } from '@/utils'
import { getCustomEchartTheme, loadingOptions, setEchartOptions } from './utils'
import { setupChartTheme } from './utils'
import { APP_THEME } from '@/app-config'
import {
useResizeObserver,
useIntersectionObserver,
watchThrottled,
} from '@vueuse/core'
import { useResizeObserver, useIntersectionObserver } from '@vueuse/core'
import { RMoreDropdown } from '@/components'
import { useSettingGetters } from '@/store'
import { useTemplateRef } from 'vue'
import { USE_CHART_PROVIDER_KEY } from './config'
import type { WatchStopHandle } from 'vue'
import type { AnyFC } from '@/types'
@ -48,8 +42,15 @@ import type {
import type { ECharts, EChartsCoreOption } from 'echarts/core'
import type { DropdownProps, DropdownOption } from 'naive-ui'
// setOption 默认配置项
const defaultChartOptions = {
notMerge: false,
lazyUpdate: true,
silent: false,
replaceMerge: [],
}
// 获取 chart 主题
const echartThemes = getCustomEchartTheme()
const echartThemes = setupChartTheme()
// download 下载功能 key
const __CHART_DOWN_LOAD_CHART__ = '__R_CHART_DOWN_LOAD_CHART__'
@ -69,39 +70,28 @@ echartThemes.forEach((curr) => {
*
* @example
* <template>
* <RChart :options="options" @register="register" />
* <RChart :options="options" />
* </template>
*
* <script setup>
* import { RChart } from '@/components'
*
* import { useChart } from 'vue'
* import { ref } from 'vue'
*
* const [register, { ...Hooks }] = useChart()
* const options = ref({ ... })
* </script>
*/
export default defineComponent({
name: 'RChart',
props,
setup(props, { expose }) {
const { getAppTheme } = useSettingGetters()
// echart 容器实例
const rayChartRef = useTemplateRef<HTMLElement>('rayChartRef')
// echart 父容器实例
const rayChartWrapperRef = useTemplateRef<HTMLElement>('rayChartWrapperRef')
// echart 实例
const echartInstanceRef = shallowRef<ECharts>()
// resize 防抖方法实例
let resizeThrottleReturn: DebouncedFunc<AnyFC> | null
// resize observer 实例
let resizeObserverReturn: UseResizeObserverReturn | null
// 当前配置主题
const { echartTheme } = APP_THEME
// watch props 回调
let watchThrottledCallback: WatchStopHandle | null
// 下拉框配置项
const rayChartRef = ref<HTMLElement>() // echart 容器实例
const rayChartWrapperRef = ref<HTMLElement>() // echart 父容器实例
const echartInstanceRef = ref<ECharts>() // echart 实例
let resizeThrottleReturn: DebouncedFunc<AnyFC> | null // resize 防抖方法实例
let resizeObserverReturn: UseResizeObserverReturn | null // resize observer 实例
const { echartTheme } = APP_THEME // 当前配置主题
let watchCallback: WatchStopHandle | null // watch props 回调
let echartInst: ECharts | null // 无代理响应式代理缓存 echart inst
const moreDropDownOptions = computed<DropdownProps['options']>(() => [
{
label: '下载图片',
@ -110,30 +100,22 @@ export default defineComponent({
echartInstanceRef.value && echartInstanceRef.value.getDom()
),
},
])
]) // 下拉框配置项
const cssVarsRef = computed(() => {
return {
'--ray-chart-width': completeSize(props.width),
'--ray-chart-height': completeSize(props.height),
}
})
// 目标是否可见
const targetIsVisible = ref(false)
// intersectionObserver 实例
let intersectionObserverReturn: UseIntersectionObserverReturn | null
// 缓存一些配置信息
const __catch = {
aria: props.showAria,
}
const chartProvideOptions = inject(USE_CHART_PROVIDER_KEY, {})
const targetIsVisible = ref(false) // 目标是否可见
let intersectionObserverReturn: UseIntersectionObserverReturn | null // intersectionObserver 实例
/**
*
* @description
* echart
* `echart` , ,
*
* echart
*
* `echart`
*
*/
const registerChartCore = async () => {
use([
@ -166,29 +148,19 @@ export default defineComponent({
/**
*
* @description
* chart
* chart
*
* theme autoChangeTheme
* theme default chart
* theme autoChangeTheme
* theme default chart
*
* Boolean(theme) false echartTheme
* echartTheme 使
* Boolean(theme) false echartTheme
* echartTheme 使
*/
const updateChartTheme = () => {
const { theme: providerTheme } = chartProvideOptions || {}
if (echartInstanceRef.value) {
if (echartInst?.getDom()) {
destroyChart()
}
// 如果配置了全局配置主题,则忽略后面所有逻辑
if (providerTheme) {
renderChart(providerTheme)
return
}
if (props.theme === 'default') {
props.autoChangeTheme ? renderChart('dark') : renderChart('')
@ -219,72 +191,71 @@ export default defineComponent({
*/
const combineChartOptions = (ops: EChartsCoreOption) => {
let options = unref(ops)
const assign = (opts: object) => Object.assign({}, options, opts)
// 拦截 aria 配置项
options = assign({
aria: {
enabled: props.showAria,
decal: {
show: props.showAria,
if (props.showAria) {
options = assign({
aria: {
enabled: true,
decal: {
show: true,
},
},
},
})
})
}
return options
}
/**
*
* @description
* echart
* `echart`
*
*
* 使, `legend`
*/
const renderChart = (theme: string = echartTheme) => {
// 获取 dom 容器
/** 获取 dom 容器 */
const element = rayChartRef.value as HTMLElement
// 获取配置项
/** 获取配置项 */
const options = combineChartOptions(props.options)
// 获取 dom 容器实际宽高
/** 获取 dom 容器实际宽高 */
const { height, width } = element.getBoundingClientRect()
const { onSuccess, onError } = props
try {
// 注册 chart
echartInstanceRef.value = init(element, theme, {
// 如果款度为 0则以 200px 填充
/** 注册 chart */
echartInst = init(element, theme, {
/** 如果款度为 0, 则以 200px 填充 */
width: width === 0 ? 200 : void 0,
// 如果高度为 0则以 200px 填充
/** 如果高度为 0, 则以 200px 填充 */
height: height === 0 ? 200 : void 0,
})
echartInstanceRef.value = echartInst
// 渲染成功回调
if (onSuccess) {
call(onSuccess, echartInstanceRef.value)
call(onSuccess, echartInst)
}
// 是否强制下一队列渲染图表
if (props.nextTick) {
echartInstanceRef.value.setOption({})
echartInst.setOption({})
nextTick(() => {
options && echartInstanceRef.value?.setOption(options)
options && echartInst?.setOption(options)
})
} else {
options && echartInstanceRef.value?.setOption(options)
options && echartInst?.setOption(options)
}
} catch (e) {
// 渲染失败回调
/** 渲染失败回调 */
if (onError) {
call(onError)
}
throw new Error(`[RChart render error]: ${e}`)
} finally {
const { onFinally } = props
if (onFinally) {
call(onFinally)
}
console.error('[RChart]: render error: ', e)
}
}
@ -294,29 +265,26 @@ export default defineComponent({
* chart
* true, false
*/
const isDisposed = () => {
return !!echartInstanceRef.value?.isDisposed()
}
const isDispose = () => !(echartInst && echartInst.getDom())
/**
*
* @description
* chart
* chart ,
*/
const destroyChart = () => {
if (!isDisposed()) {
echartInstanceRef.value?.dispose()
if (!isDispose()) {
echartInst!.clear()
echartInst!.dispose()
echartInstanceRef.value = void 0
echartInst = null
}
}
/**
*
* @description
* echarts
*/
/** 重置 echarts 尺寸 */
const resizeChart = () => {
if (echartInstanceRef.value) {
echartInstanceRef.value.resize()
if (echartInst) {
echartInst.resize()
}
}
@ -325,16 +293,15 @@ export default defineComponent({
* @param key moreDropDownOptions key
* @param option moreDropDownOptions current click option
*
* @description
* card
*
* card
*
*/
const dropdownSelect = (key: string | number, option: DropdownOption) => {
if (key === __CHART_DOWN_LOAD_CHART__ && !isDisposed()) {
if (key === __CHART_DOWN_LOAD_CHART__ && !isDispose()) {
const { filename, ...args } = props.downloadOptions
downloadBase64File(
echartInstanceRef.value!.getDataURL(args),
echartInst!.getDataURL(args),
filename ?? `${new Date().getTime()}`,
)
}
@ -346,16 +313,11 @@ export default defineComponent({
}
}
/**
*
* @description
* chart
*/
const mount = () => {
// 注册事件
if (props.autoResize) {
if (!resizeThrottleReturn) {
resizeThrottleReturn = throttle(resizeChart, 500)
resizeThrottleReturn = throttle(resizeChart, props.throttleWait)
}
/**
@ -372,7 +334,7 @@ export default defineComponent({
}
// 避免重复渲染
if (echartInstanceRef.value?.getDom()) {
if (echartInst?.getDom()) {
return
}
@ -381,25 +343,23 @@ export default defineComponent({
return
}
// 根据主题渲染 chart
// 渲染 chart
updateChartTheme()
// 初始化完成后移除 intersectionObserver 监听
intersectionObserverReturn?.stop()
// 注册 register用于 useChart hook
const { onRegister } = props
if (onRegister && echartInstanceRef.value) {
call(onRegister, echartInstanceRef.value, mount, unmount)
}
}
/**
*
* @description
* chart
*/
if (props.intersectionObserver) {
intersectionObserverReturn = useIntersectionObserver(
props.intersectionObserverTarget || rayChartWrapperRef,
([entry]) => {
targetIsVisible.value = entry.isIntersecting
},
props.intersectionOptions,
)
}
const unmount = () => {
// 卸载 echarts
destroyChart()
@ -414,17 +374,15 @@ export default defineComponent({
resizeObserverReturn = null
}
// 监听全局主题变化,然后重新渲染对应主题 echarts
/** 监听全局主题变化, 然后重新渲染对应主题 echarts */
watch(
() => getAppTheme.value,
() => {
/**
*
* @description
* Q: 为什么需要重新卸载再渲染
* A: 因为 echarts
* setTheme ECharts 访
*
* Q: 为什么需要重新卸载再渲染
* A: 因为 echarts
* A: 虽然原型上有 setTheme , ECharts 访
*/
if (props.autoChangeTheme) {
destroyChart()
@ -432,65 +390,61 @@ export default defineComponent({
}
},
)
/**
*
*
*
*
*/
watch(
() => props.showAria,
() => {
destroyChart()
updateChartTheme()
},
)
watchEffect(() => {
// 是否启用了可视区域监听
if (props.intersectionObserver) {
intersectionObserverReturn = useIntersectionObserver(
props.intersectionObserverTarget || rayChartWrapperRef,
([entry]) => {
targetIsVisible.value = entry.isIntersecting
},
props.intersectionOptions,
)
}
// 监听 options 变化
/** 监听 options 变化 */
if (props.watchOptions) {
watchThrottledCallback = watchThrottled(
watchCallback = watch(
() => props.options,
(ndata) => {
// 重新组合 options
const options = combineChartOptions(ndata)
const setOpt = Object.assign(
{},
setEchartOptions(),
props.setChartOptions,
defaultChartOptions,
)
// 如果 options 发生变动更新 echarts
echartInstanceRef.value?.setOption(options, setOpt)
echartInst?.setOption(options, setOpt)
},
{
// 深度监听 options
deep: true,
throttle: props.watchOptionsThrottleWait,
},
)
} else {
watchThrottledCallback?.()
watchCallback?.()
}
// 监听 loading 变化
props.loading
? echartInstanceRef.value?.showLoading(
loadingOptions(props.loadingOptions),
)
: echartInstanceRef.value?.hideLoading()
// 贴花是否启用
if (props.showAria !== __catch.aria && echartInstanceRef.value) {
echartInstanceRef.value.setOption(combineChartOptions(props.options))
__catch.aria = props.showAria
}
? echartInst?.showLoading(props.loadingOptions)
: echartInst?.hideLoading()
// 当前图表容器是否处于可见状态,如果可见则渲染图表
if (targetIsVisible.value && !isDisposed()) {
if (targetIsVisible.value) {
mount()
}
})
expose()
expose({
echart: echartInstanceRef,
dispose: unmount,
render: mount,
isDispose,
})
onBeforeMount(async () => {
// 注册 echarts 组件与渲染器
@ -502,7 +456,7 @@ export default defineComponent({
})
onBeforeUnmount(() => {
unmount()
watchThrottledCallback?.()
watchCallback?.()
})
return {
@ -532,7 +486,6 @@ export default defineComponent({
style={[this.cssVarsRef]}
contentStyle={contentStyle}
bordered={bordered}
embedded
>
{{
default: renderNode(
@ -546,7 +499,7 @@ export default defineComponent({
<RMoreDropdown
iconSize={18}
cursor="pointer"
options={dropdownOptions || moreDropDownOptions}
options={dropdownOptions ?? moreDropDownOptions}
trigger="click"
onSelect={dropdownSelect.bind(this)}
placement="bottom-end"

View File

@ -1,5 +1,3 @@
import { loadingOptions, setEchartOptions } from './utils'
import type * as echarts from 'echarts/core' // echarts 核心模块
import type { PropType, VNode } from 'vue'
import type { MaybeArray } from '@/types'
@ -17,7 +15,8 @@ import type {
RChartDownloadOptions,
} from './types'
import type { CardProps, DropdownProps, DropdownOption } from 'naive-ui'
import type { VoidFC } from '@/types'
import { loadingOptions } from './utils'
const props = {
/**
@ -161,7 +160,7 @@ const props = {
* @default 100%
*/
width: {
type: [String, Number] as PropType<string | number>,
type: String,
default: '100%',
},
/**
@ -174,7 +173,7 @@ const props = {
* @default 100%
*/
height: {
type: [String, Number] as PropType<string | number>,
type: String,
default: '100%',
},
/**
@ -192,8 +191,7 @@ const props = {
*
* @description
* chart
* aria enabled decal.show
* options
* options aria
*
* @default false
*/
@ -234,15 +232,6 @@ const props = {
type: [Function, Array] as PropType<MaybeArray<() => void>>,
default: null,
},
/**
*
* @description
* chart
*/
onFinally: {
type: [Function, Array] as PropType<MaybeArray<() => void>>,
default: null,
},
/**
*
* @description
@ -330,14 +319,11 @@ const props = {
/**
*
* @description
* watchThrottle options
* chart options 使
*
* watchOptions
*
*
* @default 500
*/
watchOptionsThrottleWait: {
throttleWait: {
type: Number,
default: 500,
},
@ -345,7 +331,6 @@ const props = {
*
* @description
*
*
*
* @default true
*/
@ -358,26 +343,17 @@ const props = {
* @description
* setOptions
*
* @default {notMerge:true,lazyUpdate:true,silent:false,replaceMerge:[]}
* @default {notMerge:false,lazyUpdate:true,silent:false,replaceMerge:[]}
*/
setChartOptions: {
type: Object as PropType<SetOptionOpts>,
default: () => setEchartOptions(),
default: () => ({
notMerge: false,
lazyUpdate: true,
silent: false,
replaceMerge: [],
}),
},
/**
*
* @description
* RChart
* useChart register 使便使 hooks
*
* @default null
*/
onRegister: {
type: [Function, Array] as PropType<
MaybeArray<(chartInst: ECharts, render: VoidFC, dispose: VoidFC) => void>
>,
default: null,
},
} as const
}
export default props

View File

@ -0,0 +1,89 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-07-22
*
* @workspace ray-template
*
* @remark
*/
import type {
ChartThemeRawArray,
ChartThemeRawModules,
LoadingOptions,
} from '@/components/RChart/src/types'
/**
*
* @see https://echarts.apache.org/zh/theme-builder.html
*
* @description
*
*
*
*
* 使:
* 1.
* 2.
* 3. json
* 4. echart-themes json
*/
export const setupChartTheme = () => {
// 获取所有主题
const themeRawModules: Record<string, ChartThemeRawModules> =
import.meta.glob('@/echart-themes/**/*.json', {
eager: true,
})
const regex = /\/([^/]+)\.json$/
const rawThemes = Object.keys(themeRawModules).reduce((pre, curr) => {
const name = curr.match(regex)?.[1]
if (name) {
pre.push({
name,
theme: themeRawModules[curr].default,
})
return pre
} else {
throw new Error(`[RChart Theme Error]: name ${curr} is invalid!`)
}
}, [] as ChartThemeRawArray[])
return rawThemes
}
/**
*
* @param options
*
* @description
* chart
*
* @see https://echarts.apache.org/zh/api.html#echartsInstance.showLoading
*
* @example
* const options = loadingOptions({ ...LoadingOptions })
*/
export const loadingOptions = (options?: LoadingOptions) =>
Object.assign(
{},
{
text: 'loading',
color: '#c23531',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, 0.9)',
zlevel: 0,
fontSize: 12,
showSpinner: true,
spinnerRadius: 10,
lineWidth: 5,
fontWeight: 'normal',
fontStyle: 'normal',
fontFamily: 'sans-serif',
},
options,
)

View File

@ -20,8 +20,4 @@
}
}
}
.ray-collapse-grid__suffix--btn {
align-self: var(--r-collapse-grid-action-align);
}
}

View File

@ -0,0 +1,99 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2022-12-27
*
* @workspace ray-template
*
* @remark
*/
/**
*
* <https://www.naiveui.com/zh-CN/dark/components/grid>
*
*
* 使
*
* `NGrid` `NGridItem` , 使使 `NGridItem`
*/
import './index.scss'
import { NCard, NGrid, NGridItem, NFlex } from 'naive-ui'
import { RIcon } from '@/components'
import { call } from '@/utils'
import props from './props'
export default defineComponent({
name: 'RCollapseGrid',
props,
setup(props) {
const modelCollapsed = ref(!props.open)
const collapseClick = () => {
modelCollapsed.value = !modelCollapsed.value
const { onUpdateValue, 'onUpdate:value': _onUpdateValue } = props
if (onUpdateValue) {
call(onUpdateValue, modelCollapsed.value)
}
if (_onUpdateValue) {
call(_onUpdateValue, modelCollapsed.value)
}
}
const CollapseIcon = () => (
<div class="collapse-icon" onClick={collapseClick.bind(this)}>
<span>
{modelCollapsed.value
? props.collapseToggleText[0]
: props.collapseToggleText[1]}
</span>
<RIcon
customClassName={`collapse-icon--arrow ${
modelCollapsed.value ? '' : 'collapse-icon--arrow__expanded'
}`}
name="expanded"
size="14"
/>
</div>
)
return {
modelCollapsed,
collapseClick,
CollapseIcon,
}
},
render() {
return (
<NCard bordered={this.bordered}>
{{
default: () => (
<NGrid
class="ray-collapse-grid"
{...this.$props}
collapsed={this.modelCollapsed}
xGap={this.xGap || 12}
yGap={this.yGap || 18}
collapsedRows={this.collapsedRows}
>
{this.$slots.default?.()}
<NGridItem suffix class="ray-collapse-grid__suffix--btn">
<NFlex justify="end" align="center">
{this.$slots.action?.()}
{this.CollapseIcon()}
</NFlex>
</NGridItem>
</NGrid>
),
}}
</NCard>
)
},
})

View File

@ -1,32 +1,10 @@
import { gridProps } from 'naive-ui'
import type { PropType } from 'vue'
import type { CollapseToggleText, ActionAlignType } from './types'
import type { CollapseToggleText } from './types'
import type { AnyFC, MaybeArray } from '@/types'
const props = {
/**
*
* @description
*
*
* @default 1
*/
actionSpan: {
type: Number,
default: 1,
},
/**
*
* @description
*
*
* @default end
*/
actionAlign: {
type: String as PropType<ActionAlignType>,
default: 'end',
},
open: {
/**
*
@ -59,13 +37,13 @@ const props = {
* `false`
*/
type: Boolean,
default: true,
default: false,
},
onUpdateOpen: {
onUpdateValue: {
type: [Function, Array] as PropType<MaybeArray<(bool: boolean) => void>>,
default: null,
},
'onUpdate:open': {
'onUpdate:value': {
type: [Function, Array] as PropType<MaybeArray<(bool: boolean) => void>>,
default: null,
},

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