mirror of
https://github.com/XiaoDaiGua-Ray/ray-template.git
synced 2025-04-05 19:42:07 +08:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
eb5a6aa9e2 | ||
|
674539edd3 | ||
|
ff0bcb5022 | ||
|
d3d98190a3 | ||
|
0bb707bba0 | ||
|
4bfdbccd88 | ||
|
3b2bba391e | ||
|
7647508935 | ||
|
852d7ca90a | ||
|
2c84e3ce4c | ||
|
83e0c19ba9 |
@ -1,18 +0,0 @@
|
||||
dist/*
|
||||
node_modules/*
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
.gitignore
|
||||
.vscode
|
||||
public
|
||||
yarn.*
|
||||
vite-env.*
|
||||
.prettierrc.*
|
||||
visualizer.*
|
||||
visualizer.html
|
||||
.env.*
|
||||
src/locales/lang
|
||||
.depcheckrc
|
||||
src/app-config/echart-themes/**/*.json
|
||||
*.md
|
||||
src/icons/*.svg
|
316
.eslintrc.cjs
316
.eslintrc.cjs
@ -1,316 +0,0 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
ignorePatterns: ['node_modules/', 'dist/'],
|
||||
extends: [
|
||||
'eslint-config-prettier',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'./unplugin/.eslintrc-auto-import.json',
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint', 'prettier'],
|
||||
globals: {
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly',
|
||||
defineOptions: 'readonly',
|
||||
defineModel: 'readonly',
|
||||
},
|
||||
rules: {
|
||||
'no-undefined': ['error'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
},
|
||||
],
|
||||
'prettier/prettier': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
disallowTypeAnnotations: false,
|
||||
},
|
||||
], // 强制导入类型显示标注 `import type xxx from 'xxx'`
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'accessor-pairs': 2, // 强制同时存在 `get` 与 `set`
|
||||
'constructor-super': 0, // 强制子类构造函数中使用 `super` 调用父类的构造函数
|
||||
'default-case': 2, // `switch` 中强制含有 `default`
|
||||
eqeqeq: [2, 'allow-null'], // 强制使用严格判断 `===`
|
||||
'no-alert': 0, // 禁止使用 `alert`、`confirm`
|
||||
'no-array-constructor': 2, // 禁止使用数组构造器
|
||||
'no-bitwise': 0, // 禁止使用按位运算符
|
||||
'no-caller': 1, // 禁止使用 `arguments.caller`、`arguments.callee`
|
||||
'no-catch-shadow': 2, // 禁止 `catch` 子句参数与外部作用域变量同名
|
||||
'no-class-assign': 2, // 禁止给类赋值
|
||||
'no-cond-assign': 2, // 禁止在条件表达式中使用赋值语句
|
||||
'no-const-assign': 2, // 禁止修改 `const` 声明的变量
|
||||
'no-constant-condition': 2, // 禁止在条件中使用常量表达式 `if(true)`、`if(1)`
|
||||
'no-dupe-keys': 2, // 在创建对象字面量时不允许 `key` 重复
|
||||
'no-dupe-args': 2, // 函数参数不能重复
|
||||
'no-duplicate-case': 2, // `switch` 中的 `case` 标签不能重复
|
||||
'no-eval': 1, // 禁止使用 `eval`
|
||||
'no-ex-assign': 2, // 禁止给 `catch` 语句中的异常参数赋值
|
||||
'no-extend-native': 2, // 禁止扩展 `native` 对象
|
||||
'no-extra-bind': 2, // 禁止不必要的函数绑定
|
||||
'no-extra-boolean-cast': [
|
||||
'error',
|
||||
{
|
||||
enforceForLogicalOperands: true,
|
||||
},
|
||||
], // 禁止不必要的 `bool` 转换
|
||||
'no-extra-parens': 0, // 禁止非必要的括号
|
||||
semi: [
|
||||
'error',
|
||||
'never',
|
||||
{
|
||||
beforeStatementContinuationChars: 'always',
|
||||
},
|
||||
],
|
||||
'no-fallthrough': 1, // 禁止 `switch` 穿透
|
||||
'no-func-assign': 2, // 禁止重复的函数声明
|
||||
'no-implicit-coercion': [
|
||||
'error',
|
||||
{
|
||||
allow: ['!!', '~'],
|
||||
},
|
||||
], // 禁止隐式转换
|
||||
'no-implied-eval': 2, // 禁止使用隐式 `eval`
|
||||
'no-invalid-regexp': 2, // 禁止无效的正则表达式
|
||||
'no-invalid-this': 2, // 禁止无效的 `this`
|
||||
'no-irregular-whitespace': 2, // 禁止含有不合法空格
|
||||
'no-iterator': 2, // 禁止使用 `__iterator__ ` 属性
|
||||
'no-label-var': 2, // `label` 名不能与 `var` 声明的变量名相同
|
||||
'no-labels': 2, // 禁止标签声明
|
||||
'no-lone-blocks': 2, // 禁止不必要的嵌套块
|
||||
'no-multi-spaces': 1, // 禁止使用多余的空格
|
||||
'no-multiple-empty-lines': [
|
||||
'error',
|
||||
{
|
||||
max: 2,
|
||||
},
|
||||
], // 空行最多不能超过 `2` 行
|
||||
'no-new-func': 2, // 禁止使用 `new Function`
|
||||
'no-new-object': 2, // 禁止使用 `new Object`
|
||||
'no-new-require': 2, // 禁止使用 `new require`
|
||||
'no-sparse-arrays': 2, // 禁止稀疏数组
|
||||
'no-trailing-spaces': 1, // 一行结束后面不要有空格
|
||||
'no-unreachable': 2, // 禁止有无法执行的代码
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
enforceForJSX: true,
|
||||
},
|
||||
], // 禁止无用的表达式
|
||||
'no-useless-call': 2, // 禁止不必要的 `call` 和 `apply`
|
||||
'no-var': 'error', // 禁用 `var`
|
||||
'no-with': 2, // 禁用 `with`
|
||||
'use-isnan': 2, // 强制使用 isNaN 判断 NaN
|
||||
'no-multi-assign': 2, // 禁止连续声明变量
|
||||
'prefer-arrow-callback': 2, // 强制使用箭头函数作为回调
|
||||
curly: ['error', 'all'],
|
||||
'vue/multi-word-component-names': [
|
||||
'error',
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
],
|
||||
'vue/no-use-v-if-with-v-for': [
|
||||
'error',
|
||||
{
|
||||
allowUsingIterationVar: false,
|
||||
},
|
||||
],
|
||||
'vue/require-v-for-key': ['error'],
|
||||
'vue/require-valid-default-prop': ['error'],
|
||||
'vue/component-definition-name-casing': ['error', 'PascalCase'],
|
||||
'vue/html-closing-bracket-newline': [
|
||||
'error',
|
||||
{
|
||||
singleline: 'never',
|
||||
multiline: 'always',
|
||||
},
|
||||
],
|
||||
'vue/v-on-event-hyphenation': ['error', 'never'],
|
||||
'vue/component-tags-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['template', 'script', 'style'],
|
||||
},
|
||||
],
|
||||
'vue/no-v-html': ['error'],
|
||||
'vue/no-v-text': ['error'],
|
||||
'vue/component-api-style': [
|
||||
'error',
|
||||
['script-setup', 'composition', 'composition-vue2'],
|
||||
],
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false,
|
||||
},
|
||||
],
|
||||
'vue/no-unused-refs': ['error'],
|
||||
'vue/prop-name-casing': ['error', 'camelCase'],
|
||||
'vue/component-options-name-casing': ['error', 'PascalCase'],
|
||||
'vue/attribute-hyphenation': [
|
||||
'error',
|
||||
'never',
|
||||
{
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'vue/no-restricted-static-attribute': [
|
||||
'error',
|
||||
{
|
||||
key: 'key',
|
||||
message: 'Disallow using key as a custom attribute',
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='deprecated']",
|
||||
message: 'Using deprecated API is not allowed.',
|
||||
},
|
||||
],
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'return',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'function',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['const', 'let', 'var'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: ['const', 'let', 'var'],
|
||||
next: ['const', 'let', 'var'],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: 'directive',
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'directive',
|
||||
next: 'directive',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['case', 'default'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['break'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['import'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'import',
|
||||
next: 'import',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'export',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'export',
|
||||
next: 'export',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['function'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['class'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'for',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'for',
|
||||
next: 'for',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: ['while', 'do', 'switch'],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-object-type': [
|
||||
'error',
|
||||
{
|
||||
allowInterfaces: 'with-single-extends',
|
||||
allowObjectTypes: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
6
.github/workflows/docs-deploy.yaml
vendored
6
.github/workflows/docs-deploy.yaml
vendored
@ -14,15 +14,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 20.x
|
||||
- name: Install Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 22.x
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
|
4
.github/workflows/push-build.yaml
vendored
4
.github/workflows/push-build.yaml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [22.x]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
experimental: [true]
|
||||
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"i18n-ally.localesPaths": ["src/locales/lang"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
@ -45,6 +46,5 @@
|
||||
"stylelint",
|
||||
"WUJIE",
|
||||
"zlevel"
|
||||
],
|
||||
"peacock.color": "#007fff"
|
||||
]
|
||||
}
|
||||
|
120
CHANGELOG.md
120
CHANGELOG.md
@ -1,4 +1,120 @@
|
||||
# CHANGE LOG
|
||||
## 5.1.0
|
||||
|
||||
## Feats
|
||||
|
||||
- 主流依赖更新
|
||||
- `RDraggableCard` 组件 `defaultPosition` 配置项新增 `center`, `top-center`, `bottom-center` 配置项,并且该配置项支持动态更新了
|
||||
- `RDraggableCard` 组件容器 `id` 由 `draggable-card-container` 变更为 `r-draggable-card-container`
|
||||
- `views/demo` 包命名调整
|
||||
|
||||
## Fixes
|
||||
|
||||
- 修复 `RDraggableCard` 组件设置 `dad` 为 `false` 时,初始化位置错误的问题
|
||||
|
||||
## 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
|
||||
|
||||
@ -103,7 +219,7 @@
|
||||
- 新增 `clearSigningCallback` 方法
|
||||
- `vite.custom.config` 新增 `cdn` 配置项,是否启用 `cdn` 构建项目
|
||||
- 配置 `cdn` 为 `false`,因为国内厂商更新资源速度有点慢,导致预览失败
|
||||
- `Layout` 层注入 `--window-width`, `--window-height` `css var` 属性
|
||||
- `Layout` 层注入 `--window-width`, `--window-height`, `css var` 属性
|
||||
- 稳定 `Layout` 层的 `css var` 属性
|
||||
|
||||
## Fixes
|
||||
|
@ -3,6 +3,7 @@ import { callWithAsyncErrorHandling } from '../../src/utils/basic'
|
||||
describe('callWithAsyncErrorHandling', () => {
|
||||
it('should call the function and return the result', () => {
|
||||
const fn = (x: number) => x
|
||||
|
||||
const callbackFn = () => {}
|
||||
|
||||
expect(callWithAsyncErrorHandling(fn, callbackFn, [1])).resolves.toBe(1)
|
||||
@ -14,6 +15,7 @@ describe('callWithAsyncErrorHandling', () => {
|
||||
const fn = () => {
|
||||
throw new Error('test error')
|
||||
}
|
||||
|
||||
const callbackFn = () => {
|
||||
callbackFnExecuted = 2
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { callWithErrorHandling } from '../../src/utils/basic'
|
||||
describe('callWithErrorHandling', () => {
|
||||
it('should call the function and return the result', () => {
|
||||
const fn = (x: number) => x
|
||||
|
||||
const callbackFn = () => {}
|
||||
|
||||
expect(callWithErrorHandling(fn, callbackFn, [1])).toBe(1)
|
||||
@ -14,6 +15,7 @@ describe('callWithErrorHandling', () => {
|
||||
const fn = () => {
|
||||
throw new Error('test error')
|
||||
}
|
||||
|
||||
const callbackFn = () => {
|
||||
callbackFnExecuted = 2
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ describe('isValueType', () => {
|
||||
})
|
||||
|
||||
it('should return false for Function', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
expect(isValueType<Function>(/a/i, 'Function')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
@ -14,6 +14,7 @@ describe('uuid', () => {
|
||||
|
||||
it('should return a string with length 36', () => {
|
||||
const uid = uuid(36)
|
||||
|
||||
expect(uid.length).toBe(36)
|
||||
})
|
||||
})
|
||||
|
@ -39,6 +39,7 @@ describe('useContextmenuCoordinate', () => {
|
||||
clientX: 100,
|
||||
clientY: 200,
|
||||
})
|
||||
|
||||
wrapperRef.element.dispatchEvent(event)
|
||||
|
||||
await nextTick()
|
||||
|
@ -13,6 +13,7 @@ import { mount } from '@vue/test-utils'
|
||||
*
|
||||
* const text = wrapper.find('div').text() // hello
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const createRefElement = (slots?: Record<string, Function>) => {
|
||||
const wrapper = mount(
|
||||
defineComponent({
|
||||
|
@ -3,6 +3,7 @@ import { call } from '../../src/utils/vue/call'
|
||||
describe('call', () => {
|
||||
it('should be executed once', () => {
|
||||
const fn = vi.fn()
|
||||
|
||||
call(() => fn())
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
@ -10,6 +11,7 @@ describe('call', () => {
|
||||
|
||||
it('should be executed with an argument', () => {
|
||||
const fn = vi.fn()
|
||||
|
||||
call((a: number) => fn(a), 1)
|
||||
|
||||
expect(fn).toHaveBeenCalledWith(1)
|
||||
|
@ -4,6 +4,7 @@ import createRefElement from '../utils/createRefElement'
|
||||
describe('renderNode', () => {
|
||||
it('should render string', () => {
|
||||
const wrapper = createRefElement({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
default: renderNode('hello world') as Function,
|
||||
})
|
||||
const text = wrapper.text()
|
||||
|
365
eslint.config.mjs
Normal file
365
eslint.config.mjs
Normal file
@ -0,0 +1,365 @@
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import prettier from 'eslint-plugin-prettier'
|
||||
import globals from 'globals'
|
||||
import parser from 'vue-eslint-parser'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import js from '@eslint/js'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
})
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/',
|
||||
'**/dist/',
|
||||
'dist/*',
|
||||
'node_modules/*',
|
||||
'**/auto-imports.d.ts',
|
||||
'**/components.d.ts',
|
||||
'**/.gitignore',
|
||||
'**/.vscode',
|
||||
'**/public',
|
||||
'**/yarn.*',
|
||||
'**/vite-env.*',
|
||||
'**/.prettierrc.*',
|
||||
'**/visualizer.*',
|
||||
'**/visualizer.html',
|
||||
'**/.env.*',
|
||||
'src/locales/lang',
|
||||
'**/.depcheckrc',
|
||||
'src/app-config/echart-themes/**/*.json',
|
||||
'**/*.md',
|
||||
'src/icons/*.svg',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.vue'],
|
||||
},
|
||||
...compat.extends(
|
||||
'eslint-config-prettier',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'./unplugin/.eslintrc-auto-import.json',
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
vue,
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly',
|
||||
defineOptions: 'readonly',
|
||||
defineModel: 'readonly',
|
||||
},
|
||||
parser: parser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undefined': ['error'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
},
|
||||
],
|
||||
'prettier/prettier': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
disallowTypeAnnotations: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'accessor-pairs': 2,
|
||||
'constructor-super': 0,
|
||||
'default-case': 2,
|
||||
eqeqeq: [2, 'allow-null'],
|
||||
'no-alert': 0,
|
||||
'no-array-constructor': 2,
|
||||
'no-bitwise': 0,
|
||||
'no-caller': 1,
|
||||
'no-catch-shadow': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-cond-assign': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-constant-condition': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-eval': 1,
|
||||
'no-ex-assign': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': [
|
||||
'error',
|
||||
{
|
||||
enforceForLogicalOperands: true,
|
||||
},
|
||||
],
|
||||
'no-extra-parens': 0,
|
||||
semi: [
|
||||
'error',
|
||||
'never',
|
||||
{
|
||||
beforeStatementContinuationChars: 'always',
|
||||
},
|
||||
],
|
||||
'no-fallthrough': 1,
|
||||
'no-func-assign': 2,
|
||||
'no-implicit-coercion': [
|
||||
'error',
|
||||
{
|
||||
allow: ['!!', '~'],
|
||||
},
|
||||
],
|
||||
'no-implied-eval': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-invalid-this': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-label-var': 2,
|
||||
'no-labels': 2,
|
||||
'no-lone-blocks': 2,
|
||||
'no-multi-spaces': 1,
|
||||
'no-multiple-empty-lines': [
|
||||
'error',
|
||||
{
|
||||
max: 2,
|
||||
},
|
||||
],
|
||||
'no-new-func': 2,
|
||||
'no-new-object': 2,
|
||||
'no-new-require': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-trailing-spaces': 1,
|
||||
'no-unreachable': 2,
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
enforceForJSX: true,
|
||||
},
|
||||
],
|
||||
'no-useless-call': 2,
|
||||
'no-var': 'error',
|
||||
'no-with': 2,
|
||||
'use-isnan': 2,
|
||||
'no-multi-assign': 2,
|
||||
'prefer-arrow-callback': 2,
|
||||
curly: ['error', 'all'],
|
||||
'vue/multi-word-component-names': [
|
||||
'error',
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
],
|
||||
'vue/no-use-v-if-with-v-for': [
|
||||
'error',
|
||||
{
|
||||
allowUsingIterationVar: false,
|
||||
},
|
||||
],
|
||||
'vue/require-v-for-key': ['error'],
|
||||
'vue/require-valid-default-prop': ['error'],
|
||||
'vue/component-definition-name-casing': ['error', 'PascalCase'],
|
||||
'vue/html-closing-bracket-newline': [
|
||||
'error',
|
||||
{
|
||||
singleline: 'never',
|
||||
multiline: 'always',
|
||||
},
|
||||
],
|
||||
'vue/v-on-event-hyphenation': ['error', 'never'],
|
||||
'vue/component-tags-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['template', 'script', 'style'],
|
||||
},
|
||||
],
|
||||
'vue/no-v-html': ['error'],
|
||||
'vue/no-v-text': ['error'],
|
||||
'vue/component-api-style': [
|
||||
'error',
|
||||
['script-setup', 'composition', 'composition-vue2'],
|
||||
],
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false,
|
||||
},
|
||||
],
|
||||
'vue/no-unused-refs': ['error'],
|
||||
'vue/prop-name-casing': ['error', 'camelCase'],
|
||||
'vue/component-options-name-casing': ['error', 'PascalCase'],
|
||||
'vue/attribute-hyphenation': [
|
||||
'error',
|
||||
'never',
|
||||
{
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'vue/no-restricted-static-attribute': [
|
||||
'error',
|
||||
{
|
||||
key: 'key',
|
||||
message: 'Disallow using key as a custom attribute',
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='deprecated']",
|
||||
message: 'Using deprecated API is not allowed.',
|
||||
},
|
||||
],
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['import'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'import',
|
||||
next: 'import',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'export',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'export',
|
||||
next: 'export',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['const', 'let', 'var'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: ['const', 'let', 'var'],
|
||||
next: ['const', 'let', 'var'],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: 'directive',
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: 'directive',
|
||||
next: 'directive',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: [
|
||||
'if',
|
||||
'class',
|
||||
'for',
|
||||
'do',
|
||||
'while',
|
||||
'switch',
|
||||
'try',
|
||||
'with',
|
||||
'function',
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'case',
|
||||
'continue',
|
||||
'return',
|
||||
'throw',
|
||||
'debugger',
|
||||
],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: [
|
||||
'if',
|
||||
'class',
|
||||
'for',
|
||||
'do',
|
||||
'while',
|
||||
'switch',
|
||||
'try',
|
||||
'with',
|
||||
'function',
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'case',
|
||||
'continue',
|
||||
'return',
|
||||
'throw',
|
||||
'debugger',
|
||||
],
|
||||
next: '*',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-object-type': [
|
||||
'error',
|
||||
{
|
||||
allowInterfaces: 'with-single-extends',
|
||||
allowObjectTypes: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
39
index.html
39
index.html
@ -15,6 +15,27 @@
|
||||
--preloading-title-color: <%= preloadingConfig.titleColor %>;
|
||||
--ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>;
|
||||
--ray-theme-primary-color: <%= appPrimaryColor.primaryColor %>;
|
||||
--global-loading-bg-color: #ffffff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#pre-loading-animation {
|
||||
background-color: var(--global-loading-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
#pre-loading-animation {
|
||||
background-color: var(--global-loading-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark #pre-loading-animation {
|
||||
background-color: var(--global-loading-bg-color);
|
||||
}
|
||||
|
||||
html.light #pre-loading-animation {
|
||||
background-color: var(--global-loading-bg-color);
|
||||
}
|
||||
|
||||
#pre-loading-animation {
|
||||
@ -23,13 +44,9 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
color: var(--preloading-title-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ray-template--dark #pre-loading-animation {
|
||||
background-color: #2a3146;
|
||||
background-color: var(--global-loading-bg-color);
|
||||
}
|
||||
|
||||
#pre-loading-animation .pre-loading-animation__wrapper {
|
||||
@ -95,6 +112,18 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
;(function () {
|
||||
const html = document.documentElement
|
||||
const store = window.localStorage.getItem('piniaSettingStore')
|
||||
const { _appTheme = false } = store ? JSON.parse(store) : {}
|
||||
const loadingBgColor = _appTheme ? '#1c1e23' : '#ffffff'
|
||||
|
||||
html.classList.add(_appTheme ? 'dark' : 'light')
|
||||
html.style.setProperty('--global-loading-bg-color', loadingBgColor)
|
||||
html.style.setProperty('background-color', loadingBgColor)
|
||||
})()
|
||||
</script>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="pre-loading-animation">
|
||||
|
122
package.json
122
package.json
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "ray-template",
|
||||
"private": false,
|
||||
"version": "5.0.4",
|
||||
"version": "5.1.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0",
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@ -16,7 +16,7 @@
|
||||
"prepare": "husky install",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"lint": "vue-tsc --noEmit && eslint src --ext .js,.jsx,.vue && prettier --write \"src/**/*.{ts,tsx,json,.vue}\""
|
||||
"lint": "vue-tsc --noEmit && eslint --fix && prettier --write \"**/*.{ts,tsx,json,.vue}\""
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -29,83 +29,85 @@
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{ts,tsx,vue}": [
|
||||
"eslint src"
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@logicflow/core": "2.0.6",
|
||||
"@logicflow/extension": "2.0.10",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.5",
|
||||
"@logicflow/core": "2.0.10",
|
||||
"@logicflow/extension": "2.0.14",
|
||||
"@vueuse/core": "^12.4.0",
|
||||
"axios": "^1.7.9",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "4.2.0",
|
||||
"currency.js": "^2.0.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"html-to-image": "1.11.11",
|
||||
"interactjs": "1.10.26",
|
||||
"interactjs": "1.10.27",
|
||||
"jsbarcode": "3.11.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mockjs": "1.1.0",
|
||||
"naive-ui": "^2.40.1",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^4.1.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"pinia": "^2.3.0",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"print-js": "^1.6.0",
|
||||
"vue": "^3.5.12",
|
||||
"vue-demi": "0.14.6",
|
||||
"vue-hooks-plus": "2.2.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-demi": "0.14.10",
|
||||
"vue-hooks-plus": "2.2.3",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue3-next-qrcode": "2.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.8.1",
|
||||
"@commitlint/config-conventional": "^17.8.1",
|
||||
"@interactjs/types": "1.10.21",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@amap/amap-jsapi-types": "0.0.15",
|
||||
"@ant-design/icons-vue": "7.0.1",
|
||||
"@commitlint/cli": "19.3.0",
|
||||
"@commitlint/config-conventional": "19.2.2",
|
||||
"@interactjs/types": "1.10.27",
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/jsbarcode": "3.11.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mockjs": "1.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"@typescript-eslint/parser": "^8.13.0",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vitest/ui": "1.4.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "2.4.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"depcheck": "^1.4.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"happy-dom": "14.3.1",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/mockjs": "1.0.10",
|
||||
"@types/three": "0.171.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.1",
|
||||
"@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.20",
|
||||
"depcheck": "1.4.7",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-plugin-prettier": "5.2.2",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"globals": "15.14.0",
|
||||
"happy-dom": "16.6.0",
|
||||
"husky": "8.0.3",
|
||||
"lint-staged": "^15.2.0",
|
||||
"postcss": "^8.4.38",
|
||||
"lint-staged": "15.3.0",
|
||||
"postcss": "8.5.1",
|
||||
"postcss-px-to-viewport-8-with-include": "1.2.2",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "3.4.2",
|
||||
"rollup-plugin-gzip": "4.0.1",
|
||||
"sass": "1.71.1",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.3",
|
||||
"vite-bundle-analyzer": "0.9.4",
|
||||
"sass": "1.83.4",
|
||||
"svg-sprite-loader": "6.0.11",
|
||||
"typescript": "5.6.3",
|
||||
"unplugin-auto-import": "19.0.0",
|
||||
"unplugin-vue-components": "0.28.0",
|
||||
"vite": "6.1.0",
|
||||
"vite-bundle-analyzer": "0.16.0",
|
||||
"vite-plugin-cdn2": "1.1.0",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"vite-plugin-ejs": "1.7.0",
|
||||
"vite-plugin-eslint": "1.8.1",
|
||||
"vite-plugin-inspect": "^0.8.3",
|
||||
"vite-plugin-mock-dev-server": "1.4.7",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vitest": "1.5.2",
|
||||
"vue-tsc": "^2.1.10"
|
||||
"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-tsc": "2.2.0"
|
||||
},
|
||||
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
|
||||
"main": "index.ts",
|
||||
|
6545
pnpm-lock.yaml
generated
6545
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ interface JSONPlaceholder {
|
||||
*
|
||||
* @returns 测试
|
||||
*
|
||||
* @medthod get
|
||||
* @method get
|
||||
*/
|
||||
export const getWeather = (city: string) => {
|
||||
return request<AxiosTestResponse>({
|
||||
|
@ -54,7 +54,14 @@ const AppAvatar = defineComponent({
|
||||
objectFit="cover"
|
||||
round
|
||||
size={avatarSize}
|
||||
/>
|
||||
>
|
||||
{{
|
||||
default: () =>
|
||||
getSigningCallback.avatar
|
||||
? null
|
||||
: getSigningCallback?.name?.[0],
|
||||
}}
|
||||
</NAvatar>
|
||||
{getSigningCallback?.name}
|
||||
</NFlex>
|
||||
</NButton>
|
||||
|
@ -44,6 +44,7 @@ const GlobalSpin = defineComponent({
|
||||
{...(this.$props as SpinProps)}
|
||||
show={this.spinValue}
|
||||
themeOverrides={this.overrides}
|
||||
style="height: var(--html-height)"
|
||||
>
|
||||
{{
|
||||
...this.$slots,
|
||||
|
@ -1,17 +1,10 @@
|
||||
/**
|
||||
*
|
||||
* 统一管理是否处于锁屏状态
|
||||
*
|
||||
* 可以根据后台接口进行替换该变量, 只要是一个响应式的变量值即可
|
||||
*/
|
||||
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { APP_CATCH_KEY } from '@/app-config'
|
||||
|
||||
const appLockScreen = useStorage(
|
||||
APP_CATCH_KEY.isAppLockScreen,
|
||||
false,
|
||||
sessionStorage,
|
||||
window.localStorage,
|
||||
{
|
||||
mergeDefaults: true,
|
||||
},
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { NInput, NForm, NFormItem, NButton } from 'naive-ui'
|
||||
import { NInput, NFormItem, NButton } from 'naive-ui'
|
||||
import AppAvatar from '@/app-components/app/AppAvatar'
|
||||
import { RForm } from '@/components'
|
||||
|
||||
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
|
||||
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
|
||||
import { useSettingGetters, useSettingActions } from '@/store'
|
||||
import { useSettingActions } from '@/store'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useForm } from '@/components'
|
||||
import { APP_CATCH_KEY } from '@/app-config'
|
||||
import { setStorage, encrypt } from '@/utils'
|
||||
|
||||
import type { FormInst, InputInst } from 'naive-ui'
|
||||
import type { InputInst } from 'naive-ui'
|
||||
|
||||
const LockScreen = defineComponent({
|
||||
name: 'LockScreen',
|
||||
setup() {
|
||||
const formInstRef = ref<FormInst | null>(null)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const [register, { validate }] = useForm()
|
||||
const inputInstRef = useTemplateRef<InputInst | null>('inputInstRef')
|
||||
|
||||
const { setLockAppScreen } = useAppLockScreen()
|
||||
const { updateSettingState } = useSettingActions()
|
||||
@ -19,15 +25,17 @@ const LockScreen = defineComponent({
|
||||
lockCondition: useCondition(),
|
||||
})
|
||||
|
||||
/** 锁屏 */
|
||||
const lockScreen = () => {
|
||||
formInstRef.value?.validate((error) => {
|
||||
if (!error) {
|
||||
setLockAppScreen(true)
|
||||
updateSettingState('lockScreenSwitch', true)
|
||||
validate().then(() => {
|
||||
setLockAppScreen(true)
|
||||
updateSettingState('lockScreenSwitch', false)
|
||||
setStorage(
|
||||
APP_CATCH_KEY.appLockScreenPasswordKey,
|
||||
encrypt(state.lockCondition.lockPassword),
|
||||
'localStorage',
|
||||
)
|
||||
|
||||
state.lockCondition = useCondition()
|
||||
}
|
||||
state.lockCondition = useCondition()
|
||||
})
|
||||
}
|
||||
|
||||
@ -40,40 +48,51 @@ const LockScreen = defineComponent({
|
||||
return {
|
||||
...toRefs(state),
|
||||
lockScreen,
|
||||
formInstRef,
|
||||
register,
|
||||
inputInstRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { register } = this
|
||||
|
||||
return (
|
||||
<div class="app-lock-screen__input">
|
||||
<NForm
|
||||
ref="formInstRef"
|
||||
model={this.lockCondition}
|
||||
rules={rules}
|
||||
labelPlacement="left"
|
||||
>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
ref="inputInstRef"
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入锁屏密码"
|
||||
clearable
|
||||
showPasswordOn="click"
|
||||
minlength={6}
|
||||
maxlength={12}
|
||||
onKeydown={(e: KeyboardEvent) => {
|
||||
if (e.code === 'Enter') {
|
||||
this.lockScreen()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
|
||||
锁屏
|
||||
</NButton>
|
||||
</NForm>
|
||||
<div class="app-lock-screen__content">
|
||||
<div class="app-lock-screen__input">
|
||||
<AppAvatar
|
||||
avatarSize={52}
|
||||
style="pointer-events: none;margin: 24px 0;"
|
||||
vertical
|
||||
/>
|
||||
<RForm
|
||||
ref="formInstRef"
|
||||
model={this.lockCondition}
|
||||
rules={rules}
|
||||
labelPlacement="left"
|
||||
onRegister={register}
|
||||
>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
ref="inputInstRef"
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入锁屏密码"
|
||||
clearable
|
||||
showPasswordOn="click"
|
||||
minlength={6}
|
||||
maxlength={12}
|
||||
onKeydown={(e: KeyboardEvent) => {
|
||||
if (e.code === 'Enter') {
|
||||
this.lockScreen()
|
||||
}
|
||||
}}
|
||||
autofocus
|
||||
/>
|
||||
</NFormItem>
|
||||
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
|
||||
锁屏
|
||||
</NButton>
|
||||
</RForm>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
@ -1,19 +1,22 @@
|
||||
import { NInput, NForm, NFormItem, NButton, NFlex } from 'naive-ui'
|
||||
import '../../index.scss'
|
||||
|
||||
import { NInput, NFormItem, NButton, NFlex } from 'naive-ui'
|
||||
import AppAvatar from '@/app-components/app/AppAvatar'
|
||||
import { RForm } from '@/components'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { useSigningActions, useSettingActions } from '@/store'
|
||||
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
|
||||
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
import type { FormInst, InputInst } from 'naive-ui'
|
||||
import { useForm } from '@/components'
|
||||
import { APP_CATCH_KEY } from '@/app-config'
|
||||
import { removeStorage, decrypt, getStorage } from '@/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UnlockScreen',
|
||||
setup() {
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const [register, { validate }] = useForm()
|
||||
|
||||
const { logout } = useSigningActions()
|
||||
const { updateSettingState } = useSettingActions()
|
||||
@ -22,13 +25,13 @@ export default defineComponent({
|
||||
|
||||
const HH_MM_FORMAT = 'HH:mm'
|
||||
const AM_PM_FORMAT = 'A'
|
||||
const YY_MM_DD_FORMAT = 'YY年MM月DD日'
|
||||
const YY_MM_DD_FORMAT = 'YYYY-MM-DD'
|
||||
const DDD_FORMAT = 'ddd'
|
||||
|
||||
const state = reactive({
|
||||
lockCondition: useCondition(),
|
||||
HH_MM: dayjs().format(HH_MM_FORMAT),
|
||||
AM_PM: dayjs().locale('en').format(AM_PM_FORMAT),
|
||||
AM_PM: dayjs().format(AM_PM_FORMAT),
|
||||
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
|
||||
DDD: dayjs().format(DDD_FORMAT),
|
||||
})
|
||||
@ -41,30 +44,55 @@ export default defineComponent({
|
||||
state.DDD = dayjs().format(DDD_FORMAT)
|
||||
}, 86_400_000)
|
||||
|
||||
/** 退出登陆并且回到登陆页 */
|
||||
const toSigningFn = () => {
|
||||
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
|
||||
updateSettingState('lockScreenSwitch', false)
|
||||
setTimeout(() => {
|
||||
logout()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const backToSigning = () => {
|
||||
window.$dialog.warning({
|
||||
title: '警告',
|
||||
content: '是否返回到登陆页?',
|
||||
content: '是否返回到登陆页并且重新登录',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
logout()
|
||||
setTimeout(() => {
|
||||
updateSettingState('lockScreenSwitch', false)
|
||||
})
|
||||
},
|
||||
negativeText: '重新登录',
|
||||
onPositiveClick: toSigningFn,
|
||||
})
|
||||
}
|
||||
|
||||
/** 解锁 */
|
||||
const unlockScreen = () => {
|
||||
formRef.value?.validate((error) => {
|
||||
if (!error) {
|
||||
const catchPassword = getStorage<string>(
|
||||
APP_CATCH_KEY.appLockScreenPasswordKey,
|
||||
'localStorage',
|
||||
)
|
||||
|
||||
if (!catchPassword) {
|
||||
window.$dialog.warning({
|
||||
title: '警告',
|
||||
content: () => '检测到锁屏密码被修改,请重新登录',
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
closeOnEsc: false,
|
||||
positiveText: '重新登录',
|
||||
onPositiveClick: toSigningFn,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const dCatchPassword = decrypt(catchPassword)
|
||||
|
||||
validate().then(() => {
|
||||
if (dCatchPassword === state.lockCondition.lockPassword) {
|
||||
setLockAppScreen(false)
|
||||
updateSettingState('lockScreenSwitch', false)
|
||||
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
|
||||
|
||||
state.lockCondition = useCondition()
|
||||
} else {
|
||||
window.$message.warning('密码错误,请重新输入')
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -78,71 +106,84 @@ export default defineComponent({
|
||||
...toRefs(state),
|
||||
backToSigning,
|
||||
unlockScreen,
|
||||
formRef,
|
||||
inputInstRef,
|
||||
isTabletOrSmaller,
|
||||
register,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { isTabletOrSmaller } = this
|
||||
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
|
||||
const hmSplit = HH_MM.split(':')
|
||||
const { unlockScreen, backToSigning } = this
|
||||
const { unlockScreen, backToSigning, register } = this
|
||||
|
||||
return (
|
||||
<div class="app-lock-screen__unlock">
|
||||
<div class="app-lock-screen__unlock__content">
|
||||
<div class="app-lock-screen__unlock__content-wrapper">
|
||||
<div
|
||||
class={[
|
||||
'app-lock-screen__unlock__content-bg__wrapper',
|
||||
'app-lock-screen__unlock__content-bg',
|
||||
isTabletOrSmaller
|
||||
? 'app-lock-screen__unlock__content-bg--smaller'
|
||||
: '',
|
||||
]}
|
||||
>
|
||||
<div class="left">{hmSplit[0]}</div>
|
||||
<div class="right">{hmSplit[1]}</div>
|
||||
<div class="app-lock-screen__content app-lock-screen__content--full">
|
||||
<div class="app-lock-screen__unlock">
|
||||
<div class="app-lock-screen__unlock__content">
|
||||
<div class="app-lock-screen__unlock__content-wrapper">
|
||||
<div
|
||||
class={[
|
||||
'app-lock-screen__unlock__content-bg__wrapper',
|
||||
'app-lock-screen__unlock__content-bg',
|
||||
isTabletOrSmaller
|
||||
? 'app-lock-screen__unlock__content-bg--smaller'
|
||||
: '',
|
||||
]}
|
||||
>
|
||||
<div class="left">{hmSplit[0]}</div>
|
||||
<div class="right">{hmSplit[1]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-avatar">
|
||||
<AppAvatar avatarSize={52} style="pointer-events: none;" vertical />
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-input">
|
||||
<NForm ref="formRef" model={this.lockCondition} rules={rules}>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
ref="inputInstRef"
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入解锁密码"
|
||||
clearable
|
||||
minlength={6}
|
||||
maxlength={12}
|
||||
onKeydown={(e: KeyboardEvent) => {
|
||||
if (e.code === 'Enter') {
|
||||
unlockScreen()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFlex justify="space-between">
|
||||
<NButton type="primary" text onClick={backToSigning.bind(this)}>
|
||||
返回登陆
|
||||
</NButton>
|
||||
<NButton type="primary" text onClick={unlockScreen.bind(this)}>
|
||||
进入系统
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NForm>
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-date">
|
||||
<div class="current-date">
|
||||
{HH_MM} <span>{AM_PM}</span>
|
||||
<div class="app-lock-screen__unlock__content-avatar">
|
||||
<AppAvatar
|
||||
avatarSize={52}
|
||||
style="pointer-events: none;"
|
||||
vertical
|
||||
/>
|
||||
</div>
|
||||
<div class="current-year">
|
||||
{YY_MM_DD} <span>{DDD}</span>
|
||||
<div class="app-lock-screen__unlock__content-input">
|
||||
<RForm
|
||||
onRegister={register}
|
||||
model={this.lockCondition}
|
||||
rules={rules}
|
||||
>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
autofocus
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入解锁密码"
|
||||
clearable
|
||||
minlength={6}
|
||||
onKeydown={(e: KeyboardEvent) => {
|
||||
if (e.code === 'Enter') {
|
||||
unlockScreen()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFlex justify="space-between">
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
onClick={backToSigning.bind(this)}
|
||||
>
|
||||
返回登陆
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
onClick={unlockScreen.bind(this)}
|
||||
>
|
||||
进入系统
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</RForm>
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-date">
|
||||
<div class="current-year">
|
||||
{YY_MM_DD} <span>{DDD}</span> <span>{AM_PM}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,10 @@
|
||||
.app-lock-screen__content {
|
||||
&.app-lock-screen__content--full {
|
||||
width: 100%;
|
||||
height: var(--html-height);
|
||||
@include flexCenter;
|
||||
}
|
||||
|
||||
& .app-lock-screen__input {
|
||||
& button[class*='n-button'] {
|
||||
width: 100%;
|
||||
@ -30,7 +36,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include flexCenter;
|
||||
font-size: 320px;
|
||||
font-size: 16.67rem;
|
||||
gap: 80px;
|
||||
z-index: 0;
|
||||
|
||||
@ -79,9 +85,23 @@
|
||||
|
||||
& .current-year,
|
||||
& .current-date span {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ray-template--light {
|
||||
.app-lock-screen__unlock__content-bg__wrapper {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.app-lock-screen__unlock__content-bg {
|
||||
& .left,
|
||||
& .right {
|
||||
background-color: rgba(244, 244, 245, 1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,11 @@
|
||||
/**
|
||||
*
|
||||
* 这里没有做解锁密码校验, 只要符合校验规则值皆可
|
||||
* 可以根据需求自行更改
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { RModal } from '@/components'
|
||||
import LockScreen from './components/LockScreen'
|
||||
import UnlockScreen from './components/UnlockScreen'
|
||||
|
||||
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
|
||||
import { useSettingGetters, useSettingActions } from '@/store'
|
||||
|
||||
const AppLockScreen = defineComponent({
|
||||
name: 'AppLockScreen',
|
||||
setup() {
|
||||
const { getLockAppScreen } = useAppLockScreen()
|
||||
const { updateSettingState } = useSettingActions()
|
||||
const { getLockScreenSwitch } = useSettingGetters()
|
||||
const lockScreenSwitchRef = computed({
|
||||
@ -28,12 +17,9 @@ const AppLockScreen = defineComponent({
|
||||
|
||||
return {
|
||||
lockScreenSwitchRef,
|
||||
getLockAppScreen,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { getLockAppScreen } = this
|
||||
|
||||
return (
|
||||
<RModal
|
||||
v-model:show={this.lockScreenSwitchRef}
|
||||
@ -42,12 +28,10 @@ const AppLockScreen = defineComponent({
|
||||
autoFocus={false}
|
||||
maskClosable={false}
|
||||
closeOnEsc={false}
|
||||
preset={!getLockAppScreen() ? 'dialog' : void 0}
|
||||
preset="dialog"
|
||||
title="锁定屏幕"
|
||||
>
|
||||
<div class="app-lock-screen__content">
|
||||
{!getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
|
||||
</div>
|
||||
<LockScreen />
|
||||
</RModal>
|
||||
)
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
getStorage,
|
||||
} from '@/utils'
|
||||
import { useSettingGetters } from '@/store'
|
||||
import { APP_CATCH_KEY, THEME_CLASS_NAMES } from '@/app-config'
|
||||
import { APP_CATCH_KEY, GLOBAL_CLASS_NAMES, APP_THEME } from '@/app-config'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
import type { SettingState } from '@/store/modules/setting/types'
|
||||
@ -20,9 +20,14 @@ export default defineComponent({
|
||||
|
||||
// 同步主题色变量至 html,如果未获取到缓存值则已默认值填充
|
||||
const syncPrimaryColorToBody = () => {
|
||||
// 默认主题色
|
||||
const {
|
||||
appPrimaryColor: { primaryColor, primaryFadeColor },
|
||||
} = __APP_CFG__ // 默认主题色
|
||||
} = APP_THEME
|
||||
// 主题色配置 class 名
|
||||
const { rayTemplateThemePrimaryColor, rayTemplateThemePrimaryFadeColor } =
|
||||
GLOBAL_CLASS_NAMES
|
||||
|
||||
const html = document.documentElement
|
||||
|
||||
// 获取缓存 naive ui 配置项
|
||||
@ -39,21 +44,22 @@ export default defineComponent({
|
||||
primaryColor,
|
||||
)
|
||||
// 将主色调任意颜色转换为 rgba 格式
|
||||
const fp = colorToRgba(p, 0.38)
|
||||
const fp = colorToRgba(p, 0.85)
|
||||
|
||||
// 设置全局主题色 css 变量
|
||||
html.style.setProperty('--ray-theme-primary-color', p) // 主色调
|
||||
html.style.setProperty(rayTemplateThemePrimaryColor, p) // 主色调
|
||||
// 降低透明度后的主色调
|
||||
html.style.setProperty(
|
||||
'--ray-theme-primary-fade-color',
|
||||
rayTemplateThemePrimaryFadeColor,
|
||||
fp || primaryFadeColor,
|
||||
) // 降低透明度后的主色调
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载动画
|
||||
const hiddenLoadingAnimation = () => {
|
||||
// pre-loading-animation 是默认 id
|
||||
const el = document.getElementById('pre-loading-animation')
|
||||
const el = document.getElementById(GLOBAL_CLASS_NAMES.preLoadingAnimation)
|
||||
|
||||
if (el) {
|
||||
setStyle(el, {
|
||||
@ -65,7 +71,7 @@ export default defineComponent({
|
||||
// 切换主题时,同步更新 html class 以便于进行自定义 css 配置
|
||||
const updateGlobalThemeClass = (bool: boolean) => {
|
||||
const html = document.documentElement
|
||||
const { darkClassName, lightClassName } = THEME_CLASS_NAMES
|
||||
const { darkClassName, lightClassName } = GLOBAL_CLASS_NAMES
|
||||
|
||||
bool
|
||||
? removeClass(html, lightClassName)
|
||||
@ -82,8 +88,8 @@ export default defineComponent({
|
||||
updateGlobalThemeClass(getAppTheme.value)
|
||||
// 注入全局宽高尺寸
|
||||
setStyle(document.documentElement, {
|
||||
'--html-height': `${height.value}px`,
|
||||
'--html-width': `${width.value}px`,
|
||||
[GLOBAL_CLASS_NAMES.htmlHeight]: `${height.value}px`,
|
||||
[GLOBAL_CLASS_NAMES.htmlWidth]: `${width.value}px`,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -28,18 +28,10 @@ export default defineComponent({
|
||||
if (version !== cacheVersion) {
|
||||
modalShow.value = true
|
||||
|
||||
setStorage<string>(
|
||||
APP_CATCH_KEY.appVersionProvider,
|
||||
version,
|
||||
'localStorage',
|
||||
)
|
||||
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
|
||||
}
|
||||
} else {
|
||||
setStorage<string>(
|
||||
APP_CATCH_KEY.appVersionProvider,
|
||||
version,
|
||||
'localStorage',
|
||||
)
|
||||
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
|
||||
}
|
||||
|
||||
return {
|
||||
@ -61,7 +53,7 @@ export default defineComponent({
|
||||
title="发现新版本"
|
||||
content="当前版本已更新,点击确认加载新版本~"
|
||||
zIndex={999999999}
|
||||
dad
|
||||
draggable
|
||||
positiveText="确认"
|
||||
negativeText="取消"
|
||||
onPositiveClick={logout}
|
||||
|
@ -5,11 +5,19 @@ import type { MessageProviderProps } from 'naive-ui'
|
||||
*
|
||||
* @description
|
||||
* 全局注入到 html 的样式类名。
|
||||
*
|
||||
* 如果涉及到全局主题色的 class name,需要修改的时候记得全局替换;
|
||||
* 避免样式出现奇奇怪怪的问题。
|
||||
*/
|
||||
export const THEME_CLASS_NAMES = {
|
||||
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
|
||||
|
||||
/**
|
||||
*
|
||||
@ -85,6 +93,8 @@ export const APP_CATCH_KEY_PREFIX = ''
|
||||
* - appPiniaMenuStore: pinia menu store key
|
||||
* - appPiniaSigningStore: pinia signing store key
|
||||
* - appVersionProvider: 版本信息缓存 key
|
||||
* - appMenuTagOptions: 标签页菜单列表
|
||||
* - appLockScreenPasswordKey: 锁屏密码缓存 key
|
||||
*/
|
||||
export const APP_CATCH_KEY = {
|
||||
signing: 'signing',
|
||||
@ -98,6 +108,8 @@ export const APP_CATCH_KEY = {
|
||||
appVersionProvider: 'appVersionProvider',
|
||||
isAppLockScreen: 'isAppLockScreen',
|
||||
appGlobalSearchOptions: 'appGlobalSearchOptions',
|
||||
appMenuTagOptions: 'appMenuTagOptions',
|
||||
appLockScreenPasswordKey: 'appLockScreenPasswordKey',
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
@ -3,8 +3,9 @@ import type { AppTheme } from '@/types'
|
||||
export const APP_THEME: AppTheme = {
|
||||
/**
|
||||
*
|
||||
* 系统主题颜色预设色盘
|
||||
* 支持 RGBA、RGB、十六进制
|
||||
* @description
|
||||
* 系统主题颜色预设色盘。
|
||||
* 支持 RGBA、RGB、十六进制。
|
||||
*/
|
||||
appThemeColors: [
|
||||
'#2d8cf0',
|
||||
@ -12,25 +13,26 @@ export const APP_THEME: AppTheme = {
|
||||
'#ff42bc',
|
||||
'#ee4f12',
|
||||
'#dbcb02',
|
||||
'#18A058',
|
||||
'#18a058',
|
||||
],
|
||||
/** 系统主题色 */
|
||||
// 系统主题色
|
||||
appPrimaryColor: {
|
||||
/** 主题色 */
|
||||
// 主题色
|
||||
primaryColor: '#2d8cf0',
|
||||
/** 主题辅助色(用于整体 hover、active 等之类颜色) */
|
||||
primaryFadeColor: 'rgba(45, 140, 240, 0.3)',
|
||||
// 主题辅助色(用于整体 hover、active 等之类颜色)
|
||||
primaryFadeColor: 'rgba(45, 140, 240, 0.85)',
|
||||
},
|
||||
/**
|
||||
*
|
||||
* 配置系统 naive-ui 主题色
|
||||
* 官网文档地址: <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>
|
||||
* @description
|
||||
* 配置系统 naive-ui 主题色。
|
||||
* 官网文档地址: <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>。
|
||||
*
|
||||
* 注意:
|
||||
* - appPrimaryColor common 配置优先级大于该配置
|
||||
*
|
||||
* 如果需要定制化整体组件样式, 配置示例
|
||||
* 具体自行查看官网, 还有模式更佳丰富的 peers 主题变量配置
|
||||
* 如果需要定制化整体组件样式,配置示例。
|
||||
* 具体自行查看官网,还有模式更佳丰富的 peers 主题变量配置。
|
||||
* 地址: <https://www.naiveui.com/zh-CN/dark/docs/customize-theme#%E4%BD%BF%E7%94%A8-peers-%E4%B8%BB%E9%A2%98%E5%8F%98%E9%87%8F>
|
||||
*
|
||||
* @example
|
||||
@ -46,17 +48,24 @@ export const APP_THEME: AppTheme = {
|
||||
* ```
|
||||
*/
|
||||
appNaiveUIThemeOverrides: {
|
||||
dark: {},
|
||||
light: {},
|
||||
},
|
||||
appNaiveUIThemeOverridesCommon: {
|
||||
dark: {},
|
||||
light: {},
|
||||
dark: {
|
||||
common: {
|
||||
borderRadius: '4px',
|
||||
baseColor: 'rgb(18, 18, 18)',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
common: {
|
||||
borderRadius: '4px',
|
||||
baseColor: 'rgb(255, 255, 255)',
|
||||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
*
|
||||
* 配置 echart 主题颜色
|
||||
* 约定配置时以:主题名称为文件名,其文件夹下两个主题风格的 json 文件。并且暗色主题必须为 xxx-dark.json
|
||||
* @description
|
||||
* 配置 echart 主题颜色。
|
||||
* 约定配置时以:主题名称为文件名,其文件夹下两个主题风格的 json 文件。并且暗色主题必须为 xxx-dark.json。
|
||||
*
|
||||
* [文档地址](https://xiaodaigua-ray.github.io/ray-template-doc/ray-template-docs/advanced/echart-themes.html)
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@ import type { Ref } from 'vue'
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 内容区域 ref 注册
|
||||
* 内容区域 shallowRef 注册
|
||||
* 可以控制内容区域当前滚动位置
|
||||
* 如果你需要在切换路由时候配置自定义滚动到某个视图区域时, 可以使用该属性提供的方法(scrollTo)
|
||||
*
|
||||
@ -16,12 +16,12 @@ import type { Ref } from 'vue'
|
||||
* })
|
||||
*/
|
||||
export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> =
|
||||
ref<LayoutInst | null>(null)
|
||||
shallowRef<LayoutInst | null>(null)
|
||||
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 侧边滚动栏滚动 ref 注册。
|
||||
* 侧边滚动栏滚动 shallowRef 注册。
|
||||
* 可以控制侧边滚动栏滚动位置。
|
||||
*
|
||||
* 请注意使用时机。建议使用 nextTick() 等待 dom 挂载后再执行该方法。
|
||||
@ -31,7 +31,7 @@ export const LAYOUT_CONTENT_REF: Readonly<Ref<LayoutInst | null>> =
|
||||
* })
|
||||
*/
|
||||
export const LAYOUT_SIDER_REF: Readonly<Ref<LayoutInst | null>> =
|
||||
ref<LayoutInst | null>(null)
|
||||
shallowRef<LayoutInst | null>(null)
|
||||
|
||||
export const SETUP_ROUTER_ACTION = {
|
||||
/** 是否启用路由切换时顶部加载条 */
|
||||
|
@ -16,7 +16,7 @@ import type {
|
||||
* 当然你也可以根据 request instance 来特殊处理, 这里暂时不做演示
|
||||
*/
|
||||
const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => {
|
||||
const token = getStorage<string>(APP_CATCH_KEY.token, 'localStorage')
|
||||
const token = getStorage<string | null>(APP_CATCH_KEY.token, 'localStorage')
|
||||
|
||||
if (ins.url) {
|
||||
// TODO: 根据 url 不同是否设置 token
|
||||
|
@ -40,14 +40,14 @@ function useRequest<
|
||||
fetchOptions: AppRawRequestConfig<Response>,
|
||||
option?: UseRequestOptions<Response, HookPlusParams, HookPlusPlugin>,
|
||||
) {
|
||||
const fc = () => {
|
||||
const fn = () => {
|
||||
const cb = request<Response>(fetchOptions)
|
||||
|
||||
return cb
|
||||
}
|
||||
|
||||
const hooks = useHookPlusRequest<Response, HookPlusParams>(
|
||||
fc,
|
||||
fn,
|
||||
Object.assign({}, option),
|
||||
)
|
||||
|
||||
|
@ -1,10 +1,3 @@
|
||||
/**
|
||||
*
|
||||
* 请求拦截器与响应拦截器
|
||||
* 如果有需要拓展拦截器, 请在 inject 目录下参照示例方法继续拓展
|
||||
* 该页面不应该做过多的改动与配置
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { AXIOS_CONFIG } from '@/app-config'
|
||||
import { useAxiosInterceptor } from '@/axios/utils/interceptor'
|
||||
@ -17,23 +10,33 @@ import {
|
||||
setupRequestErrorInterceptor,
|
||||
} from '@/axios/axios-interceptor/request'
|
||||
|
||||
import type { AxiosInstanceExpand } from './types'
|
||||
import type { AxiosInstanceExpand, RequestInterceptorConfig } from './types'
|
||||
|
||||
// 创建 axios 实例
|
||||
const server: AxiosInstanceExpand = axios.create(AXIOS_CONFIG)
|
||||
// 获取拦截器实例
|
||||
const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor()
|
||||
|
||||
// 请求拦截器
|
||||
server.interceptors.request.use(
|
||||
(request) => {
|
||||
createAxiosInstance(request, 'requestInstance') // 生成 request instance
|
||||
setupRequestInterceptor() // 初始化拦截器所有已注入方法
|
||||
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') // 执行拦截器所有已注入方法
|
||||
// 生成 request instance
|
||||
createAxiosInstance(
|
||||
request as RequestInterceptorConfig<unknown>,
|
||||
'requestInstance',
|
||||
)
|
||||
// 初始化拦截器所有已注入方法
|
||||
setupRequestInterceptor()
|
||||
// 执行拦截器所有已注入方法
|
||||
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok')
|
||||
|
||||
return request
|
||||
},
|
||||
(error) => {
|
||||
setupRequestErrorInterceptor() // 初始化拦截器所有已注入方法(错误状态)
|
||||
fetchError('requestError', error, 'implementRequestInterceptorErrorArray') // 执行所有已注入方法
|
||||
// 初始化拦截器所有已注入方法(错误状态)
|
||||
setupRequestErrorInterceptor()
|
||||
// 执行所有已注入方法
|
||||
fetchError('requestError', error, 'implementRequestInterceptorErrorArray')
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
@ -42,17 +45,22 @@ server.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
server.interceptors.response.use(
|
||||
(response) => {
|
||||
createAxiosInstance(response, 'responseInstance') // 创建响应实例
|
||||
setupResponseInterceptor() // 注入响应成功待执行队列
|
||||
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') // 执行响应成功拦截器
|
||||
// 创建响应实例
|
||||
createAxiosInstance(response, 'responseInstance')
|
||||
// 注入响应成功待执行队列
|
||||
setupResponseInterceptor()
|
||||
// 执行响应成功拦截器
|
||||
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok')
|
||||
|
||||
const { data } = response
|
||||
|
||||
return Promise.resolve(data)
|
||||
},
|
||||
(error) => {
|
||||
setupResponseErrorInterceptor() // 注入响应失败待执行队列
|
||||
fetchError('responseError', error, 'implementResponseInterceptorErrorArray') // 执行响应失败后拦截器
|
||||
// 注入响应失败待执行队列
|
||||
setupResponseErrorInterceptor()
|
||||
// 执行响应失败后拦截器
|
||||
fetchError('responseError', error, 'implementResponseInterceptorErrorArray')
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
|
@ -1,14 +1,3 @@
|
||||
/**
|
||||
*
|
||||
* axios 拦截器注入
|
||||
*
|
||||
* 请求拦截器、响应拦截器
|
||||
* 暴露启动方法调用所有已注册方法
|
||||
*
|
||||
* 该拦截器仅适合放置公共的 axios 拦截器操作, 并且采用队列形式管理请求拦截器的注入
|
||||
* 所以在使用的时候, 需要按照约定格式进行参数传递
|
||||
*/
|
||||
|
||||
import RequestCanceler from '@/axios/utils/RequestCanceler'
|
||||
import { getAppEnvironment } from '@/utils'
|
||||
|
||||
@ -24,31 +13,36 @@ import type {
|
||||
import type { AnyFC } from '@/types'
|
||||
import type { AxiosError } from 'axios'
|
||||
|
||||
/** 当前请求的实例 */
|
||||
// 当前请求的实例
|
||||
const axiosFetchInstance: AxiosFetchInstance = {
|
||||
requestInstance: null,
|
||||
responseInstance: null,
|
||||
}
|
||||
/** 请求失败返回值 */
|
||||
// 请求失败返回值
|
||||
const axiosFetchError: AxiosFetchError<AxiosError<unknown, unknown>> = {
|
||||
requestError: null,
|
||||
responseError: null,
|
||||
}
|
||||
/** 请求队列(区分 resolve 与 reject 状态) */
|
||||
// 请求队列(区分 resolve 与 reject 状态)
|
||||
const implement: ImplementQueue = {
|
||||
implementRequestInterceptorArray: [],
|
||||
implementResponseInterceptorArray: [],
|
||||
}
|
||||
// 请求失败队列
|
||||
const errorImplement: ErrorImplementQueue = {
|
||||
implementRequestInterceptorErrorArray: [],
|
||||
implementResponseInterceptorErrorArray: [],
|
||||
}
|
||||
|
||||
/** 取消器实例 */
|
||||
type ImplementKeys = keyof ImplementQueue
|
||||
|
||||
type ErrorImplementKeys = keyof ErrorImplementQueue
|
||||
|
||||
// 取消器实例
|
||||
export const axiosCanceler = new RequestCanceler()
|
||||
|
||||
export const useAxiosInterceptor = () => {
|
||||
/** 创建拦截器实例 */
|
||||
// 创建拦截器实例
|
||||
const createAxiosInstance = (
|
||||
instance: RequestInterceptorConfig | ResponseInterceptorConfig,
|
||||
instanceKey: keyof AxiosFetchInstance,
|
||||
@ -60,33 +54,33 @@ export const useAxiosInterceptor = () => {
|
||||
instance as ResponseInterceptorConfig)
|
||||
}
|
||||
|
||||
/** 获取当前实例 */
|
||||
// 获取当前实例
|
||||
const getAxiosInstance = (instanceKey: keyof AxiosFetchInstance) => {
|
||||
return axiosFetchInstance[instanceKey]
|
||||
}
|
||||
|
||||
/** 设置注入方法队列 */
|
||||
// 设置注入方法队列
|
||||
const setImplement = (
|
||||
key: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
key: ImplementKeys | ErrorImplementKeys,
|
||||
func: AnyFC[],
|
||||
fetchType: FetchType,
|
||||
) => {
|
||||
fetchType === 'ok'
|
||||
? (implement[key as keyof ImplementQueue] = func)
|
||||
: (errorImplement[key as keyof ErrorImplementQueue] = func)
|
||||
? (implement[key as ImplementKeys] = func)
|
||||
: (errorImplement[key as ErrorImplementKeys] = func)
|
||||
}
|
||||
|
||||
/** 获取队列中所有的所有拦截器方法 */
|
||||
// 获取队列中所有的所有拦截器方法
|
||||
const getImplement = (
|
||||
key: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
key: ImplementKeys | ErrorImplementKeys,
|
||||
fetchType: FetchType,
|
||||
): AnyFC[] => {
|
||||
return fetchType === 'ok'
|
||||
? implement[key as keyof ImplementQueue]
|
||||
: errorImplement[key as keyof ErrorImplementQueue]
|
||||
? implement[key as ImplementKeys]
|
||||
: errorImplement[key as ErrorImplementKeys]
|
||||
}
|
||||
|
||||
/** 队列执行器 */
|
||||
// 队列执行器
|
||||
const implementer = (funcs: AnyFC[], ...args: any[]) => {
|
||||
if (Array.isArray(funcs)) {
|
||||
funcs.forEach((curr) => {
|
||||
@ -97,16 +91,16 @@ export const useAxiosInterceptor = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求、响应前执行拦截器队列中的所有方法 */
|
||||
// 请求、响应前执行拦截器队列中的所有方法
|
||||
const beforeFetch = (
|
||||
key: keyof AxiosFetchInstance,
|
||||
implementKey: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
implementKey: ImplementKeys | ErrorImplementKeys,
|
||||
fetchType: FetchType,
|
||||
) => {
|
||||
const funcArr =
|
||||
fetchType === 'ok'
|
||||
? implement[implementKey as keyof ImplementQueue]
|
||||
: errorImplement[implementKey as keyof ErrorImplementQueue]
|
||||
? implement[implementKey as ImplementKeys]
|
||||
: errorImplement[implementKey as ErrorImplementKeys]
|
||||
const instance = getAxiosInstance(key)
|
||||
const { MODE } = getAppEnvironment()
|
||||
|
||||
@ -115,11 +109,11 @@ export const useAxiosInterceptor = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求、响应错误时执行队列中所有方法 */
|
||||
// 请求、响应错误时执行队列中所有方法
|
||||
const fetchError = (
|
||||
key: keyof AxiosFetchError,
|
||||
error: AxiosError<unknown, unknown>,
|
||||
errorImplementKey: keyof ErrorImplementQueue,
|
||||
errorImplementKey: ErrorImplementKeys,
|
||||
) => {
|
||||
axiosFetchError[key] = error
|
||||
|
||||
|
6
src/components/base/RChart/src/config.ts
Normal file
6
src/components/base/RChart/src/config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { ChartProviderOptions } from './hooks/useChartProvider'
|
||||
|
||||
export const USE_CHART_PROVIDER_KEY: InjectionKey<
|
||||
Partial<ChartProviderOptions>
|
||||
> = Symbol('USE_CHART_PROVIDER_KEY')
|
18
src/components/base/RChart/src/hooks/useChartProvider.ts
Normal file
18
src/components/base/RChart/src/hooks/useChartProvider.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { USE_CHART_PROVIDER_KEY } from '../config'
|
||||
|
||||
import type { ChartTheme } from '../types'
|
||||
|
||||
export interface ChartProviderOptions {
|
||||
theme: ChartTheme
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param 配置项
|
||||
*
|
||||
* @description
|
||||
* 全局 chart 配置项。
|
||||
*/
|
||||
export const useChartProvider = (options: Partial<ChartProviderOptions>) => {
|
||||
provide(USE_CHART_PROVIDER_KEY, options)
|
||||
}
|
@ -36,6 +36,7 @@ import {
|
||||
import { RMoreDropdown } from '@/components'
|
||||
import { useSettingGetters } from '@/store'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { USE_CHART_PROVIDER_KEY } from './config'
|
||||
|
||||
import type { WatchStopHandle } from 'vue'
|
||||
import type { AnyFC } from '@/types'
|
||||
@ -124,6 +125,7 @@ export default defineComponent({
|
||||
const __catch = {
|
||||
aria: props.showAria,
|
||||
}
|
||||
const chartProvideOptions = inject(USE_CHART_PROVIDER_KEY, {})
|
||||
|
||||
/**
|
||||
*
|
||||
@ -174,10 +176,19 @@ export default defineComponent({
|
||||
* 但是,如果未获取到 echartTheme 属性,则会使用默认样式。
|
||||
*/
|
||||
const updateChartTheme = () => {
|
||||
const { theme: providerTheme } = chartProvideOptions || {}
|
||||
|
||||
if (echartInstanceRef.value) {
|
||||
destroyChart()
|
||||
}
|
||||
|
||||
// 如果配置了全局配置主题,则忽略后面所有逻辑
|
||||
if (providerTheme) {
|
||||
renderChart(providerTheme)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (props.theme === 'default') {
|
||||
props.autoChangeTheme ? renderChart('dark') : renderChart('')
|
||||
|
||||
|
@ -81,6 +81,7 @@ export default defineComponent({
|
||||
yGap,
|
||||
collapsedRows,
|
||||
cssVars,
|
||||
actionSpan,
|
||||
bordered,
|
||||
} = this
|
||||
|
||||
@ -97,7 +98,11 @@ export default defineComponent({
|
||||
collapsedRows={collapsedRows}
|
||||
>
|
||||
{defaultSlot?.()}
|
||||
<NGridItem suffix class="ray-collapse-grid__suffix--btn">
|
||||
<NGridItem
|
||||
suffix
|
||||
class="ray-collapse-grid__suffix--btn"
|
||||
span={actionSpan}
|
||||
>
|
||||
<NFlex justify="end" align="center">
|
||||
{action?.()}
|
||||
{collapse
|
||||
|
@ -5,6 +5,17 @@ import type { CollapseToggleText, ActionAlignType } from './types'
|
||||
import type { AnyFC, MaybeArray } from '@/types'
|
||||
|
||||
const props = {
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 操作区域列数。
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
actionSpan: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
|
388
src/components/base/RDraggableCard/DraggableCard.tsx
Normal file
388
src/components/base/RDraggableCard/DraggableCard.tsx
Normal file
@ -0,0 +1,388 @@
|
||||
import './index.scss'
|
||||
|
||||
import { NCard } from 'naive-ui'
|
||||
import { Teleport, Transition } from 'vue'
|
||||
|
||||
import interact from 'interactjs'
|
||||
import { cardProps } from 'naive-ui'
|
||||
import { unrefElement, completeSize, queryElements } from '@/utils'
|
||||
|
||||
import type { VNode } from 'vue'
|
||||
import type { MaybeElement, MaybeRefOrGetter } from '@vueuse/core'
|
||||
import type { AnyFC } from '@/types'
|
||||
|
||||
type RestrictRectOptions = Parameters<typeof interact.modifiers.restrictRect>[0]
|
||||
|
||||
type Padding = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type DefaultPosition =
|
||||
| Padding
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right'
|
||||
| 'center'
|
||||
| 'top-center'
|
||||
| 'bottom-center'
|
||||
|
||||
const props = {
|
||||
...cardProps,
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 需要限制的区域位置。
|
||||
*
|
||||
* @default body
|
||||
*/
|
||||
restrictionElement: {
|
||||
type: [String, HTMLElement, Function, Object] as PropType<
|
||||
string | HTMLElement | (() => VNode) | MaybeRefOrGetter<MaybeElement>
|
||||
>,
|
||||
default: 'body',
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否启用拖拽。
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
dad: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 自定义限制拖拽范围。
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
restrictRectOptions: {
|
||||
type: Object as PropType<RestrictRectOptions>,
|
||||
default: void 0,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 默认位置。
|
||||
*
|
||||
* @default { x: 0, y: 0 }
|
||||
*/
|
||||
defaultPosition: {
|
||||
type: [Object, String] as PropType<DefaultPosition>,
|
||||
default: () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
}),
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 拖拽卡片宽度。
|
||||
*
|
||||
* @default 600
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 600,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 拖拽卡片 z-index。
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: void 0,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否启用动画。
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
animation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 默认的边距。
|
||||
* 设置该属性后,卡片首次出现的位置会根据该属性进行偏移。
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
padding: {
|
||||
type: Object as PropType<Padding>,
|
||||
default: void 0,
|
||||
},
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RDraggableCard',
|
||||
props,
|
||||
setup(props, { expose }) {
|
||||
const cardRef = useTemplateRef<HTMLElement>('cardRef')
|
||||
let interactInst: ReturnType<typeof interact> | null = null
|
||||
const position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
const CONTAINER_ID = 'r-draggable-card-container'
|
||||
const cssVars = computed(() => {
|
||||
return {
|
||||
'--r-draggable-card-width': completeSize(props.width),
|
||||
'--r-draggable-card-z-index': props.zIndex,
|
||||
}
|
||||
})
|
||||
let isSetup = false
|
||||
const cacheProps = {
|
||||
defaultPosition: props.defaultPosition,
|
||||
dad: props.dad,
|
||||
}
|
||||
|
||||
// 创建 DraggableCard 容器
|
||||
const createDraggableCardContainer = () => {
|
||||
if (!document.getElementById(CONTAINER_ID)) {
|
||||
const container = document.createElement('div')
|
||||
|
||||
container.id = CONTAINER_ID
|
||||
document.documentElement.appendChild(container)
|
||||
}
|
||||
}
|
||||
|
||||
createDraggableCardContainer()
|
||||
|
||||
// 获取 card, restrictionElement 的 dom 信息
|
||||
const getDom = () => {
|
||||
const card = unrefElement(cardRef)
|
||||
const re =
|
||||
typeof props.restrictionElement === 'string'
|
||||
? queryElements<HTMLElement>(props.restrictionElement)
|
||||
: props.restrictionElement
|
||||
let restrictionElement: HTMLElement | null = null
|
||||
|
||||
if (Array.isArray(re)) {
|
||||
restrictionElement = re[0]
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restrictionElement = unrefElement<HTMLElement>(re as any) as HTMLElement
|
||||
}
|
||||
|
||||
return {
|
||||
card,
|
||||
restrictionElement,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 container, card 的位置
|
||||
const getPosition = (containerRect: DOMRect, cardRect: DOMRect) => {
|
||||
const { defaultPosition, padding } = props
|
||||
const { x: paddingX = 0, y: paddingY = 0 } = padding ?? {}
|
||||
// 默认的 body restrictionElement 的偏移量是 0
|
||||
const {
|
||||
x: containerX,
|
||||
y: containerY,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
} = containerRect
|
||||
const { width: cardWidth, height: cardHeight } = cardRect
|
||||
|
||||
if (typeof defaultPosition === 'string') {
|
||||
switch (defaultPosition) {
|
||||
case 'top-center': {
|
||||
const cx1 = (containerWidth - cardWidth) / 2 + containerX
|
||||
const cy1 = paddingY + containerY
|
||||
const cx2 = paddingX + cx1
|
||||
const cy2 = cy1
|
||||
|
||||
return { x: cx2, y: cy2 }
|
||||
}
|
||||
|
||||
case 'bottom-center': {
|
||||
const cx1 = (containerWidth - cardWidth) / 2 + containerX
|
||||
const cy1 = containerHeight - cardHeight - paddingY + containerY
|
||||
const cx2 = paddingX + cx1
|
||||
const cy2 = cy1
|
||||
|
||||
return { x: cx2, y: cy2 }
|
||||
}
|
||||
|
||||
case 'center': {
|
||||
const cx1 = (containerWidth - cardWidth) / 2 + containerX
|
||||
const cy1 = (containerHeight - cardHeight) / 2 + containerY
|
||||
const cx2 = paddingX + cx1
|
||||
const cy2 = paddingY + cy1
|
||||
|
||||
return { x: cx2, y: cy2 }
|
||||
}
|
||||
|
||||
case 'top-left':
|
||||
return { x: paddingX + containerX, y: paddingY + containerY }
|
||||
|
||||
case 'top-right':
|
||||
return {
|
||||
x: containerWidth - cardWidth - paddingX + containerX,
|
||||
y: paddingY + containerY,
|
||||
}
|
||||
|
||||
case 'bottom-left':
|
||||
return {
|
||||
x: paddingX + containerX,
|
||||
y: containerHeight - cardHeight - paddingY + containerY,
|
||||
}
|
||||
|
||||
case 'bottom-right':
|
||||
return {
|
||||
x: containerWidth - cardWidth - paddingX + containerX,
|
||||
y: containerHeight - cardHeight - paddingY + containerY,
|
||||
}
|
||||
|
||||
// 默认为左上角
|
||||
default:
|
||||
return { x: paddingX + containerX, y: paddingY + containerY }
|
||||
}
|
||||
} else {
|
||||
const { x: defaultX, y: defaultY } = defaultPosition
|
||||
|
||||
return {
|
||||
x: defaultX + containerX + paddingX,
|
||||
y: defaultY + containerY + paddingY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化设置 card 的位置,并且根据配置启用拖拽
|
||||
const setupDraggable = () => {
|
||||
const { card, restrictionElement } = getDom()
|
||||
|
||||
if (!card) {
|
||||
return
|
||||
}
|
||||
|
||||
const restrictionRect = restrictionElement?.getBoundingClientRect()
|
||||
const cardHeader = card.querySelector('.n-card-header')
|
||||
const restrictRectOptions = Object.assign(
|
||||
{},
|
||||
{
|
||||
restriction: restrictionElement,
|
||||
endOnly: true,
|
||||
},
|
||||
props.restrictRectOptions,
|
||||
)
|
||||
|
||||
if (restrictionRect && !isSetup) {
|
||||
// 计算偏移位置
|
||||
const p = getPosition(restrictionRect, card.getBoundingClientRect())
|
||||
|
||||
// 设置初始位置
|
||||
card.style.transform = `translate(${p.x}px, ${p.y}px)`
|
||||
position.x = p.x
|
||||
position.y = p.y
|
||||
}
|
||||
|
||||
if (!props.dad) {
|
||||
return
|
||||
}
|
||||
|
||||
interactInst = interact(card)
|
||||
.draggable({
|
||||
inertia: true,
|
||||
autoScroll: true,
|
||||
allowFrom: cardHeader ? '.n-card-header' : '.n-card__content',
|
||||
modifiers: [interact.modifiers.restrictRect(restrictRectOptions)],
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
card.setAttribute('can-drag', 'true')
|
||||
|
||||
position.x += event.dx
|
||||
position.y += event.dy
|
||||
|
||||
card.style.transform = `translate(${position.x}px, ${position.y}px)`
|
||||
},
|
||||
},
|
||||
})
|
||||
.resizable(false)
|
||||
|
||||
isSetup = true
|
||||
}
|
||||
|
||||
// 取消拖拽
|
||||
const resetDraggable = () => {
|
||||
interactInst?.unset()
|
||||
|
||||
interactInst = null
|
||||
}
|
||||
|
||||
// 更新拖拽
|
||||
const refreshDraggableWhenPropsChange = (fn: AnyFC) => {
|
||||
isSetup = false
|
||||
|
||||
fn()
|
||||
setupDraggable()
|
||||
}
|
||||
|
||||
expose()
|
||||
|
||||
watchEffect(() => {
|
||||
props.dad ? setupDraggable() : resetDraggable()
|
||||
|
||||
if (props.defaultPosition !== cacheProps.defaultPosition) {
|
||||
refreshDraggableWhenPropsChange(() => {
|
||||
cacheProps.defaultPosition = props.defaultPosition
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupDraggable()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
cardRef,
|
||||
CONTAINER_ID,
|
||||
cssVars,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { $attrs, $slots, $props, CONTAINER_ID, cssVars, animation } = this
|
||||
|
||||
return (
|
||||
<Teleport to={`#${CONTAINER_ID}`}>
|
||||
{animation ? (
|
||||
<Transition name="draggable-card" appear mode="out-in">
|
||||
<NCard
|
||||
{...$attrs}
|
||||
{...$props}
|
||||
class="r-draggable-card"
|
||||
style={[cssVars]}
|
||||
ref="cardRef"
|
||||
>
|
||||
{{ ...$slots }}
|
||||
</NCard>
|
||||
</Transition>
|
||||
) : (
|
||||
<NCard
|
||||
{...$attrs}
|
||||
{...$props}
|
||||
class="r-draggable-card"
|
||||
style={[cssVars]}
|
||||
ref="cardRef"
|
||||
>
|
||||
{{ ...$slots }}
|
||||
</NCard>
|
||||
)}
|
||||
</Teleport>
|
||||
)
|
||||
},
|
||||
})
|
25
src/components/base/RDraggableCard/index.scss
Normal file
25
src/components/base/RDraggableCard/index.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.n-card.r-draggable-card {
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
width: var(--r-draggable-card-width);
|
||||
z-index: var(--r-draggable-card-z-index);
|
||||
}
|
||||
|
||||
#r-draggable-card-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
// draggable-card 的 Transition 样式
|
||||
.draggable-card-enter-active,
|
||||
.draggable-card-leave-active {
|
||||
transition: opacity 0.3s var(--r-bezier);
|
||||
}
|
||||
|
||||
.draggable-card-enter-from,
|
||||
.draggable-card-leave-to {
|
||||
opacity: 0;
|
||||
}
|
@ -36,11 +36,14 @@ import type { Recordable } from '@/types'
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
const useForm = <T extends Recordable, R extends RFormRules>(
|
||||
const useForm = <
|
||||
T extends Recordable = Recordable,
|
||||
R extends RFormRules = RFormRules,
|
||||
>(
|
||||
model?: T,
|
||||
rules?: R,
|
||||
) => {
|
||||
const formRef = ref<RFormInst>()
|
||||
const formRef = shallowRef<RFormInst>()
|
||||
|
||||
const register = (inst: RFormInst) => {
|
||||
if (inst) {
|
||||
|
@ -4,14 +4,12 @@ import { NModal } from 'naive-ui'
|
||||
|
||||
import props from './props'
|
||||
import { completeSize, uuid } from '@/utils'
|
||||
import { setupInteract } from './utils'
|
||||
import {
|
||||
FULLSCREEN_CARD_TYPE_CLASS,
|
||||
R_MODAL_CLASS,
|
||||
CSS_VARS_KEYS,
|
||||
} from './constant'
|
||||
|
||||
import type interact from 'interactjs'
|
||||
import type { ModalProps } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
@ -24,57 +22,11 @@ export default defineComponent({
|
||||
[CSS_VARS_KEYS['dialogWidth']]: completeSize(props.dialogWidth ?? 446),
|
||||
}))
|
||||
const uuidEl = uuid()
|
||||
let intractable: null | ReturnType<typeof interact>
|
||||
// 记录拖拽的位置
|
||||
const position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
// 当前是否为预设 card 类型并且设置了 fullscreen
|
||||
const isFullscreenCardType = computed(
|
||||
() => props.preset === 'card' && props.fullscreen,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(ndata) => {
|
||||
if (
|
||||
ndata &&
|
||||
props.dad &&
|
||||
(props.preset === 'card' || props.preset === 'dialog')
|
||||
) {
|
||||
nextTick(() => {
|
||||
const target = document.getElementById(uuidEl)
|
||||
|
||||
if (target) {
|
||||
setupInteract(target, {
|
||||
preset: props.preset,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dargCallback: (x, y) => {
|
||||
position.x = x
|
||||
position.y = y
|
||||
},
|
||||
}).then((res) => {
|
||||
intractable = res
|
||||
})
|
||||
}
|
||||
|
||||
if (props.memo && target) {
|
||||
target.style.transform = `translate(${position.x}px, ${position.y}px)`
|
||||
}
|
||||
})
|
||||
} else {
|
||||
intractable?.unset()
|
||||
|
||||
intractable = null
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
cssVars,
|
||||
isFullscreenCardType,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useModal as useNaiveModal, NScrollbar } from 'naive-ui'
|
||||
import { setupInteract } from '../utils'
|
||||
import { queryElements, setStyle, completeSize, setClass } from '@/utils'
|
||||
import { R_MODAL_CLASS, CSS_VARS_KEYS } from '../constant'
|
||||
|
||||
@ -21,10 +20,10 @@ const useModal = () => {
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
colorHover: 'rgba(0, 0, 0, 0)',
|
||||
},
|
||||
trigger: 'none',
|
||||
trigger: 'hover',
|
||||
style: {
|
||||
width: 'auto',
|
||||
height:
|
||||
maxHeight:
|
||||
'calc(var(--html-height) - 29px - var(--n-padding-bottom) - var(--n-padding-bottom) - var(--n-padding-top))',
|
||||
},
|
||||
},
|
||||
@ -35,7 +34,7 @@ const useModal = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const { preset, dad, fullscreen, width, cardWidth, dialogWidth } = options
|
||||
const { preset, fullscreen, width, cardWidth, dialogWidth } = options
|
||||
const modalReactive = naiveCreate({
|
||||
...rest,
|
||||
content: contentNode,
|
||||
@ -55,15 +54,6 @@ const useModal = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 是否启用拖拽
|
||||
if (dad) {
|
||||
setupInteract(modalElement, {
|
||||
preset,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// preset 为 card,fullscreen 为 true 时,最大化 modal
|
||||
if (fullscreen && preset === 'card') {
|
||||
setStyle(modalElement, {
|
||||
|
@ -4,6 +4,11 @@
|
||||
// 当设置全屏时,启用滚动
|
||||
& .n-card__content {
|
||||
overflow: scroll;
|
||||
max-height: calc(
|
||||
var(--html-height) - var(--n-padding-bottom) - var(--n-padding-bottom) - var(
|
||||
--n-padding-top
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,17 +3,6 @@ import type { PropType } from 'vue'
|
||||
|
||||
const props = {
|
||||
...modalProps,
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否记住上一次的位置。
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
memo: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
@ -58,18 +47,6 @@ const props = {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 446,
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否启用拖拽。
|
||||
* 当启用拖拽时,可以通过拖拽 header 部分控制模态框。
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
dad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
export default props
|
||||
|
@ -1,14 +1,6 @@
|
||||
import type { ModalOptions as NaiveModalOptions } from 'naive-ui'
|
||||
|
||||
export interface RModalProps extends NaiveModalOptions {
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否记住上一次的位置。
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
memo?: boolean
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
@ -41,13 +33,4 @@ export interface RModalProps extends NaiveModalOptions {
|
||||
* @default 446
|
||||
*/
|
||||
dialogWidth?: number | string
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 是否启用拖拽。
|
||||
* 当启用拖拽时,可以通过拖拽 header 部分控制模态框。
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
dad?: boolean
|
||||
}
|
||||
|
@ -1,101 +0,0 @@
|
||||
import interact from 'interactjs'
|
||||
|
||||
import type { ModalProps } from 'naive-ui'
|
||||
import type { RModalProps } from './types'
|
||||
|
||||
interface SetupDraggableOptions {
|
||||
scheduler?: (event: Interact.DragEvent) => void
|
||||
}
|
||||
|
||||
interface SetupInteractOptions {
|
||||
preset: ModalProps['preset']
|
||||
memo?: RModalProps['memo']
|
||||
x: number
|
||||
y: number
|
||||
dargCallback?: (x: number, y: number, event: Interact.DragEvent) => void
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bindModal modal 预设元素
|
||||
* @param preset 预设类型
|
||||
*
|
||||
* @description
|
||||
* 根据预设模态框设置拖拽效果
|
||||
* 但是该效果有且仅有 card, dialog 有效
|
||||
*
|
||||
* 默认添加 30ms 延迟,避免诡异问题
|
||||
*/
|
||||
export const setupDraggable = (
|
||||
bindModal: HTMLElement,
|
||||
preset: ModalProps['preset'],
|
||||
options?: SetupDraggableOptions,
|
||||
): Promise<ReturnType<typeof interact>> => {
|
||||
const { scheduler } = options ?? {}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const allowFromStr =
|
||||
preset === 'card' ? '.n-card-header__main' : '.n-dialog__title'
|
||||
|
||||
if (bindModal) {
|
||||
const dad = interact(bindModal)
|
||||
.draggable({
|
||||
inertia: true,
|
||||
autoScroll: true,
|
||||
allowFrom: allowFromStr,
|
||||
modifiers: [
|
||||
interact.modifiers.restrictRect({
|
||||
restriction: 'parent',
|
||||
endOnly: true,
|
||||
}),
|
||||
],
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
scheduler?.(event)
|
||||
},
|
||||
},
|
||||
})
|
||||
.resizable(false)
|
||||
|
||||
resolve(dad)
|
||||
}
|
||||
}, 30)
|
||||
})
|
||||
}
|
||||
|
||||
export const setupInteract = (
|
||||
target: HTMLElement | string,
|
||||
options: SetupInteractOptions,
|
||||
): Promise<ReturnType<typeof interact>> => {
|
||||
const _target =
|
||||
typeof target === 'string'
|
||||
? (document.querySelector(target) as HTMLElement)
|
||||
: target
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_target) {
|
||||
_target.setAttribute('can-drag', 'true')
|
||||
|
||||
const { preset, dargCallback } = options
|
||||
let { x, y } = options
|
||||
|
||||
setupDraggable(_target, preset, {
|
||||
scheduler: (event) => {
|
||||
const target = event.target
|
||||
|
||||
x += event.dx
|
||||
y += event.dy
|
||||
|
||||
target.style.transform = `translate(${x}px, ${y}px)`
|
||||
|
||||
dargCallback?.(x, y, event)
|
||||
},
|
||||
}).then((res) => {
|
||||
resolve(res)
|
||||
})
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
})
|
||||
}
|
@ -94,7 +94,6 @@ export default defineComponent({
|
||||
return (
|
||||
<NTabs
|
||||
{...($props as TabsProps)}
|
||||
ref="segmentRef"
|
||||
style={[cssVars]}
|
||||
class="r-segment"
|
||||
type="segment"
|
||||
|
@ -61,6 +61,23 @@ export default defineComponent({
|
||||
pick(props, 'striped', 'bordered'),
|
||||
),
|
||||
)
|
||||
// 默认设置 card header style
|
||||
const cardHeaderStyle = computed(() => {
|
||||
const { title, tool, cardProps } = props
|
||||
const { headerStyle = {} } = cardProps ?? {}
|
||||
|
||||
if (!title && !tool) {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
paddingTop: '0px',
|
||||
},
|
||||
headerStyle,
|
||||
)
|
||||
}
|
||||
|
||||
return headerStyle
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
@ -128,6 +145,7 @@ export default defineComponent({
|
||||
if (onUpdateColumns) {
|
||||
call(onUpdateColumns, options)
|
||||
}
|
||||
|
||||
if ($onUpdateColumns) {
|
||||
call($onUpdateColumns, options)
|
||||
}
|
||||
@ -230,6 +248,7 @@ export default defineComponent({
|
||||
tool,
|
||||
wrapperRef,
|
||||
propsPopselectValue,
|
||||
cardHeaderStyle,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
@ -242,6 +261,7 @@ export default defineComponent({
|
||||
uuidWrapper,
|
||||
privateReactive,
|
||||
propsPopselectValue,
|
||||
cardHeaderStyle,
|
||||
} = this
|
||||
const { class: className, ...restAttrs } = $attrs
|
||||
const { tool, combineRowProps, contextMenuSelect } = this
|
||||
@ -254,11 +274,12 @@ export default defineComponent({
|
||||
tableFlexHeight,
|
||||
cardProps,
|
||||
...restProps
|
||||
} = $props as ExtractPublicPropTypes<typeof props>
|
||||
} = $props
|
||||
const { headerStyle, ...restCardProps } = cardProps ?? {}
|
||||
|
||||
return (
|
||||
<NCard
|
||||
{...cardProps}
|
||||
{...restCardProps}
|
||||
{...{
|
||||
id: uuidWrapper,
|
||||
}}
|
||||
@ -266,6 +287,7 @@ export default defineComponent({
|
||||
ref="wrapperRef"
|
||||
bordered={wrapperBordered}
|
||||
class={className}
|
||||
style={cardHeaderStyle}
|
||||
>
|
||||
{{
|
||||
default: () => (
|
||||
|
@ -113,7 +113,6 @@ export default defineComponent({
|
||||
return true
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return cloneColumns.map((curr, idx) => {
|
||||
const { key, title, children, fixed, isResizable, ...args } =
|
||||
curr as C
|
||||
@ -170,7 +169,7 @@ export default defineComponent({
|
||||
}
|
||||
}) as C[]
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
set: () => {},
|
||||
})
|
||||
|
||||
|
@ -41,7 +41,7 @@ import type { PrintDomOptions } from '@/utils'
|
||||
* })
|
||||
*/
|
||||
const useTable = () => {
|
||||
const tableRef = ref<RTableInst>()
|
||||
const tableRef = shallowRef<RTableInst>()
|
||||
let extra = {} as TableProvider
|
||||
|
||||
const register: UseTableRegister = (inst, extraInfo) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import RCollapse from '../components/pro/RCollapse/Collapse'
|
||||
import RDraggableCard from '../components/base/RDraggableCard/DraggableCard'
|
||||
|
||||
// 导出所有自定义组件
|
||||
export * from './base/RChart'
|
||||
@ -14,7 +15,7 @@ export * from './base/RSegment'
|
||||
export * from './base/RBarcode'
|
||||
export * from '../components/pro/RTablePro'
|
||||
export * from './base/RFlow'
|
||||
export { RCollapse }
|
||||
export { RCollapse, RDraggableCard }
|
||||
|
||||
// 导出自定义组件类型
|
||||
export type * from './base/RChart/src/types'
|
||||
@ -32,3 +33,4 @@ export type {
|
||||
FlowGraphData,
|
||||
FlowOptions,
|
||||
} from './base/RFlow/src/types'
|
||||
export type { DefaultPosition } from './base/RDraggableCard/DraggableCard'
|
||||
|
@ -2,7 +2,7 @@ import { RTable } from '@/components'
|
||||
|
||||
import props from './props'
|
||||
import { useTable } from '@/components'
|
||||
import { call } from '@/utils'
|
||||
import { call, removeDuplicateKeys } from '@/utils'
|
||||
import { usePagination } from '@/hooks'
|
||||
|
||||
import type { TablePagination, TableRequestConfig, TableProInst } from './types'
|
||||
@ -62,7 +62,8 @@ export default defineComponent({
|
||||
const combineRequestParams = (extraConfig?: TableRequestConfig) => {
|
||||
const config = Object.assign({}, props.requestConfig, extraConfig)
|
||||
|
||||
const { params, formatRangeTime } = config
|
||||
const { formatRangeTime } = config
|
||||
let params = config.params || {}
|
||||
|
||||
// 转换时间范围,该功能仅支持 NDatePicker range 模式参数
|
||||
if (formatRangeTime?.length && params) {
|
||||
@ -84,6 +85,8 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
params = removeDuplicateKeys(params)
|
||||
|
||||
const requestParams = Object.assign({}, params, {
|
||||
page: getPage(),
|
||||
pageSize: getPageSize(),
|
||||
@ -139,6 +142,7 @@ export default defineComponent({
|
||||
filter,
|
||||
getCurrentTableRequestParams:
|
||||
combineRequestParams as TableProInst['getCurrentTableRequestParams'],
|
||||
resetTablePagination: resetPagination,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { Recordable } from '@/types'
|
||||
import type { TableProInst, TableRequestConfig } from '../types'
|
||||
import type {
|
||||
RTableInst,
|
||||
CsvOptionsType,
|
||||
FilterState,
|
||||
ScrollToOptions,
|
||||
@ -152,6 +151,14 @@ export const useTablePro = () => {
|
||||
// @ts-ignore
|
||||
getTableProInstance().getCurrentTableRequestParams.call(null, extraConfig)
|
||||
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 重置表格分页。
|
||||
*/
|
||||
const resetTablePagination = () =>
|
||||
getTableProInstance().resetTablePagination.call(null)
|
||||
|
||||
return [
|
||||
register,
|
||||
{
|
||||
@ -167,6 +174,7 @@ export const useTablePro = () => {
|
||||
runTableRequest,
|
||||
print,
|
||||
getCurrentTableRequestParams,
|
||||
resetTablePagination,
|
||||
},
|
||||
] as const
|
||||
}
|
||||
|
@ -72,4 +72,10 @@ export interface TableProInst extends Omit<RTableInst, 'getTableInstance'> {
|
||||
getCurrentTableRequestParams: <T = Recordable>(
|
||||
extraConfig?: TableRequestConfig<T>,
|
||||
) => TableRequestConfig<T>['params'] & Recordable
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 重置表格分页。
|
||||
*/
|
||||
resetTablePagination: () => void
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { DEFAULT_DAYJS_LOCAL } from '@/app-config'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import { getStorage } from '@/utils'
|
||||
import { APP_CATCH_KEY, DAYJS_LOCAL_MAP } from '@/app-config'
|
||||
|
||||
import type { SettingState } from '@/store/modules/setting/types'
|
||||
|
||||
/**
|
||||
*
|
||||
@ -10,5 +14,16 @@ import 'dayjs/locale/zh-cn'
|
||||
* 初始化 dayjs 的语言环境。
|
||||
*/
|
||||
export const setupDayjs = () => {
|
||||
dayjs.locale(DEFAULT_DAYJS_LOCAL)
|
||||
const { localeLanguage } = getStorage<SettingState>(
|
||||
APP_CATCH_KEY.appPiniaSettingStore,
|
||||
'localStorage',
|
||||
{
|
||||
defaultValue: {} as SettingState,
|
||||
},
|
||||
)
|
||||
const local =
|
||||
DAYJS_LOCAL_MAP[localeLanguage as keyof typeof DAYJS_LOCAL_MAP] ||
|
||||
DEFAULT_DAYJS_LOCAL
|
||||
|
||||
dayjs.locale(local)
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ export const combineDirective = <
|
||||
) => {
|
||||
const directives = Object.keys(directiveModules).reduce(
|
||||
(pre, curr) => {
|
||||
const fc = directiveModules[curr]?.default
|
||||
const fn = directiveModules[curr]?.default
|
||||
|
||||
if (typeof fc === 'function') {
|
||||
pre[curr as K] = fc
|
||||
if (typeof fn === 'function') {
|
||||
pre[curr as K] = fn
|
||||
|
||||
return pre
|
||||
} else {
|
||||
|
@ -23,10 +23,11 @@ export interface UseContextmenuCoordinateOptions {
|
||||
*
|
||||
* @param target 绑定元素
|
||||
*
|
||||
* 右键点击元素时,获取鼠标坐标。该方法结合 NDropdown 组件使用,可以实现右键菜单功能
|
||||
* @description
|
||||
* 右键点击元素时,获取鼠标坐标。该方法结合 NDropdown 组件使用,可以实现右键菜单功能。
|
||||
*
|
||||
* @example
|
||||
* const target = ref<HTMLElement | null>(null)
|
||||
* const target = useTemplateRef<HTMLElement | null>('target')
|
||||
* const { x, y, show, stop } = useContextmenuCoordinate(target)
|
||||
*
|
||||
* 如果需要手动停止右键菜单,可以调用 stop 方法
|
||||
@ -44,6 +45,13 @@ export const useContextmenuCoordinate = (
|
||||
const show = ref(false) // 是否显示右键菜单
|
||||
const { clickOutside } = options ?? {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value 是否显示右键菜单
|
||||
*
|
||||
* @description
|
||||
* 更新右键菜单的显示状态。
|
||||
*/
|
||||
const updateShow = (value: boolean) => {
|
||||
show.value = value
|
||||
}
|
||||
@ -52,8 +60,9 @@ export const useContextmenuCoordinate = (
|
||||
*
|
||||
* @param evt 鼠标事件
|
||||
*
|
||||
* 鼠标右键点击事件,并且阻止默认事件
|
||||
* 设置坐标后激活右键菜单
|
||||
* @description
|
||||
* 鼠标右键点击事件,并且阻止默认事件。
|
||||
* 设置坐标后激活右键菜单。
|
||||
*/
|
||||
const bindContextMenuEvent = (evt: Event) => {
|
||||
evt.preventDefault()
|
||||
@ -73,7 +82,8 @@ export const useContextmenuCoordinate = (
|
||||
if (clickOutside) {
|
||||
/**
|
||||
*
|
||||
* 自定义点击元素外部时的回调函数
|
||||
* @description
|
||||
* 自定义点击元素外部时的回调函数。
|
||||
*/
|
||||
onClickOutside(target as MaybeElementRef<MaybeElement>, (detectIframe) => {
|
||||
clickOutside(detectIframe)
|
||||
@ -82,7 +92,8 @@ export const useContextmenuCoordinate = (
|
||||
|
||||
/**
|
||||
*
|
||||
* 为传递 ref dom 绑定右键菜单事件
|
||||
* @description
|
||||
* 为传递 ref dom 绑定右键菜单事件。
|
||||
*/
|
||||
const cleanupContextmenu = useEventListener(
|
||||
target,
|
||||
@ -90,9 +101,11 @@ export const useContextmenuCoordinate = (
|
||||
bindContextMenuEvent,
|
||||
options,
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* 为传递 ref dom 绑定点击事件
|
||||
* @description
|
||||
* 为传递 ref dom 绑定点击事件。
|
||||
*/
|
||||
const cleanupClick = useEventListener(target, 'click', () => {
|
||||
updateShow(false)
|
||||
@ -100,8 +113,9 @@ export const useContextmenuCoordinate = (
|
||||
|
||||
/**
|
||||
*
|
||||
* 注销绑定的右键菜单事件、点击事件
|
||||
* 仅注销该方法绑定的事件
|
||||
* @description
|
||||
* 注销绑定的右键菜单事件、点击事件。
|
||||
* 仅注销该方法绑定的事件。
|
||||
*/
|
||||
const stop = () => {
|
||||
cleanupContextmenu()
|
||||
|
@ -10,17 +10,17 @@ export type CloseMenuTag = Key | MenuTagOptions
|
||||
/**
|
||||
*
|
||||
* @param target 标签页对象、索引、key
|
||||
* @param fc 触发函数
|
||||
* @param fn 触发函数
|
||||
*
|
||||
* 该方法用于统一获取目标标签页方法
|
||||
*/
|
||||
const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
|
||||
const normalMenuTagOption = (target: CloseMenuTag, fn: string) => {
|
||||
const { getMenuTagOptions } = useMenuGetters()
|
||||
|
||||
if (typeof target === 'number') {
|
||||
// 判断是否为 NaN
|
||||
if (isNaN(target)) {
|
||||
console.warn(`${fc}: The ${target} is NaN, expect number.`)
|
||||
console.warn(`${fn}: The ${target} is NaN, expect number.`)
|
||||
|
||||
return
|
||||
}
|
||||
@ -28,7 +28,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
|
||||
// 判断是否超出当前标签页列表最大长度或者是否为负数
|
||||
if (target > getMenuTagOptions.value.length || target < -1) {
|
||||
console.warn(
|
||||
`${fc}: The incoming index ${target} did not match the corresponding item.`,
|
||||
`${fn}: The incoming index ${target} did not match the corresponding item.`,
|
||||
)
|
||||
|
||||
return
|
||||
@ -50,7 +50,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
|
||||
index,
|
||||
}
|
||||
: console.warn(
|
||||
`${fc}: The incoming key ${target} did not match the corresponding item.`,
|
||||
`${fn}: The incoming key ${target} did not match the corresponding item.`,
|
||||
)
|
||||
} else {
|
||||
const { fullPath } = target
|
||||
@ -60,7 +60,7 @@ const normalMenuTagOption = (target: CloseMenuTag, fc: string) => {
|
||||
|
||||
if (index === -1) {
|
||||
console.warn(
|
||||
`${fc}: The incoming menuTag option ${target.fullPath} did not match the corresponding item.`,
|
||||
`${fn}: The incoming menuTag option ${target.fullPath} did not match the corresponding item.`,
|
||||
)
|
||||
|
||||
return
|
||||
@ -170,11 +170,19 @@ export function useSiderBar() {
|
||||
spliceMenTagOptions(index)
|
||||
|
||||
if (option.fullPath === getMenuKey.value) {
|
||||
const tag = getMenuTagOptions.value[index - 1]
|
||||
let i = checkCloseLeft(index)
|
||||
? index - 1
|
||||
: checkCloseRight(index)
|
||||
? index
|
||||
: index - 1
|
||||
|
||||
if (tag) {
|
||||
changeMenuModelValue(tag.fullPath, tag)
|
||||
if (i < 0) {
|
||||
i = 0
|
||||
}
|
||||
|
||||
const tag = getMenuTagOptions.value[i]
|
||||
|
||||
tag && changeMenuModelValue(tag.fullPath, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { useSettingActions, useSettingGetters } from '@/store'
|
||||
import { useI18n } from '@/hooks'
|
||||
import { APP_THEME } from '@/app-config'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { merge } from 'lodash-es'
|
||||
|
||||
export type ThemeLabel = 'Dark' | 'Light'
|
||||
|
||||
@ -33,17 +34,15 @@ const setThemeOverrides = (theme: boolean) => {
|
||||
updateSettingState(
|
||||
'primaryColorOverride',
|
||||
theme
|
||||
? Object.assign(
|
||||
? merge(
|
||||
{},
|
||||
getPrimaryColorOverride.value,
|
||||
APP_THEME.appNaiveUIThemeOverrides.dark,
|
||||
APP_THEME.appNaiveUIThemeOverridesCommon.dark,
|
||||
)
|
||||
: Object.assign(
|
||||
: merge(
|
||||
{},
|
||||
getPrimaryColorOverride.value,
|
||||
APP_THEME.appNaiveUIThemeOverrides.light,
|
||||
APP_THEME.appNaiveUIThemeOverridesCommon.light,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -42,8 +42,8 @@ export const useDayjs = () => {
|
||||
* 手动配置 dayjs 语言(国际化)。
|
||||
*
|
||||
* @example
|
||||
* locale('en')
|
||||
* locale('zh-cn')
|
||||
* locale('zh-CN')
|
||||
* locale('en-US')
|
||||
*/
|
||||
const locale = (key: LocalKey) => {
|
||||
const locale = DAYJS_LOCAL_MAP[key]
|
||||
|
@ -10,12 +10,32 @@ import {
|
||||
|
||||
import type { BasicTarget, TargetType } from '@/types'
|
||||
|
||||
// html-to-image 方法
|
||||
const domToImageMethods = {
|
||||
svg: toSvg,
|
||||
png: toPng,
|
||||
jpeg: toJpeg,
|
||||
blob: toBlob,
|
||||
pixelData: toPixelData,
|
||||
canvas: toCanvas,
|
||||
} as const
|
||||
|
||||
// 获取 html-to-image Options
|
||||
type HtmlToImageOptions = Partial<NonNullable<Parameters<typeof toPng>[1]>>
|
||||
|
||||
interface Options<
|
||||
// 获取 html-to-image 方法 keys
|
||||
type DomToImageMethodKeys = keyof typeof domToImageMethods
|
||||
|
||||
// 获取 html-to-image 方法返回值
|
||||
type DomToImageReturnType<ImageType extends DomToImageMethodKeys> = Awaited<
|
||||
ReturnType<(typeof domToImageMethods)[ImageType]>
|
||||
>
|
||||
|
||||
// 自定义拓展 Options
|
||||
type Options<
|
||||
T extends TargetType = Element,
|
||||
ImageType extends DomToImageMethodKeys = 'jpeg',
|
||||
> {
|
||||
ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
|
||||
> = {
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
@ -23,7 +43,7 @@ interface Options<
|
||||
*
|
||||
* @default jpeg
|
||||
*/
|
||||
imageType?: DomToImageReturnType<ImageType>
|
||||
imageType?: ImageType
|
||||
/**
|
||||
*
|
||||
* @param element current dom
|
||||
@ -66,22 +86,10 @@ interface Options<
|
||||
finally?: (element: T) => void
|
||||
}
|
||||
|
||||
export type UseDomToImageOptions<T extends TargetType = Element> = Options<T> &
|
||||
HtmlToImageOptions
|
||||
|
||||
export type DomToImageMethodKeys = keyof typeof domToImageMethods
|
||||
|
||||
type DomToImageReturnType<ImageType extends DomToImageMethodKeys = 'jpeg'> =
|
||||
Awaited<ReturnType<(typeof domToImageMethods)[ImageType]>>
|
||||
|
||||
const domToImageMethods = {
|
||||
svg: toSvg,
|
||||
png: toPng,
|
||||
jpeg: toJpeg,
|
||||
blob: toBlob,
|
||||
pixelData: toPixelData,
|
||||
canvas: toCanvas,
|
||||
} as const
|
||||
export type UseDomToImageOptions<
|
||||
T extends TargetType = Element,
|
||||
ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
|
||||
> = Options<T, ImageType> & HtmlToImageOptions
|
||||
|
||||
/**
|
||||
*
|
||||
@ -98,18 +106,24 @@ const domToImageMethods = {
|
||||
* 当然,你也可以不传递 imageType 参数,此时会使用 options.imageType,
|
||||
* 如果都未传递,则默认使用 jpeg。
|
||||
*
|
||||
* 如果希望 created 获得准确的类型,可以给 useDomToImage 手动声明第二个类型参数;
|
||||
* 或者就是在显式声明 options.imageType 参数。
|
||||
*
|
||||
* @example
|
||||
* const refDom = ref<HTMLElement>()
|
||||
* const { create, stop } = useDomToImage(refDom, {
|
||||
* const { create } = useDomToImage(refDom, {
|
||||
* beforeCreate: (element) => { ... },
|
||||
* created: (element, result) => { ... },
|
||||
* createdError: (error) => { ... },
|
||||
* finally: () => { ... },
|
||||
* })
|
||||
*/
|
||||
export const useDomToImage = <T extends HTMLElement>(
|
||||
export const useDomToImage = <
|
||||
T extends HTMLElement,
|
||||
ImageType extends DomToImageMethodKeys = DomToImageMethodKeys,
|
||||
>(
|
||||
target: BasicTarget<T>,
|
||||
options?: UseDomToImageOptions,
|
||||
options?: UseDomToImageOptions<T, ImageType>,
|
||||
) => {
|
||||
const {
|
||||
beforeCreate,
|
||||
@ -130,12 +144,12 @@ export const useDomToImage = <T extends HTMLElement>(
|
||||
if (!element) {
|
||||
createdError?.()
|
||||
|
||||
return reject('useDomToImage: element is undefined.')
|
||||
return reject(`[useDomToImage]: target element is undefined.`)
|
||||
}
|
||||
|
||||
const imageTypeKey = (imageType ??
|
||||
_imageType ??
|
||||
'jpeg') as keyof typeof domToImageMethods
|
||||
'jpeg') as DomToImageMethodKeys
|
||||
|
||||
domToImageMethods[imageTypeKey]?.(element, options)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -56,7 +56,7 @@ export interface UseElementFullscreenOptions {
|
||||
* @description
|
||||
* 手动设定 transition 过度效果。
|
||||
*
|
||||
* @default 'width 0.3s var(--r-bezier), height 0.3s var(--r-bezier)'
|
||||
* @default 'transform 0.3s var(--r-bezier)'
|
||||
*/
|
||||
transition?: string
|
||||
}
|
||||
@ -82,7 +82,7 @@ const styleElement = document.createElement('style')
|
||||
* <div ref="refDom" />
|
||||
* </template>
|
||||
* <script lang="ts" setup>
|
||||
* const refDom = ref<HTMLElement>()
|
||||
* const refDom = useTemplateRef<HTMLElement>('refDom')
|
||||
* const { enter, exit, toggleFullscreen } = useElementFullscreen(refDom, { UseElementFullscreenOptions })
|
||||
*
|
||||
* enter() // 进入全屏
|
||||
@ -101,7 +101,7 @@ export const useElementFullscreen = (
|
||||
exit: _exit,
|
||||
backgroundColor,
|
||||
zIndex,
|
||||
transition = 'all 0.3s var(--r-bezier)',
|
||||
transition = 'transform 0.3s var(--r-bezier)',
|
||||
} = options ?? {}
|
||||
let isSetup = false
|
||||
const catchBoundingClientRect: {
|
||||
@ -111,6 +111,8 @@ export const useElementFullscreen = (
|
||||
x: null,
|
||||
y: null,
|
||||
}
|
||||
// 使用 ref 来追踪状态
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const updateStyle = () => {
|
||||
const element = unrefElement(target) as HTMLElement | null
|
||||
@ -153,7 +155,7 @@ export const useElementFullscreen = (
|
||||
z-index: var(--element-fullscreen-z-index) !important;
|
||||
background-color: var(--element-fullscreen-background-color);
|
||||
}
|
||||
`
|
||||
`.trim()
|
||||
|
||||
styleElement.innerHTML = cssContent
|
||||
|
||||
@ -185,6 +187,7 @@ export const useElementFullscreen = (
|
||||
}
|
||||
|
||||
element.style.transition = transition
|
||||
isFullscreen.value = true
|
||||
|
||||
_enter?.()
|
||||
}
|
||||
@ -199,6 +202,8 @@ export const useElementFullscreen = (
|
||||
element.removeAttribute(ID_TAG)
|
||||
}
|
||||
|
||||
isFullscreen.value = false
|
||||
|
||||
_exit?.()
|
||||
}
|
||||
|
||||
@ -222,13 +227,15 @@ export const useElementFullscreen = (
|
||||
}
|
||||
|
||||
// 回滚 z-index 值,避免无限增加
|
||||
currentZIndex--
|
||||
currentZIndex = Math.max(999, currentZIndex - 1) // 防止 zIndex 小于初始值
|
||||
isFullscreen.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
enter,
|
||||
exit,
|
||||
toggleFullscreen,
|
||||
isFullscreen: readonly(isFullscreen), // 暴露只读状态
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,28 @@ type OmitKeys =
|
||||
| 'onUpdatePageSize'
|
||||
| 'onUpdate:page'
|
||||
| 'onUpdate:page-size'
|
||||
| 'onUpdate:pageSize'
|
||||
|
||||
export interface UsePaginationOptions extends Omit<PaginationProps, OmitKeys> {}
|
||||
export interface UsePaginationOptions extends Omit<PaginationProps, OmitKeys> {
|
||||
/**
|
||||
*
|
||||
* @param page 当前分页页码
|
||||
*
|
||||
* @description
|
||||
* 分页页码更新的回调函数。
|
||||
*/
|
||||
pageChange?: (page: number) => void
|
||||
/**
|
||||
*
|
||||
* @param pageSize 当前分页条数
|
||||
*
|
||||
* @description
|
||||
* 分页页数更新回调函数。
|
||||
*/
|
||||
pageSizeChange?: (pageSize: number) => void
|
||||
}
|
||||
|
||||
const defaultOptions: UsePaginationOptions = {
|
||||
const DEFAULT_OPTIONS: UsePaginationOptions = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
@ -36,33 +54,49 @@ export const usePagination = <T extends AnyFC>(
|
||||
callback?: T,
|
||||
options?: UsePaginationOptions,
|
||||
) => {
|
||||
const callbackRef = ref(callback)
|
||||
const omitOptions = omit(options, [
|
||||
'on-update:page',
|
||||
'on-update:page-size',
|
||||
'onUpdatePage',
|
||||
'onUpdatePageSize',
|
||||
'onUpdate:page',
|
||||
'onUpdate:page-size',
|
||||
])
|
||||
const methodsOptions = {
|
||||
// 配置合并
|
||||
const mergedOptions = computed(() => ({
|
||||
...DEFAULT_OPTIONS,
|
||||
...omit(options, [
|
||||
'on-update:page',
|
||||
'on-update:page-size',
|
||||
'onUpdatePage',
|
||||
'onUpdatePageSize',
|
||||
'onUpdate:page',
|
||||
'onUpdate:page-size',
|
||||
'onUpdate:pageSize',
|
||||
]),
|
||||
...paginationMethods,
|
||||
}))
|
||||
// 回调函数
|
||||
const callbackRef = shallowRef(callback)
|
||||
// 分页方法
|
||||
const paginationMethods = {
|
||||
onUpdatePage: (page: number) => {
|
||||
const { pageChange } = mergedOptions.value
|
||||
|
||||
paginationRef.value.page = page
|
||||
|
||||
callbackRef.value?.()
|
||||
pageChange?.(page)
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
const { pageSizeChange } = mergedOptions.value
|
||||
|
||||
paginationRef.value.pageSize = pageSize
|
||||
paginationRef.value.page = 1
|
||||
paginationRef.value.page = DEFAULT_OPTIONS.page
|
||||
|
||||
callbackRef.value?.()
|
||||
pageSizeChange?.(pageSize)
|
||||
},
|
||||
}
|
||||
const paginationRef = ref<PaginationProps>(
|
||||
Object.assign({}, defaultOptions, omitOptions, methodsOptions),
|
||||
)
|
||||
// 分页配置
|
||||
const paginationRef = ref<PaginationProps>(mergedOptions.value)
|
||||
|
||||
// 更新分页页数
|
||||
const updatePage = paginationRef.value.onUpdatePage as (page: number) => void
|
||||
|
||||
// 更新分页每页条数
|
||||
const updatePageSize = paginationRef.value.onUpdatePageSize as (
|
||||
pageSize: number,
|
||||
) => void
|
||||
@ -137,7 +171,7 @@ export const usePagination = <T extends AnyFC>(
|
||||
* @description
|
||||
* 获取回调函数。
|
||||
*/
|
||||
const getCallback = callback
|
||||
const getCallback = callbackRef.value as T
|
||||
|
||||
/**
|
||||
*
|
||||
@ -149,9 +183,8 @@ export const usePagination = <T extends AnyFC>(
|
||||
* @example
|
||||
* setCallback(() => {})
|
||||
*/
|
||||
const setCallback = (callback: T) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callbackRef.value = callback as any
|
||||
const setCallback = (callback: AnyFC) => {
|
||||
callbackRef.value = callback
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,8 +198,9 @@ export const usePagination = <T extends AnyFC>(
|
||||
const resetPagination = () => {
|
||||
const { pageSizes } = paginationRef.value
|
||||
|
||||
paginationRef.value.page = 1
|
||||
paginationRef.value.pageSize = (pageSizes?.[0] as number) || 10
|
||||
paginationRef.value.page = DEFAULT_OPTIONS.page
|
||||
paginationRef.value.pageSize =
|
||||
(pageSizes?.[0] as number) || DEFAULT_OPTIONS.pageSize
|
||||
}
|
||||
|
||||
effectDispose(() => {
|
||||
|
@ -16,16 +16,16 @@ export type UsePrintTarget<T = unknown> =
|
||||
|
||||
/**
|
||||
*
|
||||
* @param target ref dom
|
||||
* @param target useTemplateRef dom
|
||||
* @param options print-js options
|
||||
*
|
||||
* @see https://printjs.crabbly.com/
|
||||
*
|
||||
* @description
|
||||
* 拓展 print-js 的 usePrint 方法,允许 ref Dom 直接调用打印,其余的不变。
|
||||
* 拓展 print-js 的 usePrint 方法,允许 useTemplateRef Dom 直接调用打印,其余的不变。
|
||||
*
|
||||
* @example
|
||||
* const refDom = ref<HTMLElement>()
|
||||
* const refDom = useTemplateRef<HTMLElement>('refDom')
|
||||
*
|
||||
* const { print } = usePrint(refDom, {})
|
||||
*
|
||||
|
@ -6,7 +6,7 @@ import { RIcon } from '@/components'
|
||||
import { isValueType, renderNode } from '@/utils'
|
||||
import { useSettingGetters } from '@/store'
|
||||
|
||||
export const SIDER_BAR_LOGO = ref<HTMLElement>()
|
||||
export const SIDER_BAR_LOGO = shallowRef<HTMLElement>()
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SiderBarLogo',
|
||||
|
@ -7,6 +7,7 @@ import { LAYOUT_SIDER_REF } from '@/app-config'
|
||||
import { useDevice } from '@/hooks'
|
||||
import { getVariableToRefs, setVariable } from '@/global-variable'
|
||||
import { useMenuGetters, useMenuActions, useSettingGetters } from '@/store'
|
||||
import { positionSelectedMenuItem } from '@/utils'
|
||||
|
||||
import type { MenuInst } from 'naive-ui'
|
||||
import type { NaiveMenuOptions } from '@/types'
|
||||
@ -15,7 +16,8 @@ import type { AppMenuOption } from '@/types'
|
||||
export default defineComponent({
|
||||
name: 'AppMenu',
|
||||
setup() {
|
||||
const menuRef = ref<MenuInst | null>(null)
|
||||
// 这里使用 shallowRef 而不是 useTemplateRef 是因为在这里有一个特殊情况,会导致一个 readonly 的警告
|
||||
const menuRef = shallowRef<MenuInst | null>()
|
||||
|
||||
const { changeMenuModelValue, collapsedMenu, updateMenuState } =
|
||||
useMenuActions()
|
||||
@ -23,18 +25,19 @@ export default defineComponent({
|
||||
const { getMenuOptions, getCollapsed, getMenuKey } = useMenuGetters()
|
||||
const modelMenuKey = computed({
|
||||
get: () => {
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 禁用该 eslint 规则,因为在 computed 中使用了异步操作。
|
||||
* 该规则只是为了避免异步的 computed get 获取值出现问题;
|
||||
* 但是,在这里获取值的操作是同步行为,只是为了在获取值以后将对应菜单项展开;
|
||||
* 所以,这里不会出现异步获取值的问题,所以可以禁用该规则。
|
||||
*/
|
||||
// eslint-disable-next-line vue/no-async-in-computed-properties
|
||||
nextTick().then(() => {
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 禁用该 eslint 规则,因为在 computed 中使用了异步操作。
|
||||
* 该规则只是为了避免异步的 computed get 获取值出现问题;
|
||||
* 但是,在这里获取值的操作是同步行为,只是为了在获取值以后将对应菜单项展开;
|
||||
* 所以,这里不会出现异步获取值的问题,所以可以禁用该规则。
|
||||
*/
|
||||
setTimeout(() => {
|
||||
showMenuOption()
|
||||
})
|
||||
positionSelectedMenuItem()
|
||||
}, 300)
|
||||
|
||||
return getMenuKey.value
|
||||
},
|
||||
|
@ -25,43 +25,41 @@ $menuTagWrapperWidth: 76px;
|
||||
}
|
||||
|
||||
// 激活标签页关闭按钮样式
|
||||
.menu-tag {
|
||||
.menu-tag__btn {
|
||||
.menu-tag__btn-icon--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.menu-tag .menu-tag__btn {
|
||||
transition:
|
||||
color 0.3s var(--n-bezier),
|
||||
background-color 0.3s var(--n-bezier),
|
||||
opacity 0.3s var(--n-bezier),
|
||||
border-color 0.3s var(--n-bezier),
|
||||
width 0.3s var(--n-bezier);
|
||||
|
||||
.menu-tag__btn-icon {
|
||||
display: inline;
|
||||
margin-left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
.n-button__icon {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: var(--n-icon-size);
|
||||
transition: all 0.3s var(--r-bezier);
|
||||
margin-left: 0px;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.n-button__icon {
|
||||
opacity: 1;
|
||||
width: var(--n-icon-size);
|
||||
transition: all 0.3s var(--r-bezier);
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
|
||||
& .ray-icon {
|
||||
width: 11px !important;
|
||||
height: 11px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.menu-tag__btn-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
border-radius: 50%;
|
||||
padding: 1px;
|
||||
transition: all 0.3s var(--r-bezier);
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.ray-template--light {
|
||||
.menu-tag__btn-icon:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.ray-template--dark {
|
||||
.menu-tag__btn-icon:hover {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -517,6 +517,7 @@ export default defineComponent({
|
||||
}}
|
||||
size="small"
|
||||
focusable={false}
|
||||
iconPlacement="right"
|
||||
>
|
||||
{{
|
||||
default: () => (
|
||||
@ -533,16 +534,18 @@ export default defineComponent({
|
||||
},
|
||||
}}
|
||||
</span>
|
||||
<NIcon
|
||||
class="menu-tag__btn-icon"
|
||||
{...{
|
||||
onMousedown: closeCurrentMenuTag.bind(this, idx),
|
||||
}}
|
||||
>
|
||||
<RIcon name="close" size="14" />
|
||||
</NIcon>
|
||||
</>
|
||||
),
|
||||
icon: () => (
|
||||
<RIcon
|
||||
customClassName="menu-tag__btn-icon"
|
||||
name="close"
|
||||
size="15"
|
||||
{...{
|
||||
onMousedown: closeCurrentMenuTag.bind(this, idx),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
</NButton>
|
||||
))}
|
||||
|
@ -84,7 +84,14 @@ export default defineComponent({
|
||||
let searchElementIndex = 0
|
||||
// 缓存索引
|
||||
let preSearchElementIndex = searchElementIndex
|
||||
const { isTabletOrSmaller } = useDevice()
|
||||
const { isTabletOrSmaller } = useDevice({
|
||||
observer: (val) => {
|
||||
// 当处于小尺寸状态时,自动关闭搜索框
|
||||
if (val) {
|
||||
modelShow.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
const loading = ref(false)
|
||||
// 激活样式 class name
|
||||
const ACTIVE_CLASS = 'content-item--active'
|
||||
@ -218,10 +225,10 @@ export default defineComponent({
|
||||
const RenderPreIcon = (meta: AppRouteMeta) => {
|
||||
const { icon } = meta
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
if (typeof icon === 'string' && icon) {
|
||||
return <RIcon name={icon} size="24" />
|
||||
} else if (typeof icon === 'function') {
|
||||
return () => icon
|
||||
return <icon />
|
||||
} else {
|
||||
return <RIcon name="search" size="24" />
|
||||
}
|
||||
@ -309,22 +316,7 @@ export default defineComponent({
|
||||
</NFlex>
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
// 当处于小尺寸状态时,自动关闭搜索框
|
||||
if (isTabletOrSmaller.value) {
|
||||
modelShow.value = false
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener(
|
||||
window,
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
registerArouseKeyboard(e)
|
||||
registerChangeSearchElementIndex(e)
|
||||
},
|
||||
true,
|
||||
)
|
||||
useEventListener(window, 'keydown', registerArouseKeyboard)
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
@ -336,11 +328,16 @@ export default defineComponent({
|
||||
isTabletOrSmaller,
|
||||
SearchItem,
|
||||
loading,
|
||||
registerChangeSearchElementIndex,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { isTabletOrSmaller, searchOptions, loading } = this
|
||||
const { SearchItem, fuzzySearchMenuOptions } = this
|
||||
const {
|
||||
SearchItem,
|
||||
fuzzySearchMenuOptions,
|
||||
registerChangeSearchElementIndex,
|
||||
} = this
|
||||
|
||||
return isTabletOrSmaller ? (
|
||||
<div style="display: none;"></div>
|
||||
@ -350,7 +347,11 @@ export default defineComponent({
|
||||
transformOrigin="center"
|
||||
displayDirective="if"
|
||||
>
|
||||
<div class="global-search global-search--dark global-search--light">
|
||||
<div
|
||||
class="global-search global-search--dark global-search--light"
|
||||
tabindex="-1"
|
||||
onKeydown={registerChangeSearchElementIndex}
|
||||
>
|
||||
<div class="global-search__wrapper">
|
||||
<NCard
|
||||
class="global-search__card"
|
||||
|
@ -2,83 +2,6 @@ import type { SettingState } from '@/store/modules/setting/types'
|
||||
import type { InjectionKey, Reactive } from 'vue'
|
||||
import type { DebouncedFunc } from 'lodash-es'
|
||||
|
||||
type OmitKeys =
|
||||
| 'contentTransition'
|
||||
| 'watermarkSwitch'
|
||||
| 'keepAliveConfig'
|
||||
| 'menuConfig'
|
||||
| 'menuTagSwitch'
|
||||
| 'breadcrumbSwitch'
|
||||
| 'copyrightSwitch'
|
||||
| 'drawerPlacement'
|
||||
| 'colorWeakness'
|
||||
| 'watermarkConfig'
|
||||
| 'dynamicDocumentTitle'
|
||||
|
||||
interface Config extends Pick<SettingState, OmitKeys> {}
|
||||
|
||||
// SettingDrawer 默认配置系统配置项
|
||||
const defaultSettingConfig: Readonly<Partial<Config>> = {
|
||||
contentTransition: 'scale',
|
||||
watermarkSwitch: false,
|
||||
keepAliveConfig: {
|
||||
maxKeepAliveLength: 10,
|
||||
setupKeepAlive: true,
|
||||
keepAliveExclude: [],
|
||||
},
|
||||
menuConfig: {
|
||||
collapsedWidth: 64,
|
||||
collapsedMode: 'width',
|
||||
collapsedIconSize: 16,
|
||||
collapsedIndent: 24,
|
||||
accordion: false,
|
||||
menuSiderBarLogo: true,
|
||||
iconSize: 16,
|
||||
menuWidth: 272,
|
||||
inverted: false,
|
||||
nativeScrollbar: false,
|
||||
},
|
||||
menuTagSwitch: true,
|
||||
breadcrumbSwitch: true,
|
||||
copyrightSwitch: true,
|
||||
drawerPlacement: 'right',
|
||||
colorWeakness: false,
|
||||
watermarkConfig: {
|
||||
content: 'Trying be better~',
|
||||
fontSize: 16,
|
||||
lineHeight: 16,
|
||||
width: 384,
|
||||
height: 384,
|
||||
xOffset: 12,
|
||||
yOffset: 60,
|
||||
rotate: -15,
|
||||
xGap: 0,
|
||||
yGap: 0,
|
||||
cross: true,
|
||||
},
|
||||
dynamicDocumentTitle: true,
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 获取默认配置。
|
||||
*/
|
||||
export const getDefaultSettingConfig = (): Partial<Config> => {
|
||||
return defaultSettingConfig
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config 配置项
|
||||
*
|
||||
* @description
|
||||
* 更新默认配置。
|
||||
*/
|
||||
export const updateDefaultSettingConfig = (config: Partial<SettingState>) => {
|
||||
Object.assign(defaultSettingConfig, config)
|
||||
}
|
||||
|
||||
interface SettingDrawerInjectKey extends SettingState {
|
||||
throttleSetupAppMenu: DebouncedFunc<() => Promise<void>>
|
||||
}
|
||||
|
@ -15,10 +15,11 @@ import SegmentViewsWatermark from './segment-views/Watermark'
|
||||
import SegmentViewsCustomMenu from './segment-views/CustomMenu'
|
||||
|
||||
import { useSettingGetters, useSettingActions, useMenuActions } from '@/store'
|
||||
import { getDefaultSettingConfig, SETTING_DRAWER_INJECT_KEY } from './constant'
|
||||
import { forIn, throttle } from 'lodash-es'
|
||||
import { SETTING_DRAWER_INJECT_KEY } from './constant'
|
||||
import { cloneDeep, forIn, throttle } from 'lodash-es'
|
||||
import { drawerProps } from 'naive-ui'
|
||||
import { useModal } from '@/components'
|
||||
import { getDefaultSettingConfig } from '@/store/modules/setting/constant'
|
||||
|
||||
import type { SettingState } from '@/store/modules/setting/types'
|
||||
|
||||
@ -70,11 +71,13 @@ export default defineComponent({
|
||||
positiveText: '确认初始化',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
forIn(getDefaultSettingConfig(), (value, key) => {
|
||||
forIn(cloneDeep(getDefaultSettingConfig()), (value, key) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
modelReactive[key] = value
|
||||
|
||||
console.log(value, key)
|
||||
|
||||
updateSettingState(key as keyof SettingState, value)
|
||||
})
|
||||
|
||||
|
@ -37,21 +37,28 @@ import type { VNode } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'AppSiderBar',
|
||||
setup() {
|
||||
// 获取 setting 相关值
|
||||
const { updateLocale, updateSettingState } = useSettingActions()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 获取全屏相关方法
|
||||
const [isFullscreen, { toggleFullscreen, isEnabled }] = useFullscreen(
|
||||
document.getElementsByTagName('html')[0],
|
||||
)
|
||||
// 获取设置相关方法
|
||||
const { getDrawerPlacement, getBreadcrumbSwitch } = useSettingGetters()
|
||||
const showSettings = ref(false) // 是否显示设置抽屉
|
||||
const globalSearchShown = ref(false) // 是否展示全局搜索
|
||||
// 是否显示设置抽屉
|
||||
const showSettings = ref(false)
|
||||
// 是否展示全局搜索
|
||||
const globalSearchShown = ref(false)
|
||||
// 当前是否为平板或者更小的设备
|
||||
const { isTabletOrSmaller } = useDevice()
|
||||
// 获取全局 drawer 的值
|
||||
const globalDrawerValue = getVariableToRefs('globalDrawerValue')
|
||||
|
||||
/**
|
||||
*
|
||||
* 顶部左边操作栏
|
||||
* @description
|
||||
* 顶部左边操作栏。
|
||||
*/
|
||||
const leftIconOptions = computed(() =>
|
||||
createLeftIconOptions({
|
||||
@ -61,7 +68,8 @@ export default defineComponent({
|
||||
)
|
||||
/**
|
||||
*
|
||||
* 顶部右边提示框操作栏
|
||||
* @description
|
||||
* 顶部右边提示框操作栏。
|
||||
*/
|
||||
const rightTooltipIconOptions = computed(() =>
|
||||
createRightIconOptions({
|
||||
|
@ -79,8 +79,6 @@ export default defineComponent({
|
||||
layoutContentMaximize
|
||||
? 'r-layout-full__viewer-content--maximize'
|
||||
: null,
|
||||
'r-layout-full__viewer-content--maximize--light',
|
||||
'r-layout-full__viewer-content--maximize--dark',
|
||||
]}
|
||||
>
|
||||
{layoutContentMaximize ? (
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
HeaderWrapper,
|
||||
FeatureWrapper,
|
||||
} from './default'
|
||||
import UnlockScreen from '@/app-components/app/AppLockScreen/components/UnlockScreen'
|
||||
|
||||
import { LAYOUT_CONTENT_REF } from '@/app-config'
|
||||
import { layoutCssVars } from '@/layout/layout-css-vars'
|
||||
@ -28,9 +29,9 @@ import { useSettingGetters } from '@/store'
|
||||
export default defineComponent({
|
||||
name: 'RLayout',
|
||||
setup() {
|
||||
const layoutSiderBarRef = ref<HTMLElement>() // 顶部操作栏 ref
|
||||
const layoutMenuTagRef = ref<HTMLElement>() // 标签页 ref
|
||||
const layoutFooterRef = ref<HTMLElement>() // 底部版权 ref
|
||||
const layoutSiderBarRef = shallowRef<HTMLElement>() // 顶部操作栏 shallowRef
|
||||
const layoutMenuTagRef = shallowRef<HTMLElement>() // 标签页 shallowRef
|
||||
const layoutFooterRef = shallowRef<HTMLElement>() // 底部版权 shallowRef
|
||||
|
||||
const { getMenuTagSwitch, getCopyrightSwitch } = useSettingGetters()
|
||||
const { getLockAppScreen } = useAppLockScreen()
|
||||
@ -70,6 +71,8 @@ export default defineComponent({
|
||||
{getCopyrightSwitch ? <FooterWrapper ref="layoutFooterRef" /> : null}
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
) : null
|
||||
) : (
|
||||
<UnlockScreen />
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -29,5 +29,6 @@
|
||||
"TemplateHooks": "Template Api",
|
||||
"scrollReveal": "Scroll Reveal",
|
||||
"TablePro": "Table Pro",
|
||||
"Flow": "Flow"
|
||||
"Flow": "Flow",
|
||||
"DraggableCard": "Draggable Card"
|
||||
}
|
||||
|
@ -29,5 +29,6 @@
|
||||
"TemplateHooks": "模板内置 Api",
|
||||
"scrollReveal": "滚动动画",
|
||||
"TablePro": "高级表格",
|
||||
"Flow": "流程图"
|
||||
"Flow": "流程图",
|
||||
"DraggableCard": "拖拽卡片"
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import type { AppRouteRecordRaw } from '@/router/types'
|
||||
|
||||
const barcode: AppRouteRecordRaw = {
|
||||
path: 'barcode',
|
||||
component: () => import('@/views/demo/BarcodeDemo'),
|
||||
component: () => import('@/views/demo/barcode-demo'),
|
||||
meta: {
|
||||
i18nKey: t('menu.Barcode'),
|
||||
icon: 'other',
|
||||
|
19
src/router/modules/demo/draggable-card.ts
Normal file
19
src/router/modules/demo/draggable-card.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { t } from '@/hooks/web/useI18n'
|
||||
import { LAYOUT } from '@/router/constant'
|
||||
|
||||
import type { AppRouteRecordRaw } from '@/router/types'
|
||||
|
||||
const r: AppRouteRecordRaw = {
|
||||
path: '/draggable-card',
|
||||
component: () => import('@/views/demo/draggable-card'),
|
||||
meta: {
|
||||
i18nKey: t('menu.DraggableCard'),
|
||||
icon: 'other',
|
||||
order: 2,
|
||||
extra: {
|
||||
label: 'drag',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default r
|
@ -5,7 +5,7 @@ import type { AppRouteRecordRaw } from '@/router/types'
|
||||
|
||||
const r: AppRouteRecordRaw = {
|
||||
path: '/flow',
|
||||
component: () => import('@/views/demo/Flow'),
|
||||
component: () => import('@/views/demo/flow-demo'),
|
||||
meta: {
|
||||
i18nKey: t('menu.Flow'),
|
||||
icon: 'other',
|
@ -5,7 +5,7 @@ import type { AppRouteRecordRaw } from '@/router/types'
|
||||
|
||||
const r: AppRouteRecordRaw = {
|
||||
path: '/table-pro',
|
||||
component: () => import('@/views/demo/TablePro'),
|
||||
component: () => import('@/views/demo/table-pro-demo'),
|
||||
meta: {
|
||||
i18nKey: t('menu.TablePro'),
|
||||
icon: 'other',
|
||||
|
45
src/router/utils/can-skip-route.ts
Normal file
45
src/router/utils/can-skip-route.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { AppRouteRecordRaw } from '../types'
|
||||
import type { AppMenuOption } from '@/types'
|
||||
|
||||
type LikeTemplateRouteOption = AppRouteRecordRaw | AppMenuOption
|
||||
|
||||
type SkipReturnType = LikeTemplateRouteOption | undefined
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options 标准路由表或者系统菜单列表
|
||||
*
|
||||
* @description
|
||||
* 获取第一个可以跳转的路由,如果没有可以跳转的路由,返回 undefined。
|
||||
* 当你需要动态的获取可跳转路由的时候,可以使用该方法。
|
||||
*
|
||||
* @example
|
||||
* const canSkip1 = canSkipRoute(menuOptions)
|
||||
* const canSkip2 = canSkipRoute(routeOptions)
|
||||
*/
|
||||
export const canSkipRoute = (
|
||||
options: LikeTemplateRouteOption[],
|
||||
): SkipReturnType => {
|
||||
if (!Array.isArray(options)) {
|
||||
return void 0
|
||||
}
|
||||
|
||||
const skip = (skipOptions: LikeTemplateRouteOption[]): SkipReturnType => {
|
||||
for (const curr of skipOptions) {
|
||||
if (
|
||||
(!curr.children || curr.children.length === 0) &&
|
||||
!curr.meta.windowOpen
|
||||
) {
|
||||
return curr
|
||||
} else {
|
||||
const found = skip(curr.children || [])
|
||||
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skip(options)
|
||||
}
|
@ -8,3 +8,4 @@ export * from './setup-router-loading-bar'
|
||||
export * from './valid-menu-item-show'
|
||||
export * from './valid-role'
|
||||
export * from './vue-router-register'
|
||||
export { canSkipRoute } from './can-skip-route'
|
||||
|
@ -16,10 +16,6 @@ export const orderRoutes = (routes: AppRouteRecordRaw[]) => {
|
||||
const currOrder = curr.meta?.order ?? 1
|
||||
const nextOrder = next.meta?.order ?? 0
|
||||
|
||||
if (typeof currOrder !== 'number' || typeof nextOrder !== 'number') {
|
||||
throw new TypeError('orderRoutes error: order must be a number!')
|
||||
}
|
||||
|
||||
if (currOrder === nextOrder) {
|
||||
// 如果两个路由的 order 值相同,则按照路由名进行排序
|
||||
return curr.name
|
||||
|
@ -5,7 +5,8 @@ export const useKeepAliveGetters = () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取当前可缓存项 name
|
||||
* @@description
|
||||
* 获取当前可缓存项 name。
|
||||
*/
|
||||
const getKeepAliveInclude = computed(() => variable.keepAliveInclude)
|
||||
|
||||
|
@ -1,57 +1,49 @@
|
||||
import { piniaMenuStore } from '../modules/menu'
|
||||
import { useAppRoot } from '@/hooks'
|
||||
|
||||
export const useMenuGetters = () => {
|
||||
const variable = piniaMenuStore()
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取菜单列表
|
||||
* @description
|
||||
* 获取菜单列表。
|
||||
*/
|
||||
const getMenuOptions = computed(() => variable.options)
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取面包屑列表
|
||||
* @description
|
||||
* 获取面包屑列表。
|
||||
*/
|
||||
const getBreadcrumbOptions = computed(() => variable.breadcrumbOptions)
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取菜单当前 key
|
||||
* @description
|
||||
* 获取菜单当前 key。
|
||||
*/
|
||||
const getMenuKey = computed(() => variable.menuKey)
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取菜单标签列表
|
||||
* @description
|
||||
* 获取菜单标签列表。
|
||||
*/
|
||||
const getMenuTagOptions = computed(() => {
|
||||
const { getRootPath } = useAppRoot()
|
||||
|
||||
return variable.menuTagOptions.map((curr, _idx, currentArray) => {
|
||||
if (curr.key === getMenuKey.value && curr.key !== getRootPath.value) {
|
||||
curr.closeable = true
|
||||
} else {
|
||||
curr.closeable = false
|
||||
}
|
||||
|
||||
if (curr.key === getRootPath.value) {
|
||||
curr.closeable = false
|
||||
}
|
||||
|
||||
if (currentArray.length <= 1) {
|
||||
curr.closeable = false
|
||||
}
|
||||
|
||||
return curr
|
||||
})
|
||||
return variable.menuTagOptions
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取当前菜单项
|
||||
* @description
|
||||
* 获取当前菜单项。
|
||||
*/
|
||||
const getCurrentMenuOption = computed(() => variable.currentMenuOption)
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 获取是否折叠
|
||||
* @description
|
||||
* 获取是否折叠。
|
||||
*/
|
||||
const getCollapsed = computed(() => variable.collapsed)
|
||||
|
||||
|
@ -1,30 +1,14 @@
|
||||
/**
|
||||
*
|
||||
* 该文件为 menu 菜单 pinia store
|
||||
*
|
||||
* 说明:
|
||||
* - BreadcrumbMenu、TagMenu、Menu 统一管理
|
||||
* - BreadcrumbMenu、TagMenu、Menu 属性值重度依赖 vue-router routers, 所以需要按照该项目约定方法进行配置
|
||||
*
|
||||
* 缓存(sessionStorage):
|
||||
* - breadcrumbOptions
|
||||
* - menuKey
|
||||
* - menuTagOptions
|
||||
*
|
||||
* 备注:
|
||||
* - parseAndFindMatchingNodes: 如果需要反向查找完整的父子节点,可以使用该方法
|
||||
*/
|
||||
|
||||
import { NEllipsis } from 'naive-ui'
|
||||
|
||||
import { setStorage, equalRouterPath, updateObjectValue } from '@/utils'
|
||||
import { validRole, validMenuItemShow } from '@/router/utils'
|
||||
import { validRole, validMenuItemShow, canSkipRoute } from '@/router/utils'
|
||||
import {
|
||||
parseAndFindMatchingNodes,
|
||||
updateDocumentTitle,
|
||||
createMenuIcon,
|
||||
getCatchMenuKey,
|
||||
createMenuExtra,
|
||||
getCatchMenuTagOptions,
|
||||
} from './utils'
|
||||
import { useI18n } from '@/hooks'
|
||||
import { getAppRawRoutes } from '@/router/app-route-modules'
|
||||
@ -33,10 +17,9 @@ import { APP_CATCH_KEY } from '@/app-config'
|
||||
import { pick } from 'lodash-es'
|
||||
import { pickRouteRecordNormalizedConstant } from './constant'
|
||||
|
||||
import type { AppMenuOption, MenuTagOptions } from '@/types'
|
||||
import type { AppMenuOption, MenuTagOptions, AnyFC } from '@/types'
|
||||
import type { MenuState } from '@/store/modules/menu/types'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import type { UpdateMenuState } from './types'
|
||||
|
||||
let cachePreNormal: AppMenuOption | undefined = void 0
|
||||
|
||||
@ -91,7 +74,7 @@ export const piniaMenuStore = defineStore(
|
||||
menuKey: getCatchMenuKey(), // 当前菜单 `key`
|
||||
options: [], // 菜单列表
|
||||
collapsed: false, // 是否折叠菜单
|
||||
menuTagOptions: [], // tag 标签菜单
|
||||
menuTagOptions: getCatchMenuTagOptions(), // tag 标签菜单
|
||||
breadcrumbOptions: [], // 面包屑菜单
|
||||
currentMenuOption: null, // 当前激活菜单项
|
||||
})
|
||||
@ -101,12 +84,17 @@ export const piniaMenuStore = defineStore(
|
||||
*
|
||||
* @param key menu state key
|
||||
* @param value updated value
|
||||
* @param cb callback function
|
||||
*
|
||||
* @description
|
||||
* 更新 menu state 指定 key 的值。
|
||||
*/
|
||||
const updateMenuState: UpdateMenuState = (key, value, cb) => {
|
||||
updateObjectValue(menuState, key, value, cb)
|
||||
const updateMenuState = <T extends keyof MenuState>(
|
||||
key: T,
|
||||
value: Partial<MenuState[T]>,
|
||||
cb?: AnyFC,
|
||||
) => {
|
||||
updateObjectValue(menuState, key, value as MenuState[T], cb)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,11 +118,7 @@ export const piniaMenuStore = defineStore(
|
||||
|
||||
// 设置 label, i18nKey 优先级最高
|
||||
const label = computed(() => (i18nKey ? t(`${i18nKey}`) : noLocalTitle))
|
||||
/**
|
||||
*
|
||||
* 拼装菜单项
|
||||
* 容错处理,兼容以前版本 key 选取为 path 的情况
|
||||
*/
|
||||
// 拼装菜单项,容错处理,兼容以前版本 key 选取为 path 的情况
|
||||
const route = {
|
||||
...option,
|
||||
key: option.fullPath,
|
||||
@ -150,10 +134,12 @@ export const piniaMenuStore = defineStore(
|
||||
extra: createMenuExtra(option),
|
||||
})
|
||||
|
||||
// 如果当前菜单项与缓存菜单项相同,则更新当前菜单项
|
||||
if (option.fullPath === getCatchMenuKey()) {
|
||||
menuState.currentMenuOption = attr
|
||||
}
|
||||
|
||||
// 验证菜单项是否显示
|
||||
attr.show = validMenuItemShow(attr)
|
||||
|
||||
return attr
|
||||
@ -169,21 +155,20 @@ export const piniaMenuStore = defineStore(
|
||||
* 如果识别到为平级模式,则会自动追加一层面包屑。
|
||||
*/
|
||||
const setBreadcrumbOptions = (key: string | number) => {
|
||||
menuState.breadcrumbOptions = parseAndFindMatchingNodes(
|
||||
menuState.options,
|
||||
'fullPath',
|
||||
key,
|
||||
menuState.breadcrumbOptions = unref(
|
||||
parseAndFindMatchingNodes(menuState.options, 'fullPath', key),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options menu tag options
|
||||
* @param isAppend is append
|
||||
* @param options menu tag option(s)
|
||||
* @param isAppend true: 追加操作(push), false: 覆盖操作
|
||||
*
|
||||
* @description
|
||||
* 设置标签菜单。
|
||||
* true: 追加操作(push),false: 覆盖操作。
|
||||
* 设置标签页。
|
||||
* 如果传递的 options 为数组,则进行追加操作。
|
||||
* 如果传递的 options 为单个对象,则进行覆盖操作。
|
||||
*/
|
||||
const setMenuTagOptions = (
|
||||
options: MenuTagOptions | MenuTagOptions[],
|
||||
@ -195,6 +180,8 @@ export const piniaMenuStore = defineStore(
|
||||
isAppend
|
||||
? menuState.menuTagOptions.push(...arr)
|
||||
: (menuState.menuTagOptions = arr)
|
||||
|
||||
setStorage(APP_CATCH_KEY.appMenuTagOptions, menuState.menuTagOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,6 +201,13 @@ export const piniaMenuStore = defineStore(
|
||||
if (!tag) {
|
||||
menuState.menuTagOptions.push(option as MenuTagOptions)
|
||||
}
|
||||
|
||||
// 清理所有非根路径的标签页
|
||||
menuState.menuTagOptions = menuState.menuTagOptions.filter((curr) =>
|
||||
curr.fullPath?.startsWith('/'),
|
||||
)
|
||||
|
||||
setStorage(APP_CATCH_KEY.appMenuTagOptions, menuState.menuTagOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,22 +248,22 @@ export const piniaMenuStore = defineStore(
|
||||
|
||||
const { sameLevel } = meta
|
||||
|
||||
/** 更新缓存队列 */
|
||||
// 更新缓存队列
|
||||
setKeepAliveInclude(option)
|
||||
/** 更新浏览器标题 */
|
||||
// 更新浏览器标题
|
||||
updateDocumentTitle(option)
|
||||
|
||||
// 如果不为 sameLevel,则会执行更新:覆盖更新面包屑、添加标签菜单、更新缓存
|
||||
if (!sameLevel) {
|
||||
/** 更新标签菜单 */
|
||||
// 更新标签菜单
|
||||
setMenuTagOptionsWhenMenuValueChange(key, option)
|
||||
/** 更新面包屑 */
|
||||
// 更新面包屑
|
||||
setBreadcrumbOptions(key)
|
||||
|
||||
menuState.menuKey = key
|
||||
menuState.currentMenuOption = option
|
||||
|
||||
/** 缓存菜单 key(sessionStorage) */
|
||||
// 缓存菜单 key(sessionStorage)
|
||||
setStorage(APP_CATCH_KEY.appMenuKey, key)
|
||||
} else {
|
||||
// 使用 pick 提取仅需要的字段,避免 vue 抛错空引用,导致性能损耗
|
||||
@ -379,19 +373,18 @@ export const piniaMenuStore = defineStore(
|
||||
'',
|
||||
)
|
||||
|
||||
if (menuState.currentMenuOption) {
|
||||
const { currentMenuOption } = menuState
|
||||
const r =
|
||||
menuState.currentMenuOption ||
|
||||
(canSkipRoute(menuState.options) as AppMenuOption)
|
||||
|
||||
if (r) {
|
||||
// 惰性更新面包屑,避免 sameLevel 模式下的面包屑被覆盖
|
||||
if (!menuState.breadcrumbOptions.length) {
|
||||
setBreadcrumbOptions(currentMenuOption.key)
|
||||
setBreadcrumbOptions(r.key)
|
||||
}
|
||||
|
||||
setMenuTagOptionsWhenMenuValueChange(
|
||||
currentMenuOption.fullPath,
|
||||
currentMenuOption,
|
||||
)
|
||||
updateDocumentTitle(currentMenuOption)
|
||||
setMenuTagOptionsWhenMenuValueChange(r.fullPath, r)
|
||||
updateDocumentTitle(r)
|
||||
}
|
||||
|
||||
resolve()
|
||||
@ -418,8 +411,13 @@ export const piniaMenuStore = defineStore(
|
||||
* @description
|
||||
* 关闭 menu tag 标签。
|
||||
*/
|
||||
const spliceMenTagOptions = (idx: number, length = 1) =>
|
||||
menuState.menuTagOptions.splice(idx, length)
|
||||
const spliceMenTagOptions = (idx: number, length = 1) => {
|
||||
const r = menuState.menuTagOptions.splice(idx, length)
|
||||
|
||||
setStorage(APP_CATCH_KEY.appMenuTagOptions, menuState.menuTagOptions)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@ -467,7 +465,7 @@ export const piniaMenuStore = defineStore(
|
||||
persist: {
|
||||
key: APP_CATCH_KEY.appPiniaMenuStore,
|
||||
storage: window.localStorage,
|
||||
pick: ['breadcrumbOptions', 'menuKey', 'menuTagOptions', 'collapsed'],
|
||||
pick: ['menuKey', 'collapsed'],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -8,11 +8,3 @@ export interface MenuState {
|
||||
breadcrumbOptions: AppMenuOption[]
|
||||
currentMenuOption: AppMenuOption | null
|
||||
}
|
||||
|
||||
type PickUpdateKeys = 'collapsed' | 'currentMenuOption'
|
||||
|
||||
export type UpdateMenuState = <T extends keyof Pick<MenuState, PickUpdateKeys>>(
|
||||
key: T,
|
||||
value: Partial<MenuState[T]>,
|
||||
cb?: AnyFC,
|
||||
) => void
|
||||
|
@ -264,3 +264,17 @@ export const getCatchMenuKey = () => {
|
||||
|
||||
return cacheMenuKey
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns 获取缓存的菜单标签页
|
||||
*/
|
||||
export const getCatchMenuTagOptions = () => {
|
||||
return getStorage<AppMenuOption[]>(
|
||||
APP_CATCH_KEY.appMenuTagOptions,
|
||||
'sessionStorage',
|
||||
{
|
||||
defaultValue: [],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
89
src/store/modules/setting/constant.ts
Normal file
89
src/store/modules/setting/constant.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type { SettingState } from './types'
|
||||
|
||||
type OmitKeys =
|
||||
| 'contentTransition'
|
||||
| 'watermarkSwitch'
|
||||
| 'keepAliveConfig'
|
||||
| 'menuConfig'
|
||||
| 'menuTagSwitch'
|
||||
| 'breadcrumbSwitch'
|
||||
| 'copyrightSwitch'
|
||||
| 'drawerPlacement'
|
||||
| 'colorWeakness'
|
||||
| 'watermarkConfig'
|
||||
| 'dynamicDocumentTitle'
|
||||
|
||||
interface Config extends Pick<SettingState, OmitKeys> {}
|
||||
|
||||
// SettingDrawer 默认配置系统配置项
|
||||
const defaultSettingConfig: Readonly<Config> = {
|
||||
// 切换过渡效果
|
||||
contentTransition: 'scale',
|
||||
// 水印开关
|
||||
watermarkSwitch: false,
|
||||
// 缓存动态设置
|
||||
keepAliveConfig: {
|
||||
maxKeepAliveLength: 10,
|
||||
setupKeepAlive: true,
|
||||
keepAliveExclude: [],
|
||||
},
|
||||
// 菜单配置
|
||||
menuConfig: {
|
||||
collapsedWidth: 64,
|
||||
collapsedMode: 'width',
|
||||
collapsedIconSize: 16,
|
||||
collapsedIndent: 24,
|
||||
accordion: false,
|
||||
menuSiderBarLogo: true,
|
||||
iconSize: 16,
|
||||
menuWidth: 272,
|
||||
inverted: false,
|
||||
nativeScrollbar: false,
|
||||
},
|
||||
// 多标签页开关
|
||||
menuTagSwitch: true,
|
||||
// 面包屑开关
|
||||
breadcrumbSwitch: true,
|
||||
// 底部区域开关
|
||||
copyrightSwitch: true,
|
||||
// 默认设置出现位置
|
||||
drawerPlacement: 'right',
|
||||
// 色弱模式
|
||||
colorWeakness: false,
|
||||
// 水印
|
||||
watermarkConfig: {
|
||||
content: 'Trying be better~',
|
||||
fontSize: 16,
|
||||
lineHeight: 16,
|
||||
width: 384,
|
||||
height: 384,
|
||||
xOffset: 12,
|
||||
yOffset: 60,
|
||||
rotate: -15,
|
||||
xGap: 0,
|
||||
yGap: 0,
|
||||
cross: true,
|
||||
},
|
||||
// 动态标题
|
||||
dynamicDocumentTitle: true,
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
* 获取默认配置。
|
||||
*/
|
||||
export const getDefaultSettingConfig = (): Readonly<Config> => {
|
||||
return defaultSettingConfig
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config 配置项
|
||||
*
|
||||
* @description
|
||||
* 更新默认配置。
|
||||
*/
|
||||
export const updateDefaultSettingConfig = (config: Partial<SettingState>) => {
|
||||
Object.assign(defaultSettingConfig, config)
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { getAppDefaultLanguage } from '@/locales/utils'
|
||||
import { colorToRgba, setStorage, updateObjectValue, setStyle } from '@/utils'
|
||||
import { useI18n, useDayjs } from '@/hooks'
|
||||
import { APP_CATCH_KEY, APP_THEME } from '@/app-config'
|
||||
import { APP_CATCH_KEY, APP_THEME, GLOBAL_CLASS_NAMES } from '@/app-config'
|
||||
import { getDefaultSettingConfig } from './constant'
|
||||
import { cloneDeep, merge } from 'lodash-es'
|
||||
|
||||
import type { SettingState } from '@/store/modules/setting/types'
|
||||
import type { LocalKey } from '@/hooks'
|
||||
@ -11,53 +13,29 @@ export const piniaSettingStore = defineStore(
|
||||
'setting',
|
||||
() => {
|
||||
const {
|
||||
appPrimaryColor: { primaryColor },
|
||||
} = __APP_CFG__ // 默认主题色
|
||||
appPrimaryColor: { primaryColor, primaryFadeColor },
|
||||
} = APP_THEME
|
||||
const { locale } = useI18n()
|
||||
const { locale: dayjsLocal } = useDayjs()
|
||||
|
||||
const settingState = reactive<SettingState>({
|
||||
// 默认设置出现位置
|
||||
drawerPlacement: 'right',
|
||||
// 默认主题色
|
||||
primaryColorOverride: {
|
||||
common: {
|
||||
primaryColor: primaryColor,
|
||||
primaryColorHover: primaryColor,
|
||||
primaryColorHover: primaryFadeColor,
|
||||
primaryColorPressed: primaryColor,
|
||||
primaryColorSuppl: primaryFadeColor,
|
||||
},
|
||||
},
|
||||
// true 为黑夜主题, false 为明亮主题
|
||||
// 内部使用,用于判断是否为黑夜主题(为了兼容历史遗留版本);true 为黑夜主题,false 为明亮主题
|
||||
_appTheme: false,
|
||||
// 当前主题样式
|
||||
appTheme: 'light',
|
||||
// 多标签页开关
|
||||
menuTagSwitch: true,
|
||||
// 面包屑开关
|
||||
breadcrumbSwitch: true,
|
||||
// 默认国际化语言
|
||||
localeLanguage: getAppDefaultLanguage(),
|
||||
// 锁屏开关
|
||||
lockScreenSwitch: false,
|
||||
// 底部区域开关
|
||||
copyrightSwitch: true,
|
||||
// 切换过渡效果
|
||||
contentTransition: 'scale',
|
||||
// 水印开关
|
||||
watermarkSwitch: false,
|
||||
// 水印
|
||||
watermarkConfig: {
|
||||
content: 'Trying be better~',
|
||||
fontSize: 16,
|
||||
lineHeight: 16,
|
||||
width: 384,
|
||||
height: 384,
|
||||
xOffset: 12,
|
||||
xGap: 0,
|
||||
yGap: 0,
|
||||
yOffset: 60,
|
||||
rotate: -15,
|
||||
cross: true,
|
||||
},
|
||||
// 根路由信息
|
||||
appRootRoute: {
|
||||
name: 'Dashboard',
|
||||
@ -70,39 +48,16 @@ export const piniaSettingStore = defineStore(
|
||||
url: '/dashboard',
|
||||
jumpType: 'station',
|
||||
},
|
||||
// 缓存动态设置
|
||||
keepAliveConfig: {
|
||||
setupKeepAlive: true,
|
||||
keepAliveExclude: [],
|
||||
maxKeepAliveLength: 10,
|
||||
},
|
||||
// 菜单配置
|
||||
menuConfig: {
|
||||
collapsedWidth: 64,
|
||||
collapsedMode: 'width',
|
||||
collapsedIconSize: 16,
|
||||
collapsedIndent: 24,
|
||||
accordion: false,
|
||||
menuSiderBarLogo: true,
|
||||
iconSize: 16,
|
||||
menuWidth: 272,
|
||||
inverted: false,
|
||||
nativeScrollbar: false,
|
||||
},
|
||||
// 色弱模式
|
||||
colorWeakness: false,
|
||||
// 动态标题
|
||||
dynamicDocumentTitle: true,
|
||||
...cloneDeep(getDefaultSettingConfig()),
|
||||
})
|
||||
|
||||
// 修改当前语言
|
||||
const updateLocale = (key: string) => {
|
||||
locale(key)
|
||||
dayjsLocal(key as LocalKey)
|
||||
setStorage(APP_CATCH_KEY.localeLanguage, key, 'localStorage')
|
||||
|
||||
settingState.localeLanguage = key
|
||||
|
||||
setStorage(APP_CATCH_KEY.localeLanguage, key, 'localStorage')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,21 +65,24 @@ export const piniaSettingStore = defineStore(
|
||||
* @description
|
||||
* 切换主题色,传递对应颜色即可更新 naive-ui 的主题色。
|
||||
*/
|
||||
const changePrimaryColor = (value: string, alpha = 0.3) => {
|
||||
const alphaColor = colorToRgba(value, alpha)
|
||||
const changePrimaryColor = (value: string, alpha = 0.85) => {
|
||||
const alphaColor1 = colorToRgba(value, alpha)
|
||||
const themeOverrides = {
|
||||
primaryColor: value,
|
||||
primaryColorHover: value,
|
||||
primaryColorHover: alphaColor1,
|
||||
primaryColorPressed: value,
|
||||
primaryColorSuppl: alphaColor1,
|
||||
}
|
||||
const { rayTemplateThemePrimaryColor, rayTemplateThemePrimaryFadeColor } =
|
||||
GLOBAL_CLASS_NAMES
|
||||
const html = document.documentElement
|
||||
|
||||
settingState.primaryColorOverride.common = themeOverrides
|
||||
|
||||
const html = document.documentElement
|
||||
|
||||
/** 设置主题色变量 */
|
||||
html.style.setProperty('--ray-theme-primary-color', value)
|
||||
html.style.setProperty('--ray-theme-primary-fade-color', alphaColor)
|
||||
// 设置主题色变量
|
||||
html.style.setProperty(rayTemplateThemePrimaryColor, value)
|
||||
// 设置主题色辅助色变量
|
||||
html.style.setProperty(rayTemplateThemePrimaryFadeColor, alphaColor1)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +107,7 @@ export const piniaSettingStore = defineStore(
|
||||
value: Partial<V[T]>,
|
||||
cb?: C,
|
||||
) => {
|
||||
updateObjectValue(settingState, key, value, cb)
|
||||
updateObjectValue(settingState, key, value as V[T], cb)
|
||||
}
|
||||
|
||||
const toggleColorWeakness = (bool: boolean) => {
|
||||
@ -168,18 +126,16 @@ export const piniaSettingStore = defineStore(
|
||||
* 该方法会在初始化时执行一次,之后会在切换主题色时执行。
|
||||
*/
|
||||
watchEffect(() => {
|
||||
settingState.appTheme
|
||||
? (settingState.primaryColorOverride = Object.assign(
|
||||
settingState._appTheme
|
||||
? (settingState.primaryColorOverride = merge(
|
||||
{},
|
||||
settingState.primaryColorOverride,
|
||||
APP_THEME.appNaiveUIThemeOverrides.dark,
|
||||
APP_THEME.appNaiveUIThemeOverridesCommon.dark,
|
||||
))
|
||||
: (settingState.primaryColorOverride = Object.assign(
|
||||
: (settingState.primaryColorOverride = merge(
|
||||
{},
|
||||
settingState.primaryColorOverride,
|
||||
APP_THEME.appNaiveUIThemeOverrides.light,
|
||||
APP_THEME.appNaiveUIThemeOverridesCommon.light,
|
||||
))
|
||||
|
||||
toggleColorWeakness(settingState.colorWeakness)
|
||||
@ -200,6 +156,8 @@ export const piniaSettingStore = defineStore(
|
||||
{
|
||||
persist: {
|
||||
key: APP_CATCH_KEY.appPiniaSettingStore,
|
||||
omit: ['lockScreenSwitch'],
|
||||
storage: window.localStorage,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -22,6 +22,7 @@ import type {
|
||||
export const piniaSigningStore = defineStore(
|
||||
'signing',
|
||||
() => {
|
||||
const { clearRoutes, replace } = useRouter()
|
||||
const state = reactive({
|
||||
/**
|
||||
*
|
||||
@ -90,11 +91,13 @@ export const piniaSigningStore = defineStore(
|
||||
removeStorage(token, 'localStorage')
|
||||
removeStorage(signing, 'localStorage')
|
||||
removeStorage(appMenuKey, 'localStorage')
|
||||
removeStorage(APP_CATCH_KEY.isAppLockScreen, 'localStorage')
|
||||
// 关闭所有侧边栏标签
|
||||
closeAll()
|
||||
|
||||
if (toSigning) {
|
||||
window.location.replace('#/')
|
||||
clearRoutes()
|
||||
replace('/')
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
|
48
src/styles/animate.scss
vendored
48
src/styles/animate.scss
vendored
@ -22,21 +22,27 @@
|
||||
}
|
||||
|
||||
/* scale-transform */
|
||||
.scale-transform-enter-active,
|
||||
.scale-transform-enter-active {
|
||||
transition:
|
||||
transform 0.35s cubic-bezier(0.33, 1, 0.68, 1),
|
||||
opacity 0.25s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
|
||||
.scale-transform-leave-active {
|
||||
transition:
|
||||
transform 0.4s var(--r-bezier),
|
||||
opacity 0.45s var(--r-bezier);
|
||||
transform 0.35s cubic-bezier(0.32, 0, 0.67, 0),
|
||||
opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.scale-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.scale-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(1.06);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* opacity-transform */
|
||||
@ -54,35 +60,49 @@
|
||||
}
|
||||
|
||||
/* fade-bottom-transform */
|
||||
.fade-bottom-transform-enter-active,
|
||||
.fade-bottom-transform-enter-active {
|
||||
transition:
|
||||
transform 0.35s cubic-bezier(0.33, 1, 0.68, 1),
|
||||
opacity 0.25s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
|
||||
.fade-bottom-transform-leave-active {
|
||||
transition:
|
||||
opacity 0.55s var(--r-bezier),
|
||||
transform 0.45s var(--r-bezier);
|
||||
transform 0.35s cubic-bezier(0.32, 0, 0.67, 0),
|
||||
opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.fade-bottom-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
|
||||
.fade-bottom-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
/* fade-scale-transform */
|
||||
.fade-scale-transform-leave-active,
|
||||
.fade-scale-transform-enter-active {
|
||||
transition: all 0.48s var(--r-bezier);
|
||||
transition:
|
||||
transform 0.35s cubic-bezier(0.33, 1, 0.68, 1),
|
||||
opacity 0.25s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
|
||||
.fade-scale-transform-leave-active {
|
||||
transition:
|
||||
transform 0.25s cubic-bezier(0.32, 0, 0.67, 0),
|
||||
opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.fade-scale-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.fade-scale-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import '@/styles/animate.scss';
|
||||
@import '@/styles/root.scss';
|
||||
@import '@/styles/naive.scss';
|
||||
@import '@/styles/print-css.scss';
|
||||
@use '@/styles/animate.scss' as *;
|
||||
@use '@/styles/root.scss' as *;
|
||||
@use '@/styles/naive.scss' as *;
|
||||
@use '@/styles/print-css.scss' as *;
|
||||
|
||||
body,
|
||||
h1,
|
||||
|
18
src/types/app.d.ts
vendored
18
src/types/app.d.ts
vendored
@ -2,27 +2,9 @@
|
||||
|
||||
export {}
|
||||
|
||||
import 'vue-router'
|
||||
|
||||
import type { AppRouteMeta } from '@/router/types'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta extends AppRouteMeta {}
|
||||
}
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const Component: DefineComponent<{}, {}, any>
|
||||
export default Component
|
||||
}
|
||||
|
||||
declare module 'virtual:*' {
|
||||
const result: any
|
||||
export default result
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const jsonContent: Record<string, any>
|
||||
export default jsonContent
|
||||
}
|
||||
|
8
src/types/json.d.ts
vendored
Normal file
8
src/types/json.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export {}
|
||||
|
||||
declare module '*.json' {
|
||||
const jsonContent: Record<string, any>
|
||||
export default jsonContent
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export interface BasicResponse<T> {
|
||||
data: T
|
||||
msg: string
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user