mirror of
https://github.com/XiaoDaiGua-Ray/ray-template.git
synced 2025-04-04 06:02:50 +08:00
update
This commit is contained in:
parent
bbd4765d67
commit
0887bf18cd
25
.all-contributorsrc
Normal file
25
.all-contributorsrc
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "yunkuangao",
|
||||
"name": "Cloud",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/40163747?v=4",
|
||||
"profile": "https://heartofyun.com",
|
||||
"contributions": [
|
||||
"tool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true,
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "xiaodaigua-ray.github.io",
|
||||
"projectOwner": "XiaoDaiGua-Ray"
|
||||
}
|
2
.depcheckrc
Normal file
2
.depcheckrc
Normal file
@ -0,0 +1,2 @@
|
||||
ignores: [ "eslint", "babel-*", "@use-*/**", "@use-*" ]
|
||||
skip-missing: true
|
10
.env.development
Normal file
10
.env.development
Normal file
@ -0,0 +1,10 @@
|
||||
#开发环境
|
||||
NODE_ENV = 'development'
|
||||
|
||||
VITE_APP_URL = '/api'
|
||||
|
||||
# office 服务代理地址
|
||||
VITE_APP_OFFICE_PROXY_URL = '/office/'
|
||||
|
||||
# office 脚本地址(用于动态创建脚本标签)
|
||||
VITE_APP_OFFICE_SCRIPT_URL = 'https://office.yka.one/web-apps/apps/api/documents/api.js'
|
10
.env.production
Normal file
10
.env.production
Normal file
@ -0,0 +1,10 @@
|
||||
#生产环境
|
||||
NODE_ENV = 'production'
|
||||
|
||||
VITE_APP_URL = '/'
|
||||
|
||||
# office 服务代理地址
|
||||
VITE_APP_OFFICE_PROXY_URL = 'https://office.yka.one/'
|
||||
|
||||
# office 脚本地址(用于动态创建脚本标签)
|
||||
VITE_APP_OFFICE_SCRIPT_URL = 'https://office.yka.one/web-apps/apps/api/documents/api.js'
|
10
.env.test
Normal file
10
.env.test
Normal file
@ -0,0 +1,10 @@
|
||||
#测试环境
|
||||
NODE_ENV = 'test'
|
||||
|
||||
VITE_APP_URL = 'https://testray.yka.moe/doc-json/'
|
||||
|
||||
# office 服务代理地址
|
||||
VITE_APP_OFFICE_PROXY_URL = 'https://office.yka.one/'
|
||||
|
||||
# office 脚本地址(用于动态创建脚本标签)
|
||||
VITE_APP_OFFICE_SCRIPT_URL = 'https://office.yka.one/web-apps/apps/api/documents/api.js'
|
15
.eslintignore
Normal file
15
.eslintignore
Normal file
@ -0,0 +1,15 @@
|
||||
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
|
198
.eslintrc.cjs
Normal file
198
.eslintrc.cjs
Normal file
@ -0,0 +1,198 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint-config-prettier',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
],
|
||||
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',
|
||||
},
|
||||
rules: {
|
||||
'@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': [1, { max: 2 }], // 空行最多不能超过 `2` 行
|
||||
'no-new-func': 1, // 禁止使用 `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`
|
||||
'no-undef': 0,
|
||||
'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'],
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
functions: true,
|
||||
classes: true,
|
||||
variables: false,
|
||||
allowNamedExports: false,
|
||||
},
|
||||
],
|
||||
'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: true,
|
||||
globals: ['RouterView'],
|
||||
},
|
||||
],
|
||||
'vue/no-unused-refs': ['error'],
|
||||
'vue/prop-name-casing': ['error', 'camelCase'],
|
||||
'vue/component-options-name-casing': ['error', 'camelCase'],
|
||||
'vue/attribute-hyphenation': [
|
||||
'error',
|
||||
'never',
|
||||
{
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'vue/no-restricted-static-attribute': [
|
||||
'error',
|
||||
{
|
||||
key: 'key',
|
||||
message: 'Disallow using key as a custom attribute',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock*
|
||||
|
||||
node_modules
|
||||
dist-ssr
|
||||
dist
|
||||
dist/
|
||||
*.local
|
||||
visualizer.*
|
||||
components.d.ts
|
||||
auto-imports.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
yarn-*.*
|
||||
yarn.*
|
||||
pnpm.*
|
||||
pnpm-lock.yaml
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --allow-empty "$1"
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --allow-empty "$1"
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
visualizer.html
|
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@ -0,0 +1,12 @@
|
||||
dist/*
|
||||
node_modules/*
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
.gitignore
|
||||
public
|
||||
yarn.*
|
||||
vite-env.*
|
||||
.prettierrc.*
|
||||
visualizer.*
|
||||
visualizer.html
|
||||
.env.*
|
20
.prettierrc.cjs
Normal file
20
.prettierrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
printWidth: 80, // 一行最多 `80` 字符
|
||||
tabWidth: 2, // 使用 `2` 个空格缩进
|
||||
useTabs: false, // 不使用缩进符, 而使用空格
|
||||
semi: false, // 行尾不需要有分号
|
||||
singleQuote: true, // 使用单引号
|
||||
quoteProps: 'as-needed', // 对象的 `key` 仅在必要时用引号
|
||||
jsxSingleQuote: false, // `jsx` 不使用单引号, 而使用双引号
|
||||
trailingComma: 'all', // 尾随逗号
|
||||
bracketSpacing: true, // 大括号内的首尾需要空格
|
||||
jsxBracketSameLine: false, // `jsx` 标签的反尖括号需要换行
|
||||
arrowParens: 'always', // 箭头函数, 只有一个参数的时候, 也需要括号
|
||||
rangeStart: 0, // 每个文件格式化的范围是文件的全部内容
|
||||
rangeEnd: Infinity,
|
||||
requirePragma: false, // 不需要写文件开头的 `@prettier`
|
||||
insertPragma: false, // 不需要自动在文件开头插入 `@prettier`
|
||||
proseWrap: 'preserve', // 使用默认的折行标准
|
||||
htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 `html` 要不要折行
|
||||
endOfLine: 'lf', // 换行符使用 `lf`
|
||||
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["vue.volar", "lokalise.i18n-ally"]
|
||||
}
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": ["src/locales/lang"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||
"i18n-ally.enabledParsers": ["json"],
|
||||
"i18n-ally.sourceLanguage": "zh-CN",
|
||||
"i18n-ally.displayLanguage": "zh-CN",
|
||||
"i18n-ally.enabledFrameworks": ["vue", "react"]
|
||||
}
|
308
CHANGELOG.md
Normal file
308
CHANGELOG.md
Normal file
@ -0,0 +1,308 @@
|
||||
# CHANGE LOG
|
||||
|
||||
## 4.0.2
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增平级路由配置(router meta)配置项,sameLevel 允许你将子路由标记为平级模式,跳转时不会出发菜单、标签页更新,仅会更新面包屑
|
||||
- 修改路由菜单显示、隐藏逻辑,现在仅会针对权限的验证匹配选择是否加入菜单列表中
|
||||
- 更新 setupAppMenu 方法触发时机(Layout => menu store),现在将在 pinia menu store 初始化时触发 App Menu 更新
|
||||
- 更新了 utils 包中的一些方法,进行了一些重写和重命名
|
||||
- GlobalSearch 组件支持上下按键切换、回车键选择
|
||||
- 整合 router 模块的一些包,让它看起来更合理一点
|
||||
- 剔除 styles 包中一些不合理的样式模块
|
||||
- 补充了一些注释与说明文档
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复不能正确渲染浏览器标题问题
|
||||
- 修复初始化模板菜单函数与菜单更新函数重复执行一些方法的问题
|
||||
- 修复指令示例变量绑定错误导致示例错误问题
|
||||
- 修复路由白名单失效 bug
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Feats
|
||||
|
||||
- 更改自定义路由暴露形式(由变量暴露改为方法获取)
|
||||
- 模板所有方法进行检查,重命名部分方法(使其更加贴切其逻辑)
|
||||
- 部分逻辑进行重写,使代码更容易阅读与维护
|
||||
- 模板类型进一步完善
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复了内存高占用问题(路由模块)
|
||||
- 修复类型导入错误问题
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Feats
|
||||
|
||||
- 重构 types 包设计,现在的类型包更加清晰
|
||||
- 重构 utils 包设计,该包下的所有 hook 提供了更加友好的类型提示
|
||||
- RayIframe 组件新增 lazy 属性
|
||||
- 新增 v-disabled 指令
|
||||
- demo 页面展示优化
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复一些已知的 bug
|
||||
|
||||
### 补充
|
||||
|
||||
> 这次花了一点时间,将模板进行重新梳理,进行了一些很大的破坏性更新改动。核心重点是 types 包与 utils 包的重大更新。不过只是做了初步的一些大方向的更新,后续的细节更新还在继续。。。
|
||||
|
||||
## 3.3.7
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增全局指令(目前仅有:v-copy、v-debounce、v-throttle)
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复错误的插件命名,导致项目构建失败(viteComponents)
|
||||
|
||||
## 3.3.6
|
||||
|
||||
### Feats
|
||||
|
||||
- 重写 axios interceptor 方法。现在逻辑更加清晰,并且支持请求错误、响应错误处理。补充了两个工具函数
|
||||
- MenuTag 支持动态更新所在位置
|
||||
- 修复了鉴权方法的 bug
|
||||
- 更新了 router permission 方法(路由守卫)
|
||||
- 补充了一些模块文档
|
||||
- 搜索支持以菜单模块的 icon 进行渲染,如果为空则以 icon table 默认填充
|
||||
- 重写锁屏功能,现在将锁屏逻辑与解锁逻辑拆分为两个组件
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复选中所搜结果后,菜单不能默认展开 bug
|
||||
|
||||
### 补充
|
||||
|
||||
> 文档拖欠太多了,我补不回来了,就。。。算了吧,我在每个关键模块补充了对应的 md 说明文档,凑合一下吧。真希望有一个好心人帮补充文档。
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Feats
|
||||
|
||||
- Router Meta 属性支持自定义图标,不再局限于 RayIcon,支持自定义图标
|
||||
- 更改部分组件默认值,默认值统一为 `null`
|
||||
- 调整 validRole 方法逻辑,将该方法以前逻辑拆分为 validRole 与 validMenuItemShow 两个方法
|
||||
- 新增使用手册
|
||||
|
||||
### 补充
|
||||
|
||||
> 由于文档已经拖更很久,所以补充一个使用手册。最近太忙了,一直忙着更新完善模板本身,文档的事情暂时没有时间去维护更新,所以与模板断层太久。。。后续有时间肯定会补上!!!
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增 RayIframe 组件
|
||||
- 同步更新 `naive-ui` 版本至最新版本(2.34.3 => 2.34.4)
|
||||
- 支持更多 appConfig 配置
|
||||
|
||||
### TODO
|
||||
|
||||
- MenuTag: 切换页面时, 同步更新该标签的所在位置
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增五个计算方法(解决精度问题)
|
||||
- 解决一些小问题
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增 useAppTheme sass 方法
|
||||
|
||||
```ts
|
||||
useAppTheme key 类型: 'dark' | 'light'
|
||||
```
|
||||
|
||||
```scss
|
||||
// 暗色主题
|
||||
.demo--dark {
|
||||
@include useAppTheme('dark') {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
// 明亮主题
|
||||
.demo--light {
|
||||
@include useAppTheme('light') {
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 一些细节优化
|
||||
- axios 拦截器与 axios instance 进行独立(现在不再 instance.ts 文件中编写拦截器相关逻辑),拦截器逻辑放在 inject 包中
|
||||
- 一些 bug 修复
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### 特征
|
||||
|
||||
- 取消 RootRoute 属性暴露全局
|
||||
- 新增 Route Meta keepAlive 配置开启页面缓存(可以在 AppConfig APP_KEEP_ALIVE 中进行缓存的配置管理)
|
||||
- 回退使用自动导入路由模块方式,具体使用方法查看 [路由配置](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/src/router/README.md)
|
||||
- 新增 Route Meta order 配置,配置菜单顺序
|
||||
- 新增 useVueRouter 方法,让你在 setup 环境之外使用 router hook
|
||||
- 补充引入了一些 eslint 规则
|
||||
- 支持更多 appConfig 配置
|
||||
|
||||
### 补充
|
||||
|
||||
- 后续该模板还会持续维护,会尽可能多的支持更多业务场景
|
||||
- 最近破坏性更新很多,发布比较频繁,后续应该不会有这么大的破坏性更新。核心重点会放在模板整体的健壮性、可维护性上
|
||||
- 未来希望模板拆分为一个高拓展性的工程,积木式管理项目,让项目模块之间尽可能的解耦。让模板有更好的拓展性,让你在使用时,可以根据自身业务需求进行拓展(当然,我希望你能以项目的基本维护原则延续)
|
||||
|
||||
## 3.2.3
|
||||
|
||||
### 特征
|
||||
|
||||
- 新增锁屏功能(值得注意的是,锁屏解锁后会刷新当前 RouterView 区域,因为在处于锁屏状态时,会自动销毁所有的操作页面。可以理解为是一个 v-if 操作行为)
|
||||
- 新增 dayjs hook,支持国际化与切换
|
||||
- 支持更多 appConfig 配置
|
||||
- 调整 setupAppRoute 触发时机(现在会在 layout 渲染阶段触发)
|
||||
- 补充了新的组件分包 AppComponents,存放该系统的一些组件(会与系统进行一些深度绑定,例如 AppAvatar 组件依赖系统数据)
|
||||
|
||||
### 补充
|
||||
|
||||
- 锁屏功能的设计并不理想,后期会进行破坏性更新。锁屏触发条件与管理方式目前并不理想,管理有点混乱
|
||||
- 后期会考虑补充 keepAlive 功能。目前没有实现是因为该功能实现的话,需要将所有路由提升为顶层路由(这是 KeepAlive 组件限制),目前并未实现该功能。后期会在权衡后增加该功能,实现时会在 RayTransitionComponent 进行拓展补充
|
||||
|
||||
## 3.2.2
|
||||
|
||||
### 特征
|
||||
|
||||
- 移除 amfe-flexible 插件,改用为 postcss-px-to-viewport 作为适配插件
|
||||
- 支持更多 appConfig 配置
|
||||
|
||||
## 3.2.1
|
||||
|
||||
### 特征
|
||||
|
||||
- 调整系统文件分包,现在结构更加合理、更加清晰
|
||||
- 新增 src/appConfig 配置入口,配置系统(还在持续补充中...)
|
||||
- vite 版本更新到 4.3.8
|
||||
|
||||
## 3.1.8
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复路由切换不能复位容器位置问题(让可视区域置顶)
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增 useI18n hook 方法
|
||||
- 手动补充 AppRouteRecordRaw、AppRouteMeta 类型
|
||||
- 重新拆分 Layout 入口文件
|
||||
- 重新指定组件暴露方法、属性
|
||||
- 修改国际化管理方式,现在支持自动合并管理与结合 i18n-ally 使用。并且支持 unplugin-vue-i18n 构建,提高性能
|
||||
|
||||
## 3.1.7
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复默认获取容器可视区域高度问题
|
||||
- 修复登陆页虚线高度问题
|
||||
|
||||
### Feats
|
||||
|
||||
- 修改 Menu 菜单过滤逻辑,现在如果权限不匹配或者设置了 hidden 属性,则会被过滤掉
|
||||
- 移除 $activedColor 全局 sass 变量,使用 --ray-theme-primary-color 替代
|
||||
- 新增路由菜单检索功能
|
||||
- 移除 App.tsx 中同步主题方法,改为使用 cfg 配置并且使用 ejs 注入
|
||||
- 移除 MenuTag 默认主题色,现在会以当前主题色为主色
|
||||
|
||||
## 3.1.6
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复移动端登陆页显示问题
|
||||
- 改进了一些方法逻辑的问题
|
||||
- 修改移动端自适应配置方案(现在使用 postcss-px-to-viewport),默认不启用
|
||||
- 修复 RayTable 实例方法暴露错误
|
||||
- 修复 sideBarLogo.icon 为空时警告问题,现在未配置该属性则不会渲染图标
|
||||
- 修复 RayTable 演示页面 action 方法失效问题
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增加载动画
|
||||
- 现在可以直接配置首屏加载动画一些信息(cfg.ts)
|
||||
- 新增对于 ejs 支持
|
||||
- 补充一些细节注释
|
||||
- 新增 RayChart 组件 loading、loadingOptions 属性配置
|
||||
- 新增反转色模式
|
||||
- 修改 Menu 菜单过滤逻辑,现在如果权限不匹配或者设置了 hidden 属性,则会被过滤掉
|
||||
|
||||
## 3.1.5
|
||||
|
||||
### Fixes
|
||||
|
||||
- 配置 `tsconfig.json` 中 `ignoreDeprecations` 属性,消除 `ts5.0` 破坏性配置更新警告
|
||||
|
||||
### Feats
|
||||
|
||||
- 基于 `onlyoffice` 新增 `Office` 功能(待完成...)
|
||||
- 重写 `AxiosInstance` 类型
|
||||
- `src/types` 分包更加清晰
|
||||
- 将主色调同步至 `body`,默认同步 `cfg.primaryColor` 值
|
||||
- 登陆页一些修改(现在支持简单的响应式)
|
||||
- 将一些设置型功能抽离为组件
|
||||
- 调整同步主题色执行时机
|
||||
|
||||
## 3.1.4
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复主题色切换后,点击、鼠标滑入主题未被修改问题
|
||||
- 修复 menu store 菜单切换可能会重复执行问题
|
||||
|
||||
### Feats
|
||||
|
||||
- 补充 MenuTag 标签页功能,现在支持丰富的关闭操作与右键菜单激活操作菜单功能
|
||||
- 新增配置全局重定向地址配置(详情见:[cfg](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/cfg.ts))
|
||||
- 补充了一些不值一提的小东西
|
||||
|
||||
## 3.1.3
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复菜单栏、标签页栏 border 显示问题
|
||||
|
||||
### Feats
|
||||
|
||||
- RayTable 组件新增全屏、尺寸调整功能
|
||||
- 新增 css 预处理全局注入辅助函数。详情看 [mixinCSS](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/cfg.ts)
|
||||
- RayTable 组件部分提示文案修改
|
||||
- body 新增当前主题色 class 标识(dark: ray-template--dark,light: ray-template--light),便捷主题切换配置
|
||||
|
||||
## 3.1.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复 DatePicker 组件国际化部分失效问题
|
||||
|
||||
### Feats
|
||||
|
||||
- 修改 demo 页面展示
|
||||
- 修改 RayCollapseGrid、RayTable 组件为默认不展示 border
|
||||
|
||||
## 3.1.1
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复国际化语言包模块合并处理不能正常合并问题
|
||||
- 修复国际化切换时,面包屑、标签页不能正常切换
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增面包屑
|
||||
- 支持国际化语言包分包管理(但是,依旧是合并到一个文件中,所以需要注意 key 的管理)
|
||||
- 新增国内预览地址
|
21
COMMONPROBLEM.md
Normal file
21
COMMONPROBLEM.md
Normal file
@ -0,0 +1,21 @@
|
||||
## 常见问题
|
||||
|
||||
### 路由
|
||||
|
||||
#### 缓存失效
|
||||
|
||||
> 如果出现缓存配置不生效的情况可以按照如下方法进行排查
|
||||
|
||||
- 查看 APP_KEEP_ALIVE setupKeepAlive 属性是否配置为 true
|
||||
- 查看每个组件的 `name` 是否唯一,[`KeepAlive`](https://cn.vuejs.org/guide/built-ins/keep-alive.html) 组件重度依赖组件 `name` 作为唯一标识。详情可以查看官方文档
|
||||
- 查看该页面的路由配置是否正确,比如:`path` 是否按照模板约定方式进行配置
|
||||
|
||||
#### 自动导入失败
|
||||
|
||||
> 模板采用自动导入路由模块方式。如果发现路由导入有误、或者导入报错,请查看文件命名是否有误。
|
||||
|
||||
### 国际化
|
||||
|
||||
#### 国际化切换错误、警告
|
||||
|
||||
> 模板二次封装 [`useI18n`](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/src/locales/useI18n.ts) 方法,首选该方法作为国际化语言切换方法。
|
161
MANUAL.md
Normal file
161
MANUAL.md
Normal file
@ -0,0 +1,161 @@
|
||||
## Ray Template 使用手册
|
||||
|
||||
## 前言
|
||||
|
||||
> `Ray Template` 默认使用 `yarn` 作为包管理器,并且默认启用严格模式的 `eslint`。在导入模块的时候除 `.ts` `.tsx` `.d.ts` 文件等不需要手动补全后缀名,其余的模块导入应该手动补全所有后缀名。
|
||||
|
||||
### 使用
|
||||
|
||||
#### 依赖安装
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
yarn
|
||||
|
||||
# npm
|
||||
npm i
|
||||
```
|
||||
|
||||
#### 启动项目
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### 构建项目
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# npm
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 国际化
|
||||
|
||||
#### Tip
|
||||
|
||||
- 每个新的语言包文件的文件名视为 path 开头(menu.json => menu.xxx)
|
||||
|
||||
#### 新增语言包、新增语言模块
|
||||
|
||||
> 项目国际化管理模块,统一放置于 `src/locales` 下。
|
||||
|
||||
##### 文件包
|
||||
|
||||
- lang
|
||||
- helper.ts
|
||||
- index.ts
|
||||
- useI18n.ts
|
||||
|
||||
> 项目中使用 t locale 方法时,使用模板提供方法(useI18n.ts)
|
||||
|
||||
```tsx
|
||||
// 引入包
|
||||
import { useI18n } from '@/locales/useI18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
/**
|
||||
*
|
||||
* t: 用于绑定国际化 path
|
||||
* locale: 用于切换系统语言。参数 key 必须与 lang 包中的语言包一致
|
||||
*/
|
||||
|
||||
const demo = () => <span>{t('demo.demo')}</span>
|
||||
|
||||
locale('zh-CN')
|
||||
locale('en-US')
|
||||
```
|
||||
|
||||
##### 新增语言包
|
||||
|
||||
> 我们举例新增台湾地区语言包。
|
||||
|
||||
- `src/locales/lang` 文件下创建对应新语言包文件夹与语言文件
|
||||
- zh-TW 文件夹
|
||||
- zh-TW.ts 文件
|
||||
- 创建与原有语言格式一样的文件夹(可以直接 cv 过去)
|
||||
- 配置语言下拉项(LOCAL_OPTIONS)
|
||||
- 配置 dayjs 国际化映射(DAYJS_LOCAL_MAP)
|
||||
|
||||
> 具体注意事项看注释。
|
||||
|
||||
##### 最后
|
||||
|
||||
> 按照上述步骤操作后,就已经给模板添加了一个新的语言包了。可以在页面的右上角国际化下拉框中看到新增的下拉选项,点击切换后,即可切换到对应的语言了。
|
||||
|
||||
### 路由
|
||||
|
||||
#### Tip
|
||||
|
||||
- 在该模板中,路由 path 属性视 `/` 开头的路由为根路由
|
||||
- 路由会在初始化的时候将所有路由进行提升(全部提升为顶级路由),所以注意第一条 Tip
|
||||
- 路由模块会影响菜单的输出显示(菜单模块与路由模块进行拆分开来的)
|
||||
- 具体路由支持配置属性看 router/README.md 文件
|
||||
|
||||
#### 文件包
|
||||
|
||||
- constant 文件放置一些公共东西
|
||||
- helper 文件放置 router 的一些 hook 方法
|
||||
- modules 页面路由入口(modules 文件中每一个 xxx.ts 文件都会被视为是一个路由模块)
|
||||
- utils router 拓展方法
|
||||
- ...
|
||||
|
||||
##### 新增路由页面
|
||||
|
||||
- modules 中添加一个新的模块(log.ts)
|
||||
- 配置路由的相关信息
|
||||
- views 中创建 log 相关的页面信息
|
||||
|
||||
```ts
|
||||
// 辅助函数,配合 i18n-ally 插件使用
|
||||
import { t } from '@/locales/useI18n'
|
||||
|
||||
// 路由配置类型提示
|
||||
import type { AppRouteRecordRaw } from '@/router/type'
|
||||
|
||||
const log: AppRouteRecordRaw = {
|
||||
path: '/log',
|
||||
name: 'Log',
|
||||
component: () => import('@/views/log/index.vue'),
|
||||
meta: {
|
||||
i18nKey: t('menu.Log'),
|
||||
icon: 'log',
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'my-log',
|
||||
name: 'MyLog',
|
||||
component: () => import('@/views/my-log/index.vue'),
|
||||
meta: {
|
||||
i18nKey: t('menu.MyLog'),
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'group-log',
|
||||
name: 'MyLog',
|
||||
component: () => import('@/views/group-log/index.vue'),
|
||||
meta: {
|
||||
i18nKey: t('menu.GroupLog'),
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default log
|
||||
```
|
||||
|
||||
##### 最后
|
||||
|
||||
> 打开浏览器可以看到页面菜单上已经有一个日志菜单。
|
||||
|
||||
#### 未完待续。。。后续慢慢更新该手册
|
232
README.md
Normal file
232
README.md
Normal file
@ -0,0 +1,232 @@
|
||||
# `Ray Template`
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[](#contributors-)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
## 感谢
|
||||
|
||||
> 感谢 <https://me.yka.moe/> 对于本人的支持。
|
||||
|
||||
## 预览地址
|
||||
|
||||
- [点击预览](https://xiaodaigua-ray.github.io/ray-template/#/)
|
||||
- [点击预览(加速地址)](https://ray-template.yunkuangao.com/#/)
|
||||
|
||||
## 文档地址
|
||||
|
||||
- [文档](https://xiaodaigua-ray.github.io/ray-template-doc/)
|
||||
- [文档(加速地址)](https://ray-template.yunkuangao.com/ray-template-doc/)
|
||||
|
||||
## 更新日志
|
||||
|
||||
- [日志](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md)
|
||||
|
||||
## 常见问题
|
||||
|
||||
- [常见问题](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/COMMONPROBLEM.md)
|
||||
|
||||
## 功能
|
||||
|
||||
- 主题切换
|
||||
- 任意深度页面缓存
|
||||
- 系统配置化
|
||||
- 锁屏
|
||||
- 自动化路由
|
||||
- 带有拓展功能的表格
|
||||
- 封装 `axios` 自动取消重复请求,暴露拦截器注册器
|
||||
- 全局菜单搜索
|
||||
- 动态菜单(多级菜单)
|
||||
- 主题色切换
|
||||
- 错误页
|
||||
- 面包屑
|
||||
- 标签页
|
||||
- 国际化(允许按模块管理语言包)
|
||||
- 权限路由
|
||||
- 动态切换主题、贴花的 `EChart` 图
|
||||
- 最佳构建体验
|
||||
- 体积分析
|
||||
- 还有一些不值一提的小东西...
|
||||
|
||||
## 未来
|
||||
|
||||
> 根据个人时间空余情况,会不定时对该模板进行更新和迭代。希望将该工具的功能不断补全(虽然现在已经是足够日常开发和使用),将该模板打造为一个更加健全的中后台模板。如果你有好的想法和建议,可以直接联系我或者直接提 `issues` 即可。
|
||||
|
||||
## 前言
|
||||
|
||||
> 该项目模板采用 `vue3.x` `vite4.x` `pinia` `tsx` 进行开发。
|
||||
> 使用 `naive ui` 作为组件库。
|
||||
> 预设了最佳构建体验的配置与常用搬砖工具。意在提供一个简洁、快速上手的模板。
|
||||
> 该模板不支持移动端设备。
|
||||
|
||||
## 提示
|
||||
|
||||
> 项目默认启用严格模式 `eslint`,但是由于 `vite-plugin-eslint` 插件优先级最高,所以如果出现自动导入类型错误提示,请优先解决其他问题。
|
||||
> 建议开启 `vscode` 保存自动修复功能。
|
||||
|
||||
## 版本说明
|
||||
|
||||
> 做了一些大的改动升级,让模板更加好用了一点,默认主题色也做了变更更好看了一点。啰嗦两句,好像也没啥其他的了...
|
||||
|
||||
## 拉取依赖
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
|
||||
yarn
|
||||
```
|
||||
|
||||
```sh
|
||||
# npm
|
||||
|
||||
npm install
|
||||
```
|
||||
|
||||
## 启动项目
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
|
||||
yarn dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# npm
|
||||
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 项目打包
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
|
||||
yarn build
|
||||
```
|
||||
|
||||
```sh
|
||||
# npm
|
||||
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 预览项目
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
|
||||
yarn preview
|
||||
```
|
||||
|
||||
```sh
|
||||
# npm
|
||||
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 体积分析
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
|
||||
yarn report
|
||||
```
|
||||
|
||||
```sh
|
||||
# npm
|
||||
|
||||
npm run report
|
||||
```
|
||||
|
||||
## 项目依赖
|
||||
|
||||
- [pinia](https://pinia.vuejs.org/) `全局状态管理器`
|
||||
- [@vueuse](https://vueuse.org/) `vue3 hooks`
|
||||
- [vue-router](https://router.vuejs.org/zh/) `router`
|
||||
- [axios](http://axios-js.com/zh-cn/docs/index.html) `ajax request`
|
||||
- [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html) `国际化`
|
||||
- [scrollreveal.js](https://scrollrevealjs.org/) `滚动加载动画`(暂时移除)
|
||||
- [crypto-js](https://github.com/brix/crypto-js) `加密`
|
||||
- [vite-svg-loader](https://github.com/jpkleemans/vite-svg-loader) `svg组件化`
|
||||
- [vite-plugin-svg-icons](https://github.com/vbenjs/vite-plugin-svg-icons/blob/main/README.zh_CN.md) `svg雪碧图`
|
||||
- [echarts5](https://echarts.apache.org/examples/zh/index.html#chart-type-line) `可视化`
|
||||
- [lodash-es](https://www.lodashjs.com/) `拓展方法`
|
||||
- 还有一些后续补充的,懒得写了。。。自己看项目依赖页面
|
||||
|
||||
## 基础组件
|
||||
|
||||
- `RayIcon` `svg icon`
|
||||
- `RayChart` 基于 `echarts5.x` 封装可视化组件
|
||||
- `RayTransitionComponent` 带过渡动画路由组件,效果与 `RouterView` 相同
|
||||
- `RayTable` 基于 `Naive UI DataTable` 组件封装,实现了一些小功能
|
||||
- `RayCollapseGrid` 基于 `Naive UI NGrid` 组件封装的可折叠操作栏
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
- locales: 国际化多语言入口(本项目采用 json 格式)
|
||||
|
||||
- assets: 项目静态资源入口
|
||||
|
||||
- component: 全局共用组件
|
||||
|
||||
- icons: 项目svg图标资源,需要配合 RayIcon 组件使用
|
||||
|
||||
- language: 国际化
|
||||
|
||||
- layout: 全局页面结构入口
|
||||
|
||||
- router: 路由表
|
||||
|
||||
- store: 全局状态管理入口
|
||||
|
||||
- styles: 全局公共样式入口
|
||||
|
||||
- types: 全局 type
|
||||
|
||||
- utils: 工具包
|
||||
|
||||
- views: 页面入口
|
||||
|
||||
- vite-plugin: 插件注册
|
||||
```
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
> 仅支持现代浏览器,不支持 `IE`
|
||||
|
||||
## 最后,希望大家搬砖愉快
|
||||
|
||||
## 贡献者
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://heartofyun.com"><img src="https://avatars.githubusercontent.com/u/40163747?v=4?s=100" width="100px;" alt="Cloud"/><br /><sub><b>Cloud</b></sub></a><br /><a href="#tool-yunkuangao" title="Tools">🔧</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
150
cfg.ts
Normal file
150
cfg.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-04-06
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统配置文件入口
|
||||
*
|
||||
* 配置范围:
|
||||
* - 构建: 开发构建、打包构建、预览构建、体积分析构建等
|
||||
* - 系统: 根路由、标题、浏览器标题、别名等
|
||||
* - 请求: 代理配置
|
||||
*
|
||||
* 如果需要新增相关内容, 需要在 src/types/modules/cfg.ts 中进行类型配置
|
||||
* ```
|
||||
* interface Config // config 内容类型配置
|
||||
*
|
||||
* interface AppConfig // __APP_CFG__ 内容配置
|
||||
* ```
|
||||
*
|
||||
* __APP_CFG__ 说明
|
||||
* ```
|
||||
* 该属性是用于全局注入的配置方法
|
||||
*
|
||||
* const { appPrimaryColor } = __APP_CFG__
|
||||
*
|
||||
* 以上例子展示, 从 __APP_CFG__ 中解构取出 appPrimaryColor 根路由配置信息
|
||||
* __APP_CFG__ 会被挂载于全局变量 `window` 下(vite define 默认是挂载于 window 下)
|
||||
* ```
|
||||
*/
|
||||
|
||||
import path from 'node:path'
|
||||
|
||||
import {
|
||||
HTMLTitlePlugin,
|
||||
buildOptions,
|
||||
mixinCSSPlugin,
|
||||
} from './vite-plugin/index'
|
||||
import { APP_THEME } from './src/appConfig/designConfig'
|
||||
import {
|
||||
PRE_LOADING_CONFIG,
|
||||
ROOT_ROUTE,
|
||||
SIDE_BAR_LOGO,
|
||||
} from './src/appConfig/appConfig'
|
||||
|
||||
import type { AppConfigExport } from '@/types/modules/cfg'
|
||||
|
||||
const config: AppConfigExport = {
|
||||
/** 公共基础路径配置, 如果为空则会默认以 '/' 填充 */
|
||||
base: '/ray-template/',
|
||||
/** 配置首屏加载信息 */
|
||||
preloadingConfig: PRE_LOADING_CONFIG,
|
||||
/** 默认主题色(不可省略, 必填), 也用于 ejs 注入 */
|
||||
appPrimaryColor: APP_THEME.APP_PRIMARY_COLOR,
|
||||
sideBarLogo: SIDE_BAR_LOGO,
|
||||
/**
|
||||
*
|
||||
* 预处理全局需要注入的 css 文件
|
||||
*
|
||||
* 预设:
|
||||
* - ./src/styles/mixins.scss
|
||||
* - ./src/styles/setting.scss
|
||||
* - ./src/styles/theme.scss
|
||||
*
|
||||
* 如果需要删除或者修改, 需要同步修改目录下的 css 文件
|
||||
*/
|
||||
mixinCSS: mixinCSSPlugin([
|
||||
'./src/styles/mixins.scss',
|
||||
'./src/styles/setting.scss',
|
||||
]),
|
||||
/**
|
||||
*
|
||||
* 版权信息
|
||||
*
|
||||
* 也可以当作页底设置, 看实际业务需求
|
||||
*/
|
||||
copyright: 'Copyright © 2022-present Ray',
|
||||
/**
|
||||
*
|
||||
* 浏览器标题
|
||||
*/
|
||||
title: HTMLTitlePlugin('Ray Template'),
|
||||
/**
|
||||
*
|
||||
* 配置 HMR 特定选项(端口、主机、路径和协议)
|
||||
*/
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 9527,
|
||||
open: false,
|
||||
https: false,
|
||||
strictPort: false,
|
||||
fs: {
|
||||
strict: false,
|
||||
allow: [],
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'url',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
'/office': {
|
||||
target: 'https://office.yka.one/',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/office/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
*
|
||||
* 打包相关配置
|
||||
*/
|
||||
buildOptions: buildOptions,
|
||||
/**
|
||||
*
|
||||
* 预设别名
|
||||
* - `@`: `src` 根目录
|
||||
* - `@use-utils`: `src/utils` 根目录
|
||||
* - `@use-api`: `src/axios/api` 根目录
|
||||
* - `@use-images`: `src/assets/images` 根目录
|
||||
*/
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: path.resolve(__dirname, './src'),
|
||||
},
|
||||
{
|
||||
find: '@use-utils',
|
||||
replacement: path.resolve(__dirname, './src/utils'),
|
||||
},
|
||||
{
|
||||
find: '@use-api',
|
||||
replacement: path.resolve(__dirname, './src/axios/api'),
|
||||
},
|
||||
{
|
||||
find: '@use-images',
|
||||
replacement: path.resolve(__dirname, './src/assets/images'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default config
|
21
commitlint.config.cjs
Normal file
21
commitlint.config.cjs
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'bug',
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'test',
|
||||
'chore',
|
||||
'revert',
|
||||
'merge',
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
106
index.html
Normal file
106
index.html
Normal file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/ray.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<style>
|
||||
:root {
|
||||
--preloading-tag-color: <%= preloadingConfig.tagColor %>;
|
||||
--preloading-title-color: <%= preloadingConfig.titleColor %>;
|
||||
--ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>;
|
||||
--ray-theme-primary-color: <%= appPrimaryColor.primaryColor %>;
|
||||
}
|
||||
|
||||
#pre-loading-animation {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
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;
|
||||
}
|
||||
|
||||
#pre-loading-animation .pre-loading-animation__wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#pre-loading-animation
|
||||
.pre-loading-animation__wrapper
|
||||
.pre-loading-animation__wrapper-title {
|
||||
font-size: 30px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.pre-loading-animation__wrapper-loading {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
|
||||
animation: rectangle infinite 1s ease-in-out -0.2s;
|
||||
|
||||
background-color: var(--preloading-tag-color);
|
||||
}
|
||||
|
||||
.pre-loading-animation__wrapper-loading:before,
|
||||
.pre-loading-animation__wrapper-loading:after {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
content: '';
|
||||
background-color: var(--preloading-tag-color);
|
||||
}
|
||||
|
||||
.pre-loading-animation__wrapper-loading:before {
|
||||
left: -14px;
|
||||
|
||||
animation: rectangle infinite 1s ease-in-out -0.4s;
|
||||
}
|
||||
|
||||
.pre-loading-animation__wrapper-loading:after {
|
||||
right: -14px;
|
||||
|
||||
animation: rectangle infinite 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes rectangle {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
height: 20px;
|
||||
box-shadow: 0 0 var(--preloading-tag-color);
|
||||
}
|
||||
|
||||
40% {
|
||||
height: 30px;
|
||||
box-shadow: 0 -20px var(--preloading-tag-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="pre-loading-animation">
|
||||
<div class="pre-loading-animation__wrapper">
|
||||
<div class="pre-loading-animation__wrapper-title">
|
||||
<%= preloadingConfig.title %>
|
||||
</div>
|
||||
<div class="pre-loading-animation__wrapper-loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
114
package.json
Normal file
114
package.json
Normal file
@ -0,0 +1,114 @@
|
||||
{
|
||||
"name": "ray-template",
|
||||
"private": false,
|
||||
"version": "4.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"dev-build": "vue-tsc --noEmit && vite build --mode development",
|
||||
"report": "vue-tsc --noEmit && vite build --mode report",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{vue,jsx,ts,tsx,json}": [
|
||||
"prettier --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"axios": "^1.2.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.1.1",
|
||||
"currency.js": "^2.0.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^5.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.34.4",
|
||||
"pinia": "^2.0.17",
|
||||
"pinia-plugin-persistedstate": "^2.4.0",
|
||||
"print-js": "^1.6.0",
|
||||
"qrcode.vue": "^3.3.4",
|
||||
"sass": "^1.54.3",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue": "^3.2.47",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@commitlint/config-conventional": "^17.4.2",
|
||||
"@intlify/unplugin-vue-i18n": "^0.5.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/scrollreveal": "^0.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
"@typescript-eslint/parser": "^5.61.0",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"depcheck": "^1.4.3",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard-with-typescript": "^23.0.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-n": "^15.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"postcss": "^8.1.0",
|
||||
"postcss-px-to-viewport": "^1.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup-plugin-visualizer": "^5.8.3",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"typescript": "^5.0.2",
|
||||
"unplugin-auto-import": "^0.11.0",
|
||||
"unplugin-vue-components": "^0.22.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-ejs": "^1.6.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-imp": "^2.3.1",
|
||||
"vite-plugin-inspect": "^0.7.26",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-svg-loader": "^3.4.0",
|
||||
"vue-tsc": "^1.4.2"
|
||||
},
|
||||
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
|
||||
"main": "index.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ray-template",
|
||||
"vue3.2模板",
|
||||
"vue3-tsx-vite-pinia",
|
||||
"ray template",
|
||||
"Ray Template",
|
||||
"admin template",
|
||||
"中后台模板"
|
||||
],
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"author": "Ray",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io#readme"
|
||||
}
|
31
postcss.config.cjs
Normal file
31
postcss.config.cjs
Normal file
@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {
|
||||
overrideBrowserslist: [
|
||||
'Android 4.1',
|
||||
'iOS 7.1',
|
||||
'Chrome > 31',
|
||||
'ff > 31',
|
||||
'ie >= 8',
|
||||
'last 10 versions',
|
||||
],
|
||||
grid: true,
|
||||
},
|
||||
// 'postcss-px-to-viewport': {
|
||||
// /** 视窗的宽度(设计稿的宽度) */
|
||||
// viewportWidth: 1920,
|
||||
// /** 视窗的高度(设计稿高度, 一般无需指定) */
|
||||
// viewportHeight: 1080,
|
||||
// /** 指定 px 转换为视窗单位值的小数位数 */
|
||||
// unitPrecision: 3,
|
||||
// /** 指定需要转换成的视窗单位 */
|
||||
// viewportUnit: 'vw',
|
||||
// /** 指定不转换为视窗单位的类 */
|
||||
// selectorBlackList: ['.ignore'],
|
||||
// /** 小于或等于 1px 不转换为视窗单位 */
|
||||
// minPixelValue: 1,
|
||||
// /** 允许在媒体查询中转换 px */
|
||||
// mediaQuery: false,
|
||||
// },
|
||||
},
|
||||
}
|
14
public/ray.svg
Normal file
14
public/ray.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg t="1659811416176" class="icon" viewBox="0 0 1147 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10290" width="64" height="64">
|
||||
<path d="M472.342857 968.685714c-73.828571 0-130.171429-12-172.114286-36.571428-38.742857-22.742857-84.914286-68.685714-84.914285-158.057143-2.971429-141.942857 109.714286-259.542857 251.657143-262.514286 141.942857-2.971429 259.542857 109.714286 262.514285 251.657143 0.114286 3.657143 0.114286 7.2 0 10.857143 0 89.371429-46.171429 135.2-84.914285 158.057143-41.942857 24.685714-98.285714 36.571429-172.228572 36.571428z" fill="#25467A" p-id="10291"></path>
|
||||
<path d="M766.742857 968.685714c-73.942857 0-130.171429-12-172.228571-36.571428-38.742857-22.742857-84.914286-68.685714-84.914286-158.057143 2.857143-141.942857 120.228571-254.742857 262.285714-252 137.942857 2.742857 249.142857 113.942857 252 252 0 89.371429-46.171429 135.2-84.914285 158.057143-42.057143 24.685714-98.4 36.571429-172.228572 36.571428z" fill="#25467A" p-id="10292"></path>
|
||||
<path d="M512 940.571429c-153.257143 0-268.457143-24-352.457143-73.257143-50.971429-29.942857-90.514286-69.714286-117.142857-118.057143C14.285714 698.285714 0 637.028571 0 567.085714c0.114286-282.857143 229.6-512 512.457143-511.885714 205.6 0.114286 391.2 123.2 471.314286 312.571429 26.742857 63.085714 40.342857 130.857143 40.228571 199.314285 0 69.828571-14.285714 131.2-42.4 182.171429-26.742857 48.342857-66.171429 88.114286-117.142857 118.057143C780.571429 916.685714 665.257143 940.571429 512 940.571429z" fill="#25467A" p-id="10293"></path>
|
||||
<path d="M48 567.085714c0-256.228571 207.771429-464 464-464s464 207.771429 464 464S768.228571 892.571429 512 892.571429 48 823.314286 48 567.085714z" fill="#FFF3E0" p-id="10294"></path>
|
||||
<path d="M472.342857 943.542857c-69.257143 0-121.371429-10.857143-159.428571-33.142857-48.114286-28.228571-72.457143-74.171429-72.457143-136.342857 0-127.885714 104-232 232-232s232 104 232 232c0 62.171429-24.342857 108.114286-72.571429 136.342857-38.057143 22.4-90.171429 33.142857-159.542857 33.142857z" fill="#25467A" p-id="10295"></path>
|
||||
<path d="M263.314286 774.057143c0-115.428571 93.485714-209.142857 209.028571-209.142857 115.428571 0 209.142857 93.485714 209.142857 209.028571v0.114286c0 115.428571-93.6 146.628571-209.142857 146.628571s-209.028571-31.085714-209.028571-146.628571z" fill="#FFF3E0" p-id="10296"></path>
|
||||
<path d="M472.342857 652.8c-73.371429 0-132.8 51.771429-132.8 115.657143 0 18.514286 5.028571 33.142857 13.942857 44.571428-1.714286 10.971429-2.628571 22.057143-2.742857 33.142858 0 32.8 9.942857 38.971429 22.285714 38.971428s22.285714-6.285714 22.285715-38.971428c0-2.057143 0-4.228571-0.114286-6.285715 21.714286 7.085714 48.457143 9.714286 77.142857 9.714286 73.371429 0 132.8-17.257143 132.8-81.142857s-59.428571-115.657143-132.8-115.657143z" fill="#388E3C" p-id="10297"></path>
|
||||
<path d="M766.742857 943.542857c-69.257143 0-121.371429-10.857143-159.428571-33.142857-48.114286-28.228571-72.571429-74.171429-72.571429-136.342857 0-127.885714 104.114286-232 232-232s232 104 232 232c0 62.171429-24.342857 108.114286-72.571428 136.342857-38.057143 22.4-90.171429 33.142857-159.428572 33.142857z" fill="#25467A" p-id="10298"></path>
|
||||
<path d="M557.6 774.057143c0-115.428571 93.6-209.142857 209.142857-209.142857s209.142857 93.6 209.142857 209.142857c0 115.428571-93.6 146.628571-209.142857 146.628571s-209.142857-31.085714-209.142857-146.628571z" fill="#FFF3E0" p-id="10299"></path>
|
||||
<path d="M766.742857 652.8c-73.371429 0-132.8 51.771429-132.8 115.657143 0 18.514286 5.028571 33.142857 13.942857 44.571428-1.714286 10.971429-2.628571 22.057143-2.742857 33.142858 0 32.8 9.942857 38.971429 22.285714 38.971428s22.285714-6.285714 22.285715-38.971428c0-2.057143 0-4.228571-0.114286-6.285715 21.714286 7.085714 48.342857 9.714286 77.142857 9.714286 73.371429 0 132.8-17.257143 132.8-81.142857S840 652.8 766.742857 652.8z" fill="#FBC02D" p-id="10300"></path>
|
||||
<path d="M401.6 486.857143c-38.171429 0-69.142857-30.971429-69.142857-69.257143 0-7.657143 6.171429-13.828571 13.828571-13.828571 7.314286 0 13.485714 5.714286 13.828572 13.028571v0.8c-0.342857 22.971429 17.942857 41.828571 40.8 42.171429s41.828571-17.942857 42.171428-40.8v-1.371429c-0.228571-7.657143 5.828571-14.057143 13.371429-14.285714s14.057143 5.828571 14.285714 13.371428v0.8c0.114286 38.4-30.857143 69.371429-69.142857 69.371429zM221.714286 306.971429c-0.342857 22.971429 17.942857 41.828571 40.8 42.171428s41.828571-17.942857 42.171428-40.8v-1.371428c0.342857-22.971429-17.942857-41.828571-40.8-42.171429-22.971429-0.342857-41.828571 17.942857-42.171428 40.8v1.371429zM498.514286 306.971429c-0.342857 22.971429 17.828571 41.828571 40.8 42.285714 22.971429 0.342857 41.828571-17.828571 42.285714-40.8v-1.485714c0.342857-22.971429-17.942857-41.828571-40.8-42.171429-22.971429-0.342857-41.828571 17.942857-42.171429 40.8-0.114286 0.457143-0.114286 0.914286-0.114285 1.371429z" fill="#25467A" p-id="10301"></path>
|
||||
<path d="M207.885714 376.228571h-55.314285c-15.314286-0.342857-27.885714 11.771429-28.228572 27.085715-0.342857 15.314286 11.771429 27.885714 27.085714 28.228571H208c15.314286 0.342857 27.885714-11.771429 28.228571-27.085714 0.342857-15.314286-11.771429-27.885714-27.085714-28.228572h-1.257143z m442.857143 0H595.428571c-15.314286 0-27.657143 12.342857-27.657142 27.657143s12.342857 27.657143 27.657142 27.657143h55.314286c15.314286 0.342857 27.885714-11.771429 28.228572-27.085714 0.342857-15.314286-11.771429-27.885714-27.085715-28.228572h-1.142857z" fill="#F8BBD0" p-id="10302"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 5.4 KiB |
105
src/App.tsx
Normal file
105
src/App.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import RayGlobalProvider from '@/components/RayGlobalProvider/index'
|
||||
import { RouterView } from 'vue-router'
|
||||
import GlobalSpin from '@/spin/index'
|
||||
import LockScreen from '@/components/AppComponents/AppLockScreen/index'
|
||||
|
||||
import { getStorage } from '@/utils/cache'
|
||||
import { get } from 'lodash-es'
|
||||
import { useSetting } from '@/store'
|
||||
import { addClass, removeClass, addStyle, colorToRgba } from '@/utils/element'
|
||||
|
||||
import type { SettingState } from '@/store/modules/setting/type'
|
||||
|
||||
const App = defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
const settingStore = useSetting()
|
||||
|
||||
const { themeValue } = storeToRefs(settingStore)
|
||||
|
||||
/** 同步主题色变量至 body, 如果未获取到缓存值则已默认值填充 */
|
||||
const syncPrimaryColorToBody = () => {
|
||||
const {
|
||||
appPrimaryColor: { primaryColor, primaryFadeColor },
|
||||
} = __APP_CFG__ // 默认主题色
|
||||
const body = document.body
|
||||
|
||||
const primaryColorOverride = getStorage<SettingState>(
|
||||
'piniaSettingStore',
|
||||
'localStorage',
|
||||
)
|
||||
|
||||
if (primaryColorOverride) {
|
||||
const _p = get(
|
||||
primaryColorOverride,
|
||||
'primaryColorOverride.common.primaryColor',
|
||||
primaryColor,
|
||||
)
|
||||
const _fp = colorToRgba(_p, 0.3)
|
||||
|
||||
/** 设置全局主题色 css 变量 */
|
||||
body.style.setProperty('--ray-theme-primary-color', _p)
|
||||
body.style.setProperty(
|
||||
'--ray-theme-primary-fade-color',
|
||||
_fp || primaryFadeColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 隐藏加载动画 */
|
||||
const hiddenLoadingAnimation = () => {
|
||||
/** pre-loading-animation 是默认 id */
|
||||
const el = document.getElementById('pre-loading-animation')
|
||||
|
||||
if (el) {
|
||||
addStyle(el, {
|
||||
display: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
syncPrimaryColorToBody()
|
||||
hiddenLoadingAnimation()
|
||||
|
||||
/** 切换主题时, 同步更新 body class 以便于进行自定义 css 配置 */
|
||||
watch(
|
||||
() => themeValue.value,
|
||||
(newData) => {
|
||||
/**
|
||||
*
|
||||
* 初始化时根据当前主题色进行初始化 body 的 class 属性
|
||||
*
|
||||
* 根据 themeValue 进行初始化
|
||||
*/
|
||||
const body = document.body
|
||||
const darkClassName = 'ray-template--dark'
|
||||
const lightClassName = 'ray-template--light'
|
||||
|
||||
newData
|
||||
? removeClass(body, lightClassName)
|
||||
: removeClass(body, darkClassName)
|
||||
|
||||
addClass(body, newData ? darkClassName : lightClassName)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<RayGlobalProvider>
|
||||
<LockScreen />
|
||||
|
||||
<GlobalSpin>
|
||||
{{
|
||||
default: () => <RouterView />,
|
||||
description: () => 'lodaing...',
|
||||
}}
|
||||
</GlobalSpin>
|
||||
</RayGlobalProvider>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default App
|
106
src/appConfig/appConfig.ts
Normal file
106
src/appConfig/appConfig.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-23
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 系统配置 */
|
||||
|
||||
import type {
|
||||
LayoutSideBarLogo,
|
||||
PreloadingConfig,
|
||||
RootRoute,
|
||||
} from '@/types/modules/cfg'
|
||||
import type { AppMenuConfig, AppKeepAlive } from '@/types/modules/appConfig'
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统缓存
|
||||
*
|
||||
* 说明:
|
||||
* - setupKeepAlive: 是否启用系统页面缓存, 设置为 false 则关闭系统页面缓存
|
||||
* - keepAliveExclude: 排除哪些页面不缓存
|
||||
* - maxKeepAliveLength: 最大缓存页面数量
|
||||
*/
|
||||
export const APP_KEEP_ALIVE: Readonly<AppKeepAlive> = {
|
||||
setupKeepAlive: true,
|
||||
keepAliveExclude: [],
|
||||
maxKeepAliveLength: 5,
|
||||
}
|
||||
|
||||
/** 首屏加载信息配置 */
|
||||
export const PRE_LOADING_CONFIG: PreloadingConfig = {
|
||||
title: 'Ray Template',
|
||||
tagColor: '#ff6700',
|
||||
titleColor: '#2d8cf0',
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 配置根页面
|
||||
* 该项目所有重定向至首页, 都依赖该配置项
|
||||
*
|
||||
* 如果修改了该项目的首页路由配置, 需要更改该配置项, 以免重定向首页操作出现错误
|
||||
*/
|
||||
export const ROOT_ROUTE: Readonly<RootRoute> = {
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* icon: LOGO 图标, 依赖 `RayIcon` 实现(如果为空则不会渲染图标)
|
||||
* title: LOGO 标题
|
||||
* url: 点击跳转地址, 如果不配置该属性, 则不会触发跳转
|
||||
* jumpType: 跳转类型(station: 项目内跳转, outsideStation: 新页面打开)
|
||||
*
|
||||
* 如果不设置该属性或者为空, 则不会渲染 LOGO
|
||||
*/
|
||||
export const SIDE_BAR_LOGO: LayoutSideBarLogo = {
|
||||
icon: 'ray',
|
||||
title: 'Ray Template',
|
||||
url: '/dashboard',
|
||||
jumpType: 'station',
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统菜单折叠配置
|
||||
*
|
||||
* MENU_COLLAPSED_WIDTH 配置仅当 MENU_COLLAPSED_MODE 为 width 风格时才有效
|
||||
*
|
||||
* MENU_COLLAPSED_MODE:
|
||||
* - transform: 边栏将只会移动它的位置而不会改变宽度
|
||||
* - width: Sider 的内容宽度将会被实际改变
|
||||
* MENU_COLLAPSED_ICON_SIZE 配置菜单未折叠时图标的大小
|
||||
* MENU_COLLAPSED_INDENT 配置菜单每级的缩进
|
||||
* MENU_ACCORDION 手风琴模式
|
||||
*/
|
||||
export const APP_MENU_CONFIG: Readonly<AppMenuConfig> = {
|
||||
MENU_COLLAPSED_WIDTH: 64,
|
||||
MENU_COLLAPSED_MODE: 'width',
|
||||
MENU_COLLAPSED_ICON_SIZE: 22,
|
||||
MENU_COLLAPSED_INDENT: 24,
|
||||
MENU_ACCORDION: false,
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统默认缓存 key 配置
|
||||
* 仅暴露部分系统获取缓存配置, 其余 key 暂不开放
|
||||
*
|
||||
* 说明:
|
||||
* - signin: 登陆信息缓存 key
|
||||
* - localeLanguage: 国际化默认缓存 key
|
||||
* - token: token key
|
||||
*/
|
||||
export const APP_CATCH_KEY = {
|
||||
signin: 'signin',
|
||||
localeLanguage: 'localeLanguage',
|
||||
token: 'token',
|
||||
} as const
|
63
src/appConfig/designConfig.ts
Normal file
63
src/appConfig/designConfig.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-19
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 系统颜色风格配置入口 */
|
||||
|
||||
import type { AppTheme } from '@/types/modules/cfg'
|
||||
|
||||
export const APP_THEME: AppTheme = {
|
||||
/**
|
||||
*
|
||||
* 系统主题颜色预设色盘
|
||||
* 支持 RGBA、RGB、十六进制
|
||||
*/
|
||||
APP_THEME_COLOR: [
|
||||
'#2d8cf0',
|
||||
'#0960bd',
|
||||
'#536dfe',
|
||||
'#ff5c93',
|
||||
'#ee4f12',
|
||||
'#9c27b0',
|
||||
'#ff9800',
|
||||
'#18A058',
|
||||
],
|
||||
/** 系统主题色 */
|
||||
APP_PRIMARY_COLOR: {
|
||||
/** 主题色 */
|
||||
primaryColor: '#2d8cf0',
|
||||
/** 主题辅助色(用于整体 hover、active 等之类颜色) */
|
||||
primaryFadeColor: 'rgba(45, 140, 240, 0.3)',
|
||||
},
|
||||
/**
|
||||
*
|
||||
* 配置系统 naive-ui 主题色
|
||||
* 官网文档地址: <https://www.naiveui.com/zh-CN/dark/docs/customize-theme>
|
||||
*
|
||||
* 注意:
|
||||
* - APP_PRIMARY_COLOR common 配置优先级大于该配置
|
||||
*
|
||||
* 如果需要定制化整体组件样式, 配置示例
|
||||
* ```
|
||||
* const themeOverrides: GlobalThemeOverrides = {
|
||||
* common: {
|
||||
* primaryColor: '#FF0000',
|
||||
* },
|
||||
* Button: {
|
||||
* textColor: '#FF0000',
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 具体自行查看官网, 还有模式更佳丰富的 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>
|
||||
*/
|
||||
APP_NAIVE_UI_THEME_OVERRIDES: {},
|
||||
}
|
62
src/appConfig/localConfig.ts
Normal file
62
src/appConfig/localConfig.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-19
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 国际化相关配置 */
|
||||
|
||||
import type { DayjsLocal, DayjsLocalMap } from '@/dayjs/type'
|
||||
import type { AppLocalesDropdownMixedOption } from '@/locales/type'
|
||||
|
||||
/**
|
||||
*
|
||||
* 语言包语种添加后, 需要在此文件配置语言包
|
||||
* 该配置中的 key 也会影响 naiveLocales 方法, 配置后请仔细核对一下
|
||||
*
|
||||
* 添加新的语言包后, 如果需要其类型提示, 需要在 CurrentAppMessages 中添加新的类型
|
||||
*/
|
||||
export const LOCAL_OPTIONS: AppLocalesDropdownMixedOption[] = [
|
||||
{
|
||||
key: 'zh-CN',
|
||||
label: '中文(简体)',
|
||||
},
|
||||
{
|
||||
key: 'en-US',
|
||||
label: 'English(US)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统默认语言
|
||||
*
|
||||
* 配置时应该与 LOCAL_OPTIONS 的 key 一致
|
||||
*/
|
||||
export const SYSTEM_DEFAULT_LOCAL = 'zh-CN'
|
||||
|
||||
/**
|
||||
*
|
||||
* dayjs 默认语言格式
|
||||
* 默认为英文(en)
|
||||
*
|
||||
* 系统默认设置为中文(大陆-简体)
|
||||
*/
|
||||
export const DEFAULT_DAYJS_LOCAL: DayjsLocal = 'zh-cn'
|
||||
|
||||
/**
|
||||
*
|
||||
* i18n 国际化配置与 dayjs 配置的映射入口
|
||||
*
|
||||
* key 应该与 LOCAL_OPTIONS key 一致
|
||||
* 配置时请仔细检查
|
||||
*/
|
||||
export const DAYJS_LOCAL_MAP: DayjsLocalMap = {
|
||||
'zh-CN': 'zh-cn',
|
||||
'en-US': 'en',
|
||||
}
|
22
src/appConfig/regexConfig.ts
Normal file
22
src/appConfig/regexConfig.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-12
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 正则入口
|
||||
* 系统公共正则, 配置在该文件中
|
||||
*/
|
||||
|
||||
export const APP_REGEX: Record<string, RegExp> = {
|
||||
/** css 尺寸单位匹配 */
|
||||
validerCSSUnit:
|
||||
/^\d+(\.\d+)?(px|em|rem|%|vw|vh|vmin|vmax|cm|mm|in|pt|pc|ch|ex|q|s|ms|deg|rad|turn|grad|hz|khz|dpi|dpcm|dppx|fr|auto)$/,
|
||||
}
|
22
src/appConfig/requestConfig.ts
Normal file
22
src/appConfig/requestConfig.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-02
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import type { AxiosConfig } from '@/types/modules/appConfig'
|
||||
|
||||
/** axios 相关配置 */
|
||||
export const AXIOS_CONFIG: AxiosConfig = {
|
||||
baseURL: '', // `import.meta.env`,
|
||||
withCredentials: false, // 是否允许跨域携带 `cookie`
|
||||
timeout: 5 * 1000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
55
src/appConfig/routerConfig.ts
Normal file
55
src/appConfig/routerConfig.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-19
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** vue-router 相关配置入口 */
|
||||
|
||||
import type { LayoutInst } from 'naive-ui'
|
||||
|
||||
/**
|
||||
*
|
||||
* 内容区域 ref 注册
|
||||
* 可以控制内容区域当前滚动位置
|
||||
* 如果你需要在切换路由时候配置自定义滚动到某个视图区域时, 可以使用该属性提供的方法(scrollTo)
|
||||
*
|
||||
* 请注意
|
||||
* 如果你动态的添加了某个属性后, 希望控制滚动条滚动到某个区域时, 应该注意 dom 挂载后再执行该方法
|
||||
* @example
|
||||
* ```ts
|
||||
* nextTick().then(() => {
|
||||
* LAYOUT_CONTENT_REF.value?.scrollTo()
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const LAYOUT_CONTENT_REF = ref<LayoutInst>()
|
||||
|
||||
/** 是否启用路由切换时顶部加载条 */
|
||||
export const SETUP_ROUTER_LOADING_BAR = true
|
||||
|
||||
/** 是否启用路由守卫, 如果设置为 false 则不会触发路由切换校验 */
|
||||
export const SETUP_ROUTER_GUARD = true
|
||||
|
||||
/**
|
||||
*
|
||||
* 路由白名单(不进行权限校验路由)
|
||||
*
|
||||
* 路由表单白名单
|
||||
*
|
||||
* 如果需要启用该功能, 则需要配置路由 name 属性, 并且需要一一对应(对大小写敏感)
|
||||
* 并且在配置 route name 属性时, 如果 name 类型为 symbol 的话, 会认为该路由永远不与白名单列表进行匹配
|
||||
*/
|
||||
export const WHITE_ROUTES: string[] = ['RLogin', 'ErrorPage', 'RayTemplateDoc']
|
||||
|
||||
/**
|
||||
*
|
||||
* 超级管理员
|
||||
* 配置默认超级管理员, 默认拥有全部最高权限
|
||||
*/
|
||||
export const SUPER_ADMIN: (string | number)[] = ['admin']
|
14
src/assets/images/ray.svg
Normal file
14
src/assets/images/ray.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg t="1659811416176" class="icon" viewBox="0 0 1147 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10290" width="64" height="64">
|
||||
<path d="M472.342857 968.685714c-73.828571 0-130.171429-12-172.114286-36.571428-38.742857-22.742857-84.914286-68.685714-84.914285-158.057143-2.971429-141.942857 109.714286-259.542857 251.657143-262.514286 141.942857-2.971429 259.542857 109.714286 262.514285 251.657143 0.114286 3.657143 0.114286 7.2 0 10.857143 0 89.371429-46.171429 135.2-84.914285 158.057143-41.942857 24.685714-98.285714 36.571429-172.228572 36.571428z" fill="#25467A" p-id="10291"></path>
|
||||
<path d="M766.742857 968.685714c-73.942857 0-130.171429-12-172.228571-36.571428-38.742857-22.742857-84.914286-68.685714-84.914286-158.057143 2.857143-141.942857 120.228571-254.742857 262.285714-252 137.942857 2.742857 249.142857 113.942857 252 252 0 89.371429-46.171429 135.2-84.914285 158.057143-42.057143 24.685714-98.4 36.571429-172.228572 36.571428z" fill="#25467A" p-id="10292"></path>
|
||||
<path d="M512 940.571429c-153.257143 0-268.457143-24-352.457143-73.257143-50.971429-29.942857-90.514286-69.714286-117.142857-118.057143C14.285714 698.285714 0 637.028571 0 567.085714c0.114286-282.857143 229.6-512 512.457143-511.885714 205.6 0.114286 391.2 123.2 471.314286 312.571429 26.742857 63.085714 40.342857 130.857143 40.228571 199.314285 0 69.828571-14.285714 131.2-42.4 182.171429-26.742857 48.342857-66.171429 88.114286-117.142857 118.057143C780.571429 916.685714 665.257143 940.571429 512 940.571429z" fill="#25467A" p-id="10293"></path>
|
||||
<path d="M48 567.085714c0-256.228571 207.771429-464 464-464s464 207.771429 464 464S768.228571 892.571429 512 892.571429 48 823.314286 48 567.085714z" fill="#FFF3E0" p-id="10294"></path>
|
||||
<path d="M472.342857 943.542857c-69.257143 0-121.371429-10.857143-159.428571-33.142857-48.114286-28.228571-72.457143-74.171429-72.457143-136.342857 0-127.885714 104-232 232-232s232 104 232 232c0 62.171429-24.342857 108.114286-72.571429 136.342857-38.057143 22.4-90.171429 33.142857-159.542857 33.142857z" fill="#25467A" p-id="10295"></path>
|
||||
<path d="M263.314286 774.057143c0-115.428571 93.485714-209.142857 209.028571-209.142857 115.428571 0 209.142857 93.485714 209.142857 209.028571v0.114286c0 115.428571-93.6 146.628571-209.142857 146.628571s-209.028571-31.085714-209.028571-146.628571z" fill="#FFF3E0" p-id="10296"></path>
|
||||
<path d="M472.342857 652.8c-73.371429 0-132.8 51.771429-132.8 115.657143 0 18.514286 5.028571 33.142857 13.942857 44.571428-1.714286 10.971429-2.628571 22.057143-2.742857 33.142858 0 32.8 9.942857 38.971429 22.285714 38.971428s22.285714-6.285714 22.285715-38.971428c0-2.057143 0-4.228571-0.114286-6.285715 21.714286 7.085714 48.457143 9.714286 77.142857 9.714286 73.371429 0 132.8-17.257143 132.8-81.142857s-59.428571-115.657143-132.8-115.657143z" fill="#388E3C" p-id="10297"></path>
|
||||
<path d="M766.742857 943.542857c-69.257143 0-121.371429-10.857143-159.428571-33.142857-48.114286-28.228571-72.571429-74.171429-72.571429-136.342857 0-127.885714 104.114286-232 232-232s232 104 232 232c0 62.171429-24.342857 108.114286-72.571428 136.342857-38.057143 22.4-90.171429 33.142857-159.428572 33.142857z" fill="#25467A" p-id="10298"></path>
|
||||
<path d="M557.6 774.057143c0-115.428571 93.6-209.142857 209.142857-209.142857s209.142857 93.6 209.142857 209.142857c0 115.428571-93.6 146.628571-209.142857 146.628571s-209.142857-31.085714-209.142857-146.628571z" fill="#FFF3E0" p-id="10299"></path>
|
||||
<path d="M766.742857 652.8c-73.371429 0-132.8 51.771429-132.8 115.657143 0 18.514286 5.028571 33.142857 13.942857 44.571428-1.714286 10.971429-2.628571 22.057143-2.742857 33.142858 0 32.8 9.942857 38.971429 22.285714 38.971428s22.285714-6.285714 22.285715-38.971428c0-2.057143 0-4.228571-0.114286-6.285715 21.714286 7.085714 48.342857 9.714286 77.142857 9.714286 73.371429 0 132.8-17.257143 132.8-81.142857S840 652.8 766.742857 652.8z" fill="#FBC02D" p-id="10300"></path>
|
||||
<path d="M401.6 486.857143c-38.171429 0-69.142857-30.971429-69.142857-69.257143 0-7.657143 6.171429-13.828571 13.828571-13.828571 7.314286 0 13.485714 5.714286 13.828572 13.028571v0.8c-0.342857 22.971429 17.942857 41.828571 40.8 42.171429s41.828571-17.942857 42.171428-40.8v-1.371429c-0.228571-7.657143 5.828571-14.057143 13.371429-14.285714s14.057143 5.828571 14.285714 13.371428v0.8c0.114286 38.4-30.857143 69.371429-69.142857 69.371429zM221.714286 306.971429c-0.342857 22.971429 17.942857 41.828571 40.8 42.171428s41.828571-17.942857 42.171428-40.8v-1.371428c0.342857-22.971429-17.942857-41.828571-40.8-42.171429-22.971429-0.342857-41.828571 17.942857-42.171428 40.8v1.371429zM498.514286 306.971429c-0.342857 22.971429 17.828571 41.828571 40.8 42.285714 22.971429 0.342857 41.828571-17.828571 42.285714-40.8v-1.485714c0.342857-22.971429-17.942857-41.828571-40.8-42.171429-22.971429-0.342857-41.828571 17.942857-42.171429 40.8-0.114286 0.457143-0.114286 0.914286-0.114285 1.371429z" fill="#25467A" p-id="10301"></path>
|
||||
<path d="M207.885714 376.228571h-55.314285c-15.314286-0.342857-27.885714 11.771429-28.228572 27.085715-0.342857 15.314286 11.771429 27.885714 27.085714 28.228571H208c15.314286 0.342857 27.885714-11.771429 28.228571-27.085714 0.342857-15.314286-11.771429-27.885714-27.085714-28.228572h-1.257143z m442.857143 0H595.428571c-15.314286 0-27.657143 12.342857-27.657142 27.657143s12.342857 27.657143 27.657142 27.657143h55.314286c15.314286 0.342857 27.885714-11.771429 28.228572-27.085714 0.342857-15.314286-11.771429-27.885714-27.085715-28.228572h-1.142857z" fill="#F8BBD0" p-id="10302"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 5.4 KiB |
71
src/axios/README.md
Normal file
71
src/axios/README.md
Normal file
@ -0,0 +1,71 @@
|
||||
## 说明
|
||||
|
||||
> axios 包,全局的 axios 使用入口
|
||||
> `src/axios/instance.ts` 文件为 axios 实例文件,应该不可更改,如果需要有拦截器相关的操作,应该在 `src/axios/inject` 文件下按照需求在对应注册器中进行注册相关方法。该项目将 axios instance 与 axios interceptor 进行解耦,避免实例文件的臃肿。
|
||||
|
||||
## 工具函数
|
||||
|
||||
- BeforeFetchFunction
|
||||
- FetchErrorFunction
|
||||
|
||||
> 两个工具函数方便类型推导。
|
||||
|
||||
## 约束
|
||||
|
||||
### 拦截器添加
|
||||
|
||||
> 这里以请求拦截器添加为示例
|
||||
|
||||
```ts
|
||||
/**
|
||||
*
|
||||
* 拦截器说明
|
||||
*
|
||||
* 注册一个根据系统当前环境是否携带测试环境请求拦截器
|
||||
*/
|
||||
|
||||
import { appendRequestHeaders } from '@/axios/helper/axiosCopilot'
|
||||
|
||||
import type {
|
||||
RequestInterceptorConfig,
|
||||
BeforeFetchFunction,
|
||||
FetchErrorFunction,
|
||||
} from '@/axios/type'
|
||||
|
||||
const injectRequestHeaderOfEnv: BeforeFetchFunction<
|
||||
RequestInterceptorConfig
|
||||
> = (ins, mode) => {
|
||||
if (mode === 'development') {
|
||||
appendRequestHeaders(ins, [
|
||||
{
|
||||
key: 'Development-Mode',
|
||||
value: 'development',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 在 setupRequestInterceptor 中注册
|
||||
*/
|
||||
|
||||
export const setupRequestInterceptor = () => {
|
||||
setImplement(
|
||||
'implementRequestInterceptorArray',
|
||||
[injectRequestHeaderOfEnv],
|
||||
'ok',
|
||||
)
|
||||
}
|
||||
|
||||
/** 至此完成了请求拦截器的注册 */
|
||||
```
|
||||
|
||||
### 注册器
|
||||
|
||||
> 每个类型注册器都有两个方法,用于注册拦截器方法。都以 setupXXX 开头命名。注册器以队列形式管理拦截器方法,所以可能需要注意执行顺序。如果有新的拦截器方法,需要根据其使用场景在对应的注册器中注册(注册器第二个参数中注册)。
|
||||
|
||||
- 请求注册器: setupRequestInterceptor
|
||||
- 请求错误注册器: setupRequestErrorInterceptor
|
||||
- 响应注册器: setupResponseInterceptor
|
||||
- 响应错误注册器: setupResponseErrorInterceptor
|
44
src/axios/api/test.ts
Normal file
44
src/axios/api/test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-31
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 该方法演示如何使用 axios
|
||||
*
|
||||
* 示范如何完整批注响应体及其数据:
|
||||
*
|
||||
* ```
|
||||
* const demoRequest = () => {
|
||||
* return {} as AxiosResponseBody<AxiosTestResponse>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import useRequest from '@/axios/instance'
|
||||
|
||||
import type { AxiosResponseBody } from '@/types/modules/axios'
|
||||
|
||||
interface AxiosTestResponse extends UnknownObjectKey {
|
||||
data: UnknownObjectKey[]
|
||||
city?: string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns 测试
|
||||
*
|
||||
* @medthod get
|
||||
*/
|
||||
export const onAxiosTest = async (city: string) => {
|
||||
return useRequest<AxiosTestResponse>({
|
||||
url: `https://www.tianqiapi.com/api?version=v9&appid=23035354&appsecret=8YvlPNrz&city=${city}`,
|
||||
})
|
||||
}
|
35
src/axios/helper/axiosCopilot.ts
Normal file
35
src/axios/helper/axiosCopilot.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-02
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** axios 拦截器工具 */
|
||||
|
||||
import type { RawAxiosRequestHeaders, AxiosRequestConfig } from 'axios'
|
||||
import type { RequestHeaderOptions } from '../type'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param instance axios instance
|
||||
* @param options axios headers options
|
||||
*
|
||||
* @remark 自定义 `axios` 请求头配置
|
||||
*/
|
||||
export const appendRequestHeaders = <T = unknown>(
|
||||
instance: AxiosRequestConfig<T>,
|
||||
options: RequestHeaderOptions[],
|
||||
) => {
|
||||
if (instance) {
|
||||
const requestHeaders = instance.headers as RawAxiosRequestHeaders
|
||||
|
||||
options.forEach((curr) => {
|
||||
requestHeaders[curr.key] = curr.value
|
||||
})
|
||||
}
|
||||
}
|
82
src/axios/helper/canceler.ts
Normal file
82
src/axios/helper/canceler.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-02-27
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 自动取消重复请求
|
||||
*
|
||||
* 可以根据自己项目进行定制化配置
|
||||
*/
|
||||
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
export default class RequestCanceler {
|
||||
pendingRequest: Map<string, AbortController>
|
||||
|
||||
constructor() {
|
||||
this.pendingRequest = new Map<string, AbortController>()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config 请求体 config
|
||||
* @returns 返回当前请求拼接 key
|
||||
*
|
||||
* @remark 将当前请求 config 生成 request key
|
||||
*/
|
||||
generateRequestKey(config: AxiosRequestConfig): string {
|
||||
const { method, url } = config
|
||||
|
||||
return [
|
||||
url || '',
|
||||
method || '',
|
||||
JSON.stringify(config.params),
|
||||
JSON.stringify(config.data),
|
||||
].join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config axios request config
|
||||
*
|
||||
* @remark 给请求体添加 signal 属性, 用于取消请求
|
||||
*/
|
||||
addPendingRequest(config: AxiosRequestConfig) {
|
||||
const requestKey = this.generateRequestKey(config)
|
||||
|
||||
if (!this.pendingRequest.has(requestKey)) {
|
||||
const controller = new AbortController()
|
||||
|
||||
config.signal = controller.signal
|
||||
|
||||
this.pendingRequest.set(requestKey, controller)
|
||||
} else {
|
||||
// 如果已经有该 key 则重新挂载 signal
|
||||
config.signal = this.pendingRequest.get(requestKey)?.signal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config axios request config
|
||||
*
|
||||
* @remark 取消该请求, 并且清除 map 中对应 generateRequestKey value
|
||||
*/
|
||||
removePendingRequest(config: AxiosRequestConfig) {
|
||||
const requestKey = this.generateRequestKey(config)
|
||||
|
||||
if (this.pendingRequest.has(requestKey)) {
|
||||
this.pendingRequest.get(requestKey)!.abort()
|
||||
|
||||
this.pendingRequest.delete(requestKey)
|
||||
}
|
||||
}
|
||||
}
|
145
src/axios/helper/interceptor.ts
Normal file
145
src/axios/helper/interceptor.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-05
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* axios 拦截器注入
|
||||
*
|
||||
* 请求拦截器、响应拦截器
|
||||
* 暴露启动方法调用所有已注册方法
|
||||
*
|
||||
* 该拦截器仅适合放置公共的 axios 拦截器操作, 并且采用队列形式管理请求拦截器的注入
|
||||
* 所以在使用的时候, 需要按照约定格式进行参数传递
|
||||
*/
|
||||
|
||||
import RequestCanceler from '@/axios/helper/canceler'
|
||||
import { getAppEnvironment } from '@use-utils/hook'
|
||||
|
||||
import type {
|
||||
RequestInterceptorConfig,
|
||||
ResponseInterceptorConfig,
|
||||
ImplementQueue,
|
||||
ErrorImplementQueue,
|
||||
FetchType,
|
||||
AxiosFetchInstance,
|
||||
AxiosFetchError,
|
||||
} from '@/axios/type'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
/** 当前请求的实例 */
|
||||
const axiosFetchInstance: AxiosFetchInstance = {
|
||||
requestInstance: null,
|
||||
responseInstance: null,
|
||||
}
|
||||
/** 请求失败返回值 */
|
||||
const axiosFetchError: AxiosFetchError = {
|
||||
requestError: null,
|
||||
responseError: null,
|
||||
}
|
||||
/** 请求队列(区分 reslove 与 reject 状态) */
|
||||
const implement: ImplementQueue = {
|
||||
implementRequestInterceptorArray: [],
|
||||
implementResponseInterceptorArray: [],
|
||||
}
|
||||
const errorImplement: ErrorImplementQueue = {
|
||||
implementRequestInterceptorErrorArray: [],
|
||||
implementResponseInterceptorErrorArray: [],
|
||||
}
|
||||
/** 取消器实例 */
|
||||
export const axiosCanceler = new RequestCanceler()
|
||||
|
||||
export const useAxiosInterceptor = () => {
|
||||
/** 创建拦截器实例 */
|
||||
const createAxiosInstance = (
|
||||
instance: RequestInterceptorConfig | ResponseInterceptorConfig,
|
||||
instanceKey: keyof AxiosFetchInstance,
|
||||
) => {
|
||||
instanceKey === 'requestInstance'
|
||||
? (axiosFetchInstance['requestInstance'] =
|
||||
instance as RequestInterceptorConfig)
|
||||
: (axiosFetchInstance['responseInstance'] =
|
||||
instance as ResponseInterceptorConfig)
|
||||
}
|
||||
|
||||
/** 获取当前实例 */
|
||||
const getAxiosInstance = (instanceKey: keyof AxiosFetchInstance) => {
|
||||
return axiosFetchInstance[instanceKey]
|
||||
}
|
||||
|
||||
/** 设置注入方法队列 */
|
||||
const setImplement = (
|
||||
key: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
func: AnyFunc[],
|
||||
fetchType: FetchType,
|
||||
) => {
|
||||
fetchType === 'ok' ? (implement[key] = func) : (errorImplement[key] = func)
|
||||
}
|
||||
|
||||
/** 获取队列中所有的所有拦截器方法 */
|
||||
const getImplement = (
|
||||
key: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
fetchType: FetchType,
|
||||
): AnyFunc[] => {
|
||||
return fetchType === 'ok' ? implement[key] : errorImplement[key]
|
||||
}
|
||||
|
||||
/** 队列执行器 */
|
||||
const implementer = (funcs: AnyFunc[], ...args: any[]) => {
|
||||
if (Array.isArray(funcs)) {
|
||||
funcs?.forEach((curr) => {
|
||||
if (typeof curr === 'function') {
|
||||
curr(...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求、响应前执行拦截器队列中的所有方法 */
|
||||
const beforeFetch = (
|
||||
key: keyof AxiosFetchInstance,
|
||||
implementKey: keyof ImplementQueue | keyof ErrorImplementQueue,
|
||||
fetchType: FetchType,
|
||||
) => {
|
||||
const funcArr =
|
||||
fetchType === 'ok'
|
||||
? implement[implementKey]
|
||||
: errorImplement[implementKey]
|
||||
const instance = getAxiosInstance(key)
|
||||
const { MODE } = getAppEnvironment()
|
||||
|
||||
if (instance) {
|
||||
implementer(funcArr, instance, MODE)
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求、响应错误时执行队列中所有方法 */
|
||||
const fetchError = (
|
||||
key: keyof AxiosFetchError,
|
||||
error: unknown,
|
||||
errorImplementKey: keyof ErrorImplementQueue,
|
||||
) => {
|
||||
axiosFetchError[key] = error
|
||||
|
||||
const funcArr = errorImplement[errorImplementKey]
|
||||
const { MODE } = getAppEnvironment()
|
||||
|
||||
implementer(funcArr, error, MODE)
|
||||
}
|
||||
|
||||
return {
|
||||
createAxiosInstance,
|
||||
setImplement,
|
||||
getImplement,
|
||||
getAxiosInstance,
|
||||
beforeFetch,
|
||||
fetchError,
|
||||
}
|
||||
}
|
103
src/axios/inject/request/provide.ts
Normal file
103
src/axios/inject/request/provide.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-06
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 请求拦截器入口
|
||||
* 被注册方法执行时其实例能够保证获取到, 所以不需要做额外空判断
|
||||
* 在内部执行方法中, 已经做了边界处理
|
||||
*
|
||||
* 提供两个工具方法, 方便类型推导
|
||||
*/
|
||||
|
||||
import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor'
|
||||
import { appendRequestHeaders } from '@/axios/helper/axiosCopilot'
|
||||
import { APP_CATCH_KEY } from '@/appConfig/appConfig'
|
||||
import { getStorage } from '@/utils/cache'
|
||||
|
||||
import type {
|
||||
RequestInterceptorConfig,
|
||||
BeforeFetchFunction,
|
||||
FetchErrorFunction,
|
||||
} from '@/axios/type'
|
||||
|
||||
const { setImplement } = useAxiosInterceptor()
|
||||
|
||||
/**
|
||||
*
|
||||
* 这里只是示例如何获取到系统缓存的 token 并且返回请求头 token 的 key 和 value
|
||||
* 尽可能的拆分每个拦截器的功能函数
|
||||
* 这是这个包存在的意义
|
||||
*
|
||||
* 当然你也可以根据 request instance 来特殊处理, 这里暂时不做演示
|
||||
*/
|
||||
const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => {
|
||||
const token = getStorage<string>(APP_CATCH_KEY.token)
|
||||
|
||||
if (ins.url) {
|
||||
// TODO: 根据 url 不同是否设置 token
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'X-TOKEN',
|
||||
value: token,
|
||||
}
|
||||
}
|
||||
|
||||
/** 注入请求头信息 */
|
||||
const injectRequestHeaders: BeforeFetchFunction<RequestInterceptorConfig> = (
|
||||
ins,
|
||||
mode,
|
||||
) => {
|
||||
appendRequestHeaders(ins, [
|
||||
requestHeaderToken(ins, mode),
|
||||
{
|
||||
key: 'Demo-Header-Key',
|
||||
value: 'Demo Header Value',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/** 注入重复请求拦截器 */
|
||||
const injectCanceler: BeforeFetchFunction<RequestInterceptorConfig> = (
|
||||
ins,
|
||||
mode,
|
||||
) => {
|
||||
axiosCanceler.removePendingRequest(ins) // 检查是否存在重复请求, 若存在则取消已发的请求
|
||||
axiosCanceler.addPendingRequest(ins) // 把当前的请求信息添加到 pendingRequest 表中
|
||||
}
|
||||
|
||||
/** 请求发生错误示例 */
|
||||
const requestError: FetchErrorFunction<unknown> = (error, mode) => {
|
||||
console.log(error, mode)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 注册请求拦截器
|
||||
* 请注意执行顺序
|
||||
*/
|
||||
export const setupRequestInterceptor = () => {
|
||||
setImplement(
|
||||
'implementRequestInterceptorArray',
|
||||
[injectRequestHeaders, injectCanceler],
|
||||
'ok',
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 注册请求错误拦截器
|
||||
* 请注意执行顺序
|
||||
*/
|
||||
export const setupRequestErrorInterceptor = () => {
|
||||
setImplement('implementRequestInterceptorErrorArray', [requestError], 'error')
|
||||
}
|
77
src/axios/inject/response/provide.ts
Normal file
77
src/axios/inject/response/provide.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-06
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 响应拦截器入口
|
||||
* 被注册方法执行时其实例能够保证获取到, 所以不需要做额外空判断
|
||||
* 在内部执行方法中, 已经做了边界处理
|
||||
*
|
||||
* 提供两个工具方法, 方便类型推导
|
||||
*/
|
||||
|
||||
import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor'
|
||||
|
||||
import type {
|
||||
ResponseInterceptorConfig,
|
||||
BeforeFetchFunction,
|
||||
FetchErrorFunction,
|
||||
} from '@/axios/type'
|
||||
|
||||
const { setImplement } = useAxiosInterceptor()
|
||||
|
||||
/** 响应成功后移除缓存请求 url */
|
||||
const injectResponseCanceler: BeforeFetchFunction<ResponseInterceptorConfig> = (
|
||||
ins,
|
||||
mode,
|
||||
) => {
|
||||
axiosCanceler.removePendingRequest(ins.config)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error 错误信息
|
||||
* @param mode 当前环境
|
||||
*
|
||||
* 你可以在响应错误的时候做一些什么
|
||||
* 这里不做具体演示
|
||||
*
|
||||
* 方法执行时会有两个参数, 可以根据报错信息与环境定做一些处理
|
||||
*/
|
||||
const responseError: FetchErrorFunction<unknown> = (error, mode) => {
|
||||
console.log(error, mode)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 注册响应拦截器
|
||||
* 请注意执行顺序
|
||||
*/
|
||||
export const setupResponseInterceptor = () => {
|
||||
setImplement(
|
||||
'implementResponseInterceptorArray',
|
||||
[injectResponseCanceler],
|
||||
'ok',
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 注册响应错误拦截器
|
||||
* 请注意执行顺序
|
||||
*/
|
||||
export const setupResponseErrorInterceptor = () => {
|
||||
setImplement(
|
||||
'implementResponseInterceptorErrorArray',
|
||||
[responseError],
|
||||
'error',
|
||||
)
|
||||
}
|
80
src/axios/instance.ts
Normal file
80
src/axios/instance.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-11-18
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 请求拦截器与响应拦截器
|
||||
* 如果有需要拓展拦截器, 请在 inject 目录下参照示例方法继续拓展
|
||||
* 该页面不做改动与配置
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { AXIOS_CONFIG } from '@/appConfig/requestConfig'
|
||||
import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor'
|
||||
import {
|
||||
setupResponseInterceptor,
|
||||
setupResponseErrorInterceptor,
|
||||
} from '@/axios/inject/response/provide'
|
||||
import {
|
||||
setupRequestInterceptor,
|
||||
setupRequestErrorInterceptor,
|
||||
} from '@/axios/inject/request/provide'
|
||||
|
||||
import type { AxiosInstanceExpand } from './type'
|
||||
|
||||
const server: AxiosInstanceExpand = axios.create(AXIOS_CONFIG)
|
||||
const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor()
|
||||
|
||||
// 请求拦截器
|
||||
server.interceptors.request.use(
|
||||
(request) => {
|
||||
// 生成 request instance
|
||||
createAxiosInstance(request, 'requestInstance')
|
||||
// 初始化拦截器所有已注入方法
|
||||
setupRequestInterceptor()
|
||||
// 执行拦截器所有已注入方法
|
||||
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok')
|
||||
|
||||
return request
|
||||
},
|
||||
(error) => {
|
||||
// 初始化拦截器所有已注入方法(错误状态)
|
||||
setupRequestErrorInterceptor()
|
||||
// 执行所有已注入方法
|
||||
fetchError('requestError', error, 'implementRequestInterceptorErrorArray')
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
server.interceptors.response.use(
|
||||
(response) => {
|
||||
createAxiosInstance(response, 'responseInstance')
|
||||
setupResponseInterceptor()
|
||||
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok')
|
||||
|
||||
const { data } = response
|
||||
|
||||
return Promise.resolve(data)
|
||||
},
|
||||
(error) => {
|
||||
setupResponseErrorInterceptor()
|
||||
fetchError('responseError', error, 'implementResponseInterceptorErrorArray')
|
||||
|
||||
// 注销该失败请求的取消器
|
||||
axiosCanceler.removePendingRequest(error.config || {})
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default server
|
116
src/axios/type.ts
Normal file
116
src/axios/type.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
AxiosHeaders,
|
||||
AxiosRequestConfig,
|
||||
HeadersDefaults,
|
||||
AxiosDefaults,
|
||||
Axios,
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
} from 'axios'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
export type AxiosHeaderValue =
|
||||
| AxiosHeaders
|
||||
| string
|
||||
| string[]
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
|
||||
export interface RequestHeaderOptions {
|
||||
key: string
|
||||
value: AxiosHeaderValue
|
||||
}
|
||||
|
||||
export interface AxiosInstanceExpand extends Axios {
|
||||
<T = any, D = any>(config: AxiosRequestConfig<D>): Promise<T>
|
||||
<T = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<T>
|
||||
|
||||
getUri(config?: AxiosRequestConfig): string
|
||||
request<R = any, D = any>(config: AxiosRequestConfig<D>): Promise<R>
|
||||
get<R = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>
|
||||
delete<R = any, D = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
head<R = any, D = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
options<R = any, D = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
post<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
put<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
patch<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
postForm<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
putForm<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
patchForm<R = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R>
|
||||
|
||||
defaults: Omit<AxiosDefaults, 'headers' | 'cancelToken'> & {
|
||||
headers: HeadersDefaults & {
|
||||
[key: string]: AxiosHeaderValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type RequestInterceptorConfig<T = any> = InternalAxiosRequestConfig<T>
|
||||
|
||||
export type ResponseInterceptorConfig<T = any, K = any> = AxiosResponse<T, K>
|
||||
|
||||
export interface ImplementQueue {
|
||||
implementRequestInterceptorArray: AnyFunc[]
|
||||
implementResponseInterceptorArray: AnyFunc[]
|
||||
}
|
||||
|
||||
export interface ErrorImplementQueue {
|
||||
implementRequestInterceptorErrorArray: AnyFunc[]
|
||||
implementResponseInterceptorErrorArray: AnyFunc[]
|
||||
}
|
||||
|
||||
export type BeforeFetchFunction<
|
||||
T = RequestInterceptorConfig | ResponseInterceptorConfig,
|
||||
> = <K extends T>(ins: K, mode: string) => void
|
||||
|
||||
export type FetchType = 'ok' | 'error'
|
||||
|
||||
export type FetchErrorFunction<T = any> = <K extends T>(
|
||||
error: K,
|
||||
mode: string,
|
||||
) => void
|
||||
|
||||
export interface AxiosFetchInstance {
|
||||
requestInstance: RequestInterceptorConfig | null
|
||||
responseInstance: ResponseInterceptorConfig | null
|
||||
}
|
||||
|
||||
export interface AxiosFetchError<T = unknown> {
|
||||
requestError: T | null
|
||||
responseError: T | null
|
||||
}
|
7
src/components/AppComponents/AppAvatar/index.scss
Normal file
7
src/components/AppComponents/AppAvatar/index.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.app-avatar {
|
||||
cursor: var(--app-avatar-cursor);
|
||||
|
||||
& .app-avatar__name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
88
src/components/AppComponents/AppAvatar/index.tsx
Normal file
88
src/components/AppComponents/AppAvatar/index.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-31
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 系统管理员头像与名称
|
||||
*
|
||||
* 头像展示基于 naive ui Avatar 组件, 继承该组件所有属性与方法
|
||||
* 默认读取本地 session catch 缓存
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NAvatar, NSpace } from 'naive-ui'
|
||||
|
||||
import { avatarProps, spaceProps } from 'naive-ui'
|
||||
import { APP_CATCH_KEY } from '@/appConfig/appConfig'
|
||||
import { getStorage } from '@/utils/cache'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { AvatarProps, SpaceProps } from 'naive-ui'
|
||||
import type { SigninCallback } from '@/store/modules/signin/type'
|
||||
|
||||
const AppAvatar = defineComponent({
|
||||
name: 'AppAvatar',
|
||||
props: {
|
||||
...avatarProps,
|
||||
...spaceProps,
|
||||
cursor: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
spaceSize: {
|
||||
type: [String, Number] as PropType<SpaceProps['size']>,
|
||||
default: 'medium',
|
||||
},
|
||||
avatarSize: {
|
||||
type: [String, Number] as PropType<AvatarProps['size']>,
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const signin = getStorage<SigninCallback>(APP_CATCH_KEY.signin)
|
||||
const cssVars = computed(() => {
|
||||
const vars = {
|
||||
'--app-avatar-cursor': props.cursor,
|
||||
}
|
||||
|
||||
return vars
|
||||
})
|
||||
|
||||
return {
|
||||
signin,
|
||||
cssVars,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NSpace
|
||||
class="app-avatar"
|
||||
{...this.$props}
|
||||
wrapItem={false}
|
||||
style={this.cssVars}
|
||||
size={this.spaceSize}
|
||||
>
|
||||
<NAvatar
|
||||
// eslint-disable-next-line prettier/prettier, @typescript-eslint/no-explicit-any
|
||||
{...(this.$props as any)}
|
||||
src={this.signin?.avatar}
|
||||
objectFit="cover"
|
||||
round
|
||||
size={this.avatarSize}
|
||||
/>
|
||||
<div class="app-avatar__name">{this.signin?.name}</div>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default AppAvatar
|
36
src/components/AppComponents/AppLockScreen/appLockVar.ts
Normal file
36
src/components/AppComponents/AppLockScreen/appLockVar.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-20
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 统一管理是否处于锁屏状态
|
||||
*
|
||||
* 可以根据后台接口进行替换该变量, 只要是一个响应式的变量值即可
|
||||
*/
|
||||
|
||||
const appLockScreen = useStorage('isAppLockScreen', false, sessionStorage, {
|
||||
mergeDefaults: true,
|
||||
})
|
||||
|
||||
const useAppLockScreen = () => {
|
||||
const setLockAppScreen = (bool: boolean) => {
|
||||
appLockScreen.value = bool
|
||||
}
|
||||
|
||||
const getLockAppScreen = () => appLockScreen.value
|
||||
|
||||
return {
|
||||
setLockAppScreen,
|
||||
getLockAppScreen,
|
||||
}
|
||||
}
|
||||
|
||||
export default useAppLockScreen
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-20
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 锁屏界面 */
|
||||
|
||||
import { NInput, NForm, NFormItem, NButton, NSpace } from 'naive-ui'
|
||||
import AppAvatar from '@/components/AppComponents/AppAvatar/index'
|
||||
|
||||
import { useSetting } from '@/store'
|
||||
import useAppLockScreen from '@/components/AppComponents/AppLockScreen/appLockVar'
|
||||
import {
|
||||
rules,
|
||||
useCondition,
|
||||
autoFouceInput,
|
||||
} from '@/components/AppComponents/AppLockScreen/hook'
|
||||
|
||||
import type { FormInst, InputInst } from 'naive-ui'
|
||||
|
||||
const LockScreen = defineComponent({
|
||||
name: 'LockScreen',
|
||||
setup() {
|
||||
const formInstRef = ref<FormInst | null>(null)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
|
||||
const { setLockAppScreen } = useAppLockScreen()
|
||||
const { changeSwitcher } = useSetting()
|
||||
|
||||
const state = reactive({
|
||||
lockCondition: useCondition(),
|
||||
})
|
||||
|
||||
/** 锁屏 */
|
||||
const lockScreen = () => {
|
||||
formInstRef.value?.validate((error) => {
|
||||
if (!error) {
|
||||
setLockAppScreen(true)
|
||||
changeSwitcher(true, 'lockScreenSwitch')
|
||||
|
||||
state.lockCondition = useCondition()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
autoFouceInput(inputInstRef)
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
lockScreen,
|
||||
formInstRef,
|
||||
inputInstRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class="app-lock-screen__input">
|
||||
<AppAvatar vertical align="center" avatarSize={52} />
|
||||
<NForm
|
||||
ref="formInstRef"
|
||||
model={this.lockCondition}
|
||||
rules={rules}
|
||||
labelPlacement="left"
|
||||
>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
ref="inputInstRef"
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入锁屏密码"
|
||||
clearable
|
||||
minlength={6}
|
||||
maxlength={12}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NButton type="primary" onClick={this.lockScreen.bind(this)}>
|
||||
锁屏
|
||||
</NButton>
|
||||
</NForm>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default LockScreen
|
@ -0,0 +1,156 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-20
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 解锁界面 */
|
||||
|
||||
import { NInput, NForm, NFormItem, NButton, NSpace } from 'naive-ui'
|
||||
import AppAvatar from '@/components/AppComponents/AppAvatar/index'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { useSetting, useSignin } from '@/store'
|
||||
import {
|
||||
rules,
|
||||
useCondition,
|
||||
autoFouceInput,
|
||||
} from '@/components/AppComponents/AppLockScreen/hook'
|
||||
import useAppLockScreen from '@/components/AppComponents/AppLockScreen/appLockVar'
|
||||
|
||||
import type { FormInst, InputInst } from 'naive-ui'
|
||||
|
||||
const UnlockScreen = defineComponent({
|
||||
name: 'UnlockScreen',
|
||||
setup() {
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
|
||||
const { logout } = useSignin()
|
||||
const { changeSwitcher } = useSetting()
|
||||
const { setLockAppScreen } = useAppLockScreen()
|
||||
|
||||
const HH_MM_FORMAT = 'HH:mm'
|
||||
const AM_PM_FORMAT = 'A'
|
||||
const YY_MM_DD_FORMAT = 'YY年MM月DD日'
|
||||
const DDD_FORMAT = 'ddd'
|
||||
|
||||
const state = reactive({
|
||||
lockCondition: useCondition(),
|
||||
HH_MM: dayjs().format(HH_MM_FORMAT),
|
||||
AM_PM: dayjs().locale('en').format(AM_PM_FORMAT),
|
||||
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
|
||||
DDD: dayjs().format(DDD_FORMAT),
|
||||
})
|
||||
const dayInterval = setInterval(() => {
|
||||
state.HH_MM = dayjs().format(HH_MM_FORMAT)
|
||||
state.AM_PM = dayjs().format(AM_PM_FORMAT)
|
||||
}, 6_000)
|
||||
const yearInterval = setInterval(() => {
|
||||
state.YY_MM_DD = dayjs().format(YY_MM_DD_FORMAT)
|
||||
state.DDD = dayjs().format(DDD_FORMAT)
|
||||
}, 86_400_000)
|
||||
|
||||
/** 退出登陆并且回到登陆页 */
|
||||
const backToSignin = () => {
|
||||
window.$dialog.warning({
|
||||
title: '警告',
|
||||
content: '是否返回到登陆页?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
logout()
|
||||
setTimeout(() => {
|
||||
changeSwitcher(false, 'lockScreenSwitch')
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 解锁 */
|
||||
const unlockScreen = () => {
|
||||
formRef.value?.validate((error) => {
|
||||
if (!error) {
|
||||
setLockAppScreen(false)
|
||||
changeSwitcher(false, 'lockScreenSwitch')
|
||||
|
||||
state.lockCondition = useCondition()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(dayInterval)
|
||||
clearInterval(yearInterval)
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
backToSignin,
|
||||
unlockScreen,
|
||||
formRef,
|
||||
inputInstRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class="app-lock-screen__unlock">
|
||||
<div class="app-lock-screen__unlock__content">
|
||||
<div class="app-lock-screen__unlock__content-bg">
|
||||
<div class="left">{this.HH_MM?.split(':')[0]}</div>
|
||||
<div class="right">{this.HH_MM?.split(':')[1]}</div>
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-avatar">
|
||||
<AppAvatar vertical align="center" avatarSize={52} />
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-input">
|
||||
<NForm ref="formRef" model={this.lockCondition} rules={rules}>
|
||||
<NFormItem path="lockPassword">
|
||||
<NInput
|
||||
ref="inputInstRef"
|
||||
v-model:value={this.lockCondition.lockPassword}
|
||||
type="password"
|
||||
placeholder="请输入解锁密码"
|
||||
clearable
|
||||
minlength={6}
|
||||
maxlength={12}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace justify="space-between">
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
onClick={this.backToSignin.bind(this)}
|
||||
>
|
||||
返回登陆
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
onClick={this.unlockScreen.bind(this)}
|
||||
>
|
||||
进入系统
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</div>
|
||||
<div class="app-lock-screen__unlock__content-date">
|
||||
<div class="current-date">
|
||||
{this.HH_MM} <span>{this.AM_PM}</span>
|
||||
</div>
|
||||
<div class="current-year">
|
||||
{this.YY_MM_DD} <span>{this.DDD}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default UnlockScreen
|
38
src/components/AppComponents/AppLockScreen/hook.ts
Normal file
38
src/components/AppComponents/AppLockScreen/hook.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-20
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import type { InputInst } from 'naive-ui'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/** 统一的校验锁屏密码校验规则 */
|
||||
export const rules = {
|
||||
lockPassword: {
|
||||
required: true,
|
||||
message: '请输入正确格式密码',
|
||||
min: 6,
|
||||
max: 12,
|
||||
trigger: ['input'],
|
||||
},
|
||||
}
|
||||
|
||||
/** 锁屏密码参数 */
|
||||
export const useCondition = () => {
|
||||
return {
|
||||
lockPassword: null,
|
||||
}
|
||||
}
|
||||
|
||||
/** 自动获取焦点 */
|
||||
export const autoFouceInput = (inputInstRef: Ref<InputInst | null>) => {
|
||||
nextTick(() => {
|
||||
inputInstRef.value?.focus()
|
||||
})
|
||||
}
|
68
src/components/AppComponents/AppLockScreen/index.scss
Normal file
68
src/components/AppComponents/AppLockScreen/index.scss
Normal file
@ -0,0 +1,68 @@
|
||||
.app-lock-screen__content {
|
||||
& .app-lock-screen__input {
|
||||
& button[class*="n-button"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& form[class*="n-form"] {
|
||||
margin: 24px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
& .app-lock-screen__unlock {
|
||||
.app-lock-screen__unlock__content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include flexCenter;
|
||||
flex-direction: column;
|
||||
|
||||
& .app-lock-screen__unlock__content-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include flexCenter;
|
||||
font-size: 220px;
|
||||
gap: 80px;
|
||||
z-index: 0;
|
||||
|
||||
& .left,
|
||||
& .right {
|
||||
@include flexCenter;
|
||||
border-radius: 30px;
|
||||
background-color: #141313;
|
||||
font-weight: 700;
|
||||
padding: 80px;
|
||||
filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
& .app-lock-screen__unlock__content-avatar {
|
||||
margin-top: 5px;
|
||||
color: #bababa;
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
& .app-lock-screen__unlock__content-input {
|
||||
width: 260px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
& .app-lock-screen__unlock__content-date {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
bottom: 24px;
|
||||
z-index: 1;
|
||||
|
||||
& .current-year,
|
||||
& .current-date span {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
src/components/AppComponents/AppLockScreen/index.tsx
Normal file
59
src/components/AppComponents/AppLockScreen/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-05-13
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 这里没有做解锁密码校验, 只要符合校验规则值皆可
|
||||
* 可以根据需求自行更改
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NModal } from 'naive-ui'
|
||||
import LockScreen from './components/LockScreen'
|
||||
import UnlockScreen from './components/UnlockScreen'
|
||||
|
||||
import { useSetting } from '@/store'
|
||||
import useAppLockScreen from '@/components/AppComponents/AppLockScreen/appLockVar'
|
||||
|
||||
const AppLockScreen = defineComponent({
|
||||
name: 'AppLockScreen',
|
||||
setup() {
|
||||
const settingStore = useSetting()
|
||||
const { lockScreenSwitch } = storeToRefs(settingStore)
|
||||
|
||||
const { getLockAppScreen } = useAppLockScreen()
|
||||
|
||||
return {
|
||||
lockScreenSwitch,
|
||||
getLockAppScreen,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NModal
|
||||
v-model:show={this.lockScreenSwitch}
|
||||
transformOrigin="center"
|
||||
show
|
||||
maskClosable={false}
|
||||
closeOnEsc={false}
|
||||
preset={!this.getLockAppScreen() ? 'dialog' : undefined}
|
||||
title="锁定屏幕"
|
||||
>
|
||||
<div class="app-lock-screen__content">
|
||||
{!this.getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
|
||||
</div>
|
||||
</NModal>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default AppLockScreen
|
9
src/components/AppComponents/README.md
Normal file
9
src/components/AppComponents/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
## 描述
|
||||
|
||||
> 该组件包存放依赖系统数据的公共组件。
|
||||
|
||||
## 约束
|
||||
|
||||
- 该组件包仅存放与系统数据有绑定、关联的组件,纯组件或纯 UI 组件应放置于外层包中
|
||||
- 以 `App` 开头标记组件是系统组件
|
||||
- 组件应该尽量避免与其他系统组件有关联性
|
7
src/components/RayChart/index.scss
Normal file
7
src/components/RayChart/index.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.ray-chart {
|
||||
width: var(--ray-chart-width);
|
||||
height: var(--ray-chart-height);
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
503
src/components/RayChart/index.tsx
Normal file
503
src/components/RayChart/index.tsx
Normal file
@ -0,0 +1,503 @@
|
||||
/**
|
||||
*
|
||||
* 基于 `echarts` 的组件. 意在便捷的使用 `chart` 图
|
||||
*
|
||||
* 暂时不支持自动解析导入 `chart` 组件, 如果使用未注册的组件, 需要在顶部手动导入并且再使用 `use` 注册
|
||||
*
|
||||
* 预引入: 柱状图, 折线图, 饼图, k线图, 散点图等
|
||||
* 预引入: 提示框, 标题, 直角坐标系, 数据集, 内置数据转换器等
|
||||
*
|
||||
* 如果需要大批量数据渲染, 可以通过获取实例后阶段性调用 `setOption` 方法注入数据
|
||||
*
|
||||
* 该组件会在卸载组件时, 自动释放资源
|
||||
*
|
||||
* 注意: 尽量别一次性倒入全部 `chart` 会造成打包体积异常大
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import * as echarts from 'echarts/core' // `echarts` 核心模块
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
AriaComponent,
|
||||
} from 'echarts/components' // 提示框, 标题, 直角坐标系, 数据集, 内置数据转换器等组件(组件后缀都为 `Component`)
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
CandlestickChart,
|
||||
ScatterChart,
|
||||
PictorialBarChart,
|
||||
} from 'echarts/charts' // 系列类型(后缀都为 `SeriesOption`)
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features' // 标签自动布局, 全局过渡动画等特性
|
||||
import { CanvasRenderer } from 'echarts/renderers' // `echarts` 渲染器
|
||||
|
||||
import { useSetting } from '@/store'
|
||||
import { cloneDeep, debounce } from 'lodash-es'
|
||||
import { on, off, addStyle, completeSize } from '@/utils/element'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { EChartsInstance } from '@/types/modules/component'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
export type AutoResize =
|
||||
| boolean
|
||||
| {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface LoadingOptions {
|
||||
text: string // 文本内容
|
||||
color: string // 颜色
|
||||
textColor: string // 字体颜色
|
||||
maskColor: string // 遮罩颜色
|
||||
zlevel: number // 水平
|
||||
fontSize: number // 字体大小
|
||||
showSpinner: boolean // 是否显示旋转动画(`spinner`)
|
||||
spinnerRadius: number // 旋转动画(`spinner`)的半径
|
||||
lineWidth: number // 旋转动画(`spinner`)的线宽
|
||||
fontWeight: string // 字体粗细
|
||||
fontStyle: string // 字体风格
|
||||
fontFamily: string // 字体系列
|
||||
}
|
||||
|
||||
export type ChartTheme = 'dark' | '' | object
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns LoadingOptions
|
||||
*
|
||||
* 为了方便使用加载动画, 写了此方法, 虽然没啥用
|
||||
*/
|
||||
export const loadingOptions = (options?: LoadingOptions) =>
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
text: 'loading',
|
||||
color: '#c23531',
|
||||
textColor: '#000',
|
||||
maskColor: 'rgba(255, 255, 255, 0.9)',
|
||||
zlevel: 0,
|
||||
fontSize: 12,
|
||||
showSpinner: true,
|
||||
spinnerRadius: 10,
|
||||
lineWidth: 5,
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
fontFamily: 'sans-serif',
|
||||
},
|
||||
options,
|
||||
)
|
||||
|
||||
const RayChart = defineComponent({
|
||||
name: 'RayChart',
|
||||
props: {
|
||||
width: {
|
||||
/**
|
||||
*
|
||||
* chart 容器初始化宽度
|
||||
*
|
||||
* 如果未能继承宽度, 则会以 200px 宽度填充
|
||||
*/
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
/**
|
||||
*
|
||||
* chart 容器初始化高度
|
||||
*
|
||||
* 如果未能继承高度, 则会以 200px 宽度填充
|
||||
*/
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
autoResize: {
|
||||
/**
|
||||
*
|
||||
* `chart` 是否跟随窗口尺寸变化自动变化
|
||||
*
|
||||
* 如果为对象, 则可以指定其变化尺寸, 实现图表大小不等于容器大小的效果
|
||||
*/
|
||||
type: [Boolean, Object] as PropType<AutoResize>,
|
||||
default: true,
|
||||
},
|
||||
canvasRender: {
|
||||
/**
|
||||
*
|
||||
* `chart` 渲染器, 默认使用 `canvas`
|
||||
*
|
||||
* 考虑到打包体积与大多数业务场景缘故, 暂时移除 `SVGRenderer` 渲染器的默认导入
|
||||
*/
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAria: {
|
||||
/**
|
||||
*
|
||||
* 是否开启 `chart` 无障碍访问
|
||||
*
|
||||
* 此选项会覆盖 `options` 中的 `aria` 配置
|
||||
*/
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<echarts.EChartsCoreOption>,
|
||||
default: () => ({}),
|
||||
},
|
||||
success: {
|
||||
/**
|
||||
*
|
||||
* 返回 chart 实例
|
||||
*
|
||||
* 渲染成功回调函数
|
||||
*
|
||||
* () => EChartsInstance
|
||||
*/
|
||||
type: Function,
|
||||
default: () => ({}),
|
||||
},
|
||||
error: {
|
||||
/**
|
||||
*
|
||||
* 渲染失败回调函数
|
||||
*
|
||||
* () => void
|
||||
*/
|
||||
type: Function,
|
||||
default: () => ({}),
|
||||
},
|
||||
theme: {
|
||||
type: [String, Object] as PropType<ChartTheme>,
|
||||
default: '',
|
||||
},
|
||||
autoChangeTheme: {
|
||||
/**
|
||||
*
|
||||
* 是否自动跟随模板主题切换
|
||||
*
|
||||
* 如果开启此属性, 则会覆盖 `theme` 属性
|
||||
*
|
||||
* 注意: 这个属性重度依赖此模板, 所以默认不开启. 并且动态切换主题有一定的性能问题
|
||||
*/
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
use: {
|
||||
/**
|
||||
*
|
||||
* 拓展 `echarts` 图表
|
||||
*
|
||||
* 由于官方并没有提供该类型, 手动去复刻成本过高, 故而采用 `any`
|
||||
*/
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
watchOptions: {
|
||||
/** 主动监听 options 变化 */
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
/** 加载动画 */
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingOptions: {
|
||||
/** 配置加载动画样式 */
|
||||
type: Object as PropType<LoadingOptions>,
|
||||
default: () => loadingOptions(),
|
||||
},
|
||||
},
|
||||
setup(props, { expose }) {
|
||||
const settingStore = useSetting()
|
||||
const { themeValue } = storeToRefs(settingStore)
|
||||
const rayChartRef = ref<HTMLElement>() // `echart` 容器实例
|
||||
const echartInstanceRef = ref<EChartsInstance>() // `echart` 拷贝实例, 解决直接使用响应式实例带来的问题
|
||||
let echartInstance: EChartsInstance // `echart` 实例
|
||||
let resizeDebounce: AnyFunc // resize 防抖方法实例
|
||||
|
||||
const cssVarsRef = computed(() => {
|
||||
const cssVars = {
|
||||
'--ray-chart-width': completeSize(props.width),
|
||||
'--ray-chart-height': completeSize(props.height),
|
||||
}
|
||||
|
||||
return cssVars
|
||||
})
|
||||
const modelLoadingOptions = computed(() =>
|
||||
loadingOptions(props.loadingOptions),
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* 注册 `echart` 组件, 图利, 渲染器等
|
||||
*
|
||||
* 会自动合并拓展 `echart` 组件
|
||||
* 该方法必须在注册图表之前调用
|
||||
*/
|
||||
const registerChartCore = async () => {
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
AriaComponent,
|
||||
]) // 注册组件
|
||||
|
||||
echarts.use([
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
CandlestickChart,
|
||||
ScatterChart,
|
||||
PictorialBarChart,
|
||||
]) // 注册类型
|
||||
|
||||
echarts.use([LabelLayout, UniversalTransition]) // 注册布局, 过度效果
|
||||
|
||||
// 如果业务场景中需要 `svg` 渲染器, 手动导入渲染器后使用该行代码即可(不过为了体积考虑, 移除了 SVG 渲染器)
|
||||
// echarts.use([props.canvasRender ? CanvasRenderer : SVGRenderer])
|
||||
echarts.use([CanvasRenderer]) // 注册渲染器
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
echarts.use(props.use as any[])
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Error: wrong property and method passed in extend attribute',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns `chart options`
|
||||
*
|
||||
* 合并配置项
|
||||
*
|
||||
* 如果有需要特殊全局配置的可以在此继续写...
|
||||
*/
|
||||
const useMergeOptions = () => {
|
||||
let options = cloneDeep(props.options)
|
||||
|
||||
const merge = (opts: object) => Object.assign({}, options, opts)
|
||||
|
||||
if (props.showAria) {
|
||||
options = merge({
|
||||
aria: {
|
||||
enabled: true,
|
||||
decal: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 渲染 `echart`
|
||||
*
|
||||
* 缓存两个实例
|
||||
*
|
||||
* 直接使用响应式代理实例会出现诡异的问题, 例如 `legend` 点击时报错
|
||||
*/
|
||||
const renderChart = (theme: ChartTheme) => {
|
||||
/** 获取 dom 容器 */
|
||||
const element = rayChartRef.value as HTMLElement
|
||||
/** 获取配置项 */
|
||||
const options = useMergeOptions()
|
||||
/** 获取 dom 容器实际宽高 */
|
||||
const { height, width } = element.getBoundingClientRect()
|
||||
|
||||
/** 如果高度为 0, 则以 200px 填充 */
|
||||
if (height === 0) {
|
||||
addStyle(element, {
|
||||
height: '200px',
|
||||
})
|
||||
}
|
||||
|
||||
/** 如果款度为 0, 则以 200px 填充 */
|
||||
if (width === 0) {
|
||||
addStyle(element, {
|
||||
width: '200px',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
/** 注册 chart */
|
||||
echartInstance = echarts.init(element, theme)
|
||||
echartInstanceRef.value = echartInstance
|
||||
|
||||
/** 设置 options 配置项 */
|
||||
options && echartInstance.setOption(options)
|
||||
|
||||
/** 渲染成功回调 */
|
||||
props.success?.(echartInstance)
|
||||
} catch (e) {
|
||||
/** 渲染失败回调 */
|
||||
props.error?.()
|
||||
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bool 渲染带有主题色的可视化图
|
||||
*
|
||||
* 区别自动跟随模板主题切换与指定主题切换
|
||||
*/
|
||||
const renderThemeChart = (bool?: boolean) => {
|
||||
if (props.autoChangeTheme) {
|
||||
bool ? renderChart('dark') : renderChart('')
|
||||
|
||||
return void 0
|
||||
}
|
||||
|
||||
if (!props.theme) {
|
||||
renderChart('')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 销毁 `chart` 实例, 释放资源
|
||||
*/
|
||||
const destroyChart = () => {
|
||||
if (echartInstance) {
|
||||
echartInstance.clear()
|
||||
echartInstance.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置 echarts 尺寸 */
|
||||
const resizeChart = () => {
|
||||
if (echartInstance) {
|
||||
echartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听全局主题变化, 然后重新渲染对应主题 echarts */
|
||||
watch(
|
||||
() => [themeValue.value],
|
||||
([theme]) => {
|
||||
/**
|
||||
*
|
||||
* Q: 为什么需要重新卸载再渲染
|
||||
* A: 因为 echarts 官方文档并未提供动态渲染方法
|
||||
* A: 虽然原型上有 setTheme 方法, 但是官方标记是仅限于在类 ECharts 中访问
|
||||
*/
|
||||
if (props.autoChangeTheme) {
|
||||
destroyChart()
|
||||
|
||||
renderThemeChart(theme)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showAria,
|
||||
() => {
|
||||
destroyChart()
|
||||
|
||||
/**
|
||||
*
|
||||
* 贴花跟随主题渲染
|
||||
*
|
||||
* 自动跟随模板主题或者指定主题皆可
|
||||
*/
|
||||
if (props.autoChangeTheme || props.theme) {
|
||||
themeValue.value ? renderChart('dark') : renderChart('')
|
||||
} else {
|
||||
renderChart('')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 显示/隐藏加载动画 */
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newData) => {
|
||||
newData
|
||||
? echartInstance?.showLoading(modelLoadingOptions.value)
|
||||
: echartInstance?.hideLoading()
|
||||
},
|
||||
)
|
||||
|
||||
/** 监听 options 变化 */
|
||||
if (props.watchOptions) {
|
||||
watch(
|
||||
() => props.watchOptions,
|
||||
() => {
|
||||
/** 重新组合 options */
|
||||
const options = useMergeOptions()
|
||||
|
||||
/** 如果 options 发生变动更新 echarts */
|
||||
echartInstance?.setOption(options)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
/** 注册 echarts 组件与渲染器 */
|
||||
await registerChartCore()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
/** 注册 echarts */
|
||||
if (props.autoChangeTheme) {
|
||||
renderThemeChart(themeValue.value)
|
||||
} else {
|
||||
props.theme ? renderChart('dark') : renderChart('')
|
||||
}
|
||||
|
||||
/** 注册事件 */
|
||||
if (props.autoResize) {
|
||||
resizeDebounce = debounce(resizeChart, 500)
|
||||
|
||||
on(window, 'resize', resizeDebounce)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
/** 卸载 echarts */
|
||||
destroyChart()
|
||||
/** 卸载事件柄 */
|
||||
off(window, 'resize', resizeDebounce)
|
||||
})
|
||||
|
||||
expose({
|
||||
echart: echartInstanceRef,
|
||||
})
|
||||
|
||||
return {
|
||||
rayChartRef,
|
||||
cssVarsRef,
|
||||
echartInstance: echartInstanceRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class="ray-chart" style={[this.cssVarsRef]} ref="rayChartRef"></div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayChart
|
3
src/components/RayCollapseGrid/index.ts
Normal file
3
src/components/RayCollapseGrid/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import RayCollapseGrid from './src/index'
|
||||
|
||||
export default RayCollapseGrid
|
20
src/components/RayCollapseGrid/src/index.scss
Normal file
20
src/components/RayCollapseGrid/src/index.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.ray-collapse-grid {
|
||||
box-sizing: border-box;
|
||||
|
||||
& .collapse-icon {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s var(--r-bezier);
|
||||
|
||||
> .collapse-icon--arrow {
|
||||
margin-left: 0.5em;
|
||||
transition: color 0.3s var(--r-bezier), transform 0.3s var(--r-bezier);
|
||||
|
||||
&.collapse-icon--arrow__expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
95
src/components/RayCollapseGrid/src/index.tsx
Normal file
95
src/components/RayCollapseGrid/src/index.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-12-27
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { collapseGridProps } from './props'
|
||||
|
||||
import { NCard, NGrid, NGridItem, NSpace } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon'
|
||||
|
||||
const RayCollapseGrid = defineComponent({
|
||||
name: 'RayCollapseGrid',
|
||||
props: collapseGridProps,
|
||||
emits: ['updateValue'],
|
||||
setup(props, { emit }) {
|
||||
const modelCollapsed = ref(props.value)
|
||||
|
||||
const handleCollapse = () => {
|
||||
modelCollapsed.value = !modelCollapsed.value
|
||||
|
||||
emit('updateValue', modelCollapsed.value)
|
||||
}
|
||||
|
||||
const CollapseIcon = () => (
|
||||
<div class="collapse-icon" onClick={handleCollapse.bind(this)}>
|
||||
<span>
|
||||
{modelCollapsed.value
|
||||
? props.collapseToggleText[0]
|
||||
: props.collapseToggleText[1]}
|
||||
</span>
|
||||
<RayIcon
|
||||
customClassName={`collapse-icon--arrow ${
|
||||
modelCollapsed.value ? '' : 'collapse-icon--arrow__expanded'
|
||||
}`}
|
||||
name="expanded"
|
||||
size="14"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return {
|
||||
modelCollapsed,
|
||||
handleCollapse,
|
||||
CollapseIcon,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NCard bordered={this.bordered}>
|
||||
{{
|
||||
default: () => (
|
||||
<NGrid
|
||||
class="ray-collapse-grid"
|
||||
{...this.$props}
|
||||
collapsed={this.modelCollapsed}
|
||||
xGap={this.xGap || 12}
|
||||
yGap={this.yGap || 18}
|
||||
cols={this.cols}
|
||||
collapsedRows={this.collapsedRows}
|
||||
>
|
||||
{this.$slots.default?.()}
|
||||
<NGridItem suffix class="ray-collapse-grid__suffix--btn">
|
||||
<NSpace justify="end">
|
||||
{this.$slots.action?.()}
|
||||
{this.CollapseIcon()}
|
||||
</NSpace>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
),
|
||||
}}
|
||||
</NCard>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayCollapseGrid
|
||||
|
||||
/**
|
||||
*
|
||||
* <https://www.naiveui.com/zh-CN/dark/components/grid>
|
||||
*
|
||||
* 可折叠操作栏
|
||||
*
|
||||
* 可以结合表单或者表格使用
|
||||
*
|
||||
* 该组件完全基于 `NGrid` `NGridItem` 实现, 所以需要在使用该组件时使用 `NGridItem` 包裹元素
|
||||
*/
|
53
src/components/RayCollapseGrid/src/props.ts
Normal file
53
src/components/RayCollapseGrid/src/props.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { gridProps } from 'naive-ui'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { CollapseToggleText } from './type'
|
||||
|
||||
export const collapseGridProps = {
|
||||
value: {
|
||||
/**
|
||||
*
|
||||
* 是否折叠操作栏
|
||||
*
|
||||
* 默认折叠
|
||||
*
|
||||
* `true` 收起, `false` 展开
|
||||
*/
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
collapseToggleText: {
|
||||
/**
|
||||
*
|
||||
* 自定义展开与收起文案
|
||||
*
|
||||
* 默认 `['展开', '收起']`
|
||||
*
|
||||
* 索引第一位为展开时显示内容, 所以第二位置为收起时显示内容
|
||||
*/
|
||||
type: Array as unknown as PropType<CollapseToggleText>,
|
||||
default: () => ['展开', '收起'],
|
||||
},
|
||||
bordered: {
|
||||
/**
|
||||
*
|
||||
* 卡片边框
|
||||
*
|
||||
* 默认 `false`
|
||||
*/
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
...gridProps,
|
||||
} as const
|
||||
|
||||
/**
|
||||
*
|
||||
* 基于 `NGird` 实现
|
||||
*
|
||||
* 继承该组件所有属性和方法, <https://www.naiveui.com/zh-CN/dark/components/grid>
|
||||
*
|
||||
* `xGap` 默认 `12`
|
||||
*
|
||||
* `yGap` 默认 `18`
|
||||
*/
|
1
src/components/RayCollapseGrid/src/type.ts
Normal file
1
src/components/RayCollapseGrid/src/type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type CollapseToggleText = [string | number, string | number]
|
94
src/components/RayGlobalProvider/index.tsx
Normal file
94
src/components/RayGlobalProvider/index.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-14
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 全局注入 naive ui 提示性组件
|
||||
* 使用该组件注册后, 可以直接通过 window.$message、window.$notification、window.$dialog、window.$loadingBar 访问
|
||||
* 但是, 使用该组件注册后, 使用 window.$notification 组件时不能更改 placement 位置(只能默认右上角弹出)
|
||||
* 如果需要更改弹出位置, 需要在需要地方重新定义组件注册
|
||||
*/
|
||||
|
||||
import {
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
NMessageProvider,
|
||||
NNotificationProvider,
|
||||
NConfigProvider,
|
||||
createDiscreteApi,
|
||||
darkTheme,
|
||||
NGlobalStyle,
|
||||
} from 'naive-ui'
|
||||
|
||||
import { useSetting } from '@/store'
|
||||
import { naiveLocales } from '@/locales/helper'
|
||||
|
||||
const GlobalProvider = defineComponent({
|
||||
name: 'GlobalProvider',
|
||||
setup() {
|
||||
const settingStore = useSetting()
|
||||
|
||||
const modelPrimaryColorOverride = computed(
|
||||
() => settingStore.primaryColorOverride,
|
||||
)
|
||||
const modelThemeValue = computed(() =>
|
||||
settingStore.themeValue ? darkTheme : null,
|
||||
)
|
||||
const localePackage = computed(() => {
|
||||
const key = settingStore.localeLanguage
|
||||
|
||||
return naiveLocales(key)
|
||||
})
|
||||
|
||||
const { message, notification, dialog, loadingBar } = createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar'],
|
||||
{
|
||||
configProviderProps: computed(() => ({
|
||||
theme: modelThemeValue.value,
|
||||
})),
|
||||
},
|
||||
)
|
||||
|
||||
window.$dialog = dialog // 注入 `dialog`
|
||||
window.$message = message // 注入 `message`
|
||||
window.$loadingBar = loadingBar // 注入 `loadingBar`
|
||||
window.$notification = notification // 注入 `notification`
|
||||
|
||||
return {
|
||||
modelPrimaryColorOverride,
|
||||
modelThemeValue,
|
||||
localePackage,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NConfigProvider
|
||||
themeOverrides={this.modelPrimaryColorOverride}
|
||||
theme={this.modelThemeValue}
|
||||
locale={this.localePackage.locale}
|
||||
dateLocale={this.localePackage.dateLocal}
|
||||
>
|
||||
<NLoadingBarProvider>
|
||||
<NMessageProvider>
|
||||
<NDialogProvider>
|
||||
<NNotificationProvider>
|
||||
<NGlobalStyle />
|
||||
{this.$slots.default?.()}
|
||||
</NNotificationProvider>
|
||||
</NDialogProvider>
|
||||
</NMessageProvider>
|
||||
</NLoadingBarProvider>
|
||||
</NConfigProvider>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default GlobalProvider
|
20
src/components/RayIcon/index.scss
Normal file
20
src/components/RayIcon/index.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.ray-icon {
|
||||
position: relative;
|
||||
width: var(--ray-icon-width);
|
||||
height: var(--ray-icon-height);
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
fill: currentColor;
|
||||
transform: translateZ(0);
|
||||
opacity: var(--ray-icon-depth);
|
||||
cursor: var(--ray-icon-cursor);
|
||||
|
||||
& svg[RayIconAttribute="ray-icon"] {
|
||||
width: var(--ray-icon-width);
|
||||
height: var(--ray-icon-height);
|
||||
}
|
||||
}
|
106
src/components/RayIcon/index.tsx
Normal file
106
src/components/RayIcon/index.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-01-04
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const RayIcon = defineComponent({
|
||||
name: 'RayIcon',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor',
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 14,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
customClassName: {
|
||||
/** 自定义 class name */
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
depth: {
|
||||
/** 图标深度 */
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
cursor: {
|
||||
/** 鼠标指针样式 */
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, ctx) {
|
||||
const emit = ctx.emit
|
||||
|
||||
const modelColor = computed(() => props.color)
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
const cssVars = computed(() => {
|
||||
const cssVar = {
|
||||
'--ray-icon-width': props.width
|
||||
? props.width + 'px'
|
||||
: props.size + 'px',
|
||||
'--ray-icon-height': props.height
|
||||
? props.height + 'px'
|
||||
: props.size + 'px',
|
||||
'--ray-icon-depth': props.depth,
|
||||
'--ray-icon-cursor': props.cursor,
|
||||
}
|
||||
|
||||
return cssVar
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click')
|
||||
}
|
||||
|
||||
return {
|
||||
modelColor,
|
||||
symbolId,
|
||||
cssVars,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<span
|
||||
class={['ray-icon', this.customClassName]}
|
||||
style={[this.cssVars]}
|
||||
onClick={this.handleClick.bind(this)}
|
||||
>
|
||||
<svg
|
||||
{...({ RayIconAttribute: 'ray-icon', ariaHidden: true } as object)}
|
||||
>
|
||||
<use {...{ 'xlink:href': this.symbolId }} fill={this.modelColor} />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayIcon
|
3
src/components/RayIframe/index.ts
Normal file
3
src/components/RayIframe/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import RayIframe from './src/index'
|
||||
|
||||
export default RayIframe
|
13
src/components/RayIframe/src/index.scss
Normal file
13
src/components/RayIframe/src/index.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.ray-iframe {
|
||||
width: var(--ray-iframe-width);
|
||||
height: var(--ray-iframe-height);
|
||||
box-sizing: border-box;
|
||||
border: var(--ray-iframe-frameborder);
|
||||
|
||||
& .ray-iframe__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
180
src/components/RayIframe/src/index.tsx
Normal file
180
src/components/RayIframe/src/index.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-09
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NSpin } from 'naive-ui'
|
||||
|
||||
import { completeSize, on, off } from '@use-utils/element'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { SpinProps } from 'naive-ui'
|
||||
|
||||
const RayIframe = defineComponent({
|
||||
name: 'RayIframe',
|
||||
props: {
|
||||
src: {
|
||||
/** iframe url */
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iframeWrapperClass: {
|
||||
/** 自定义类名 */
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
frameborder: {
|
||||
/** 边框尺寸, 0 则不显示 */
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
width: {
|
||||
/** iframe 宽度 */
|
||||
type: [String, Number],
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
/** iframe 高度 */
|
||||
type: [String, Number],
|
||||
default: '100%',
|
||||
},
|
||||
allow: {
|
||||
/**
|
||||
*
|
||||
* iframe 特征策略
|
||||
*
|
||||
* ```
|
||||
* 全屏激活: allow = 'fullscreen'
|
||||
* 允许跨域: allow = 'payment'
|
||||
* ```
|
||||
*
|
||||
* 但是该配置属性受到浏览器安全策略影响, 使用前请仔细阅读文档
|
||||
*/
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
/** iframe 定位嵌入的浏览上下文的名称 */
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
/** 标识 iframe 的主要内容 */
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
success: {
|
||||
/**
|
||||
*
|
||||
* iframe 加载成功回调
|
||||
* 返回值: iframe 对象, Event
|
||||
*/
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
error: {
|
||||
/**
|
||||
*
|
||||
* iframe 加载失败回调
|
||||
* 返回值: iframe 对象, Event
|
||||
*/
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
customSpinProps: {
|
||||
type: Object as PropType<SpinProps>,
|
||||
default: () => ({}),
|
||||
},
|
||||
lazy: {
|
||||
/** 是否延迟加载 iframe */
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { expose }) {
|
||||
const cssVars = computed(() => {
|
||||
const cssVar = {
|
||||
'--ray-iframe-frameborder': completeSize(props.frameborder),
|
||||
'--ray-iframe-width': completeSize(props.width),
|
||||
'--ray-iframe-height': completeSize(props.height),
|
||||
}
|
||||
|
||||
return cssVar
|
||||
})
|
||||
const iframeRef = ref<HTMLIFrameElement>()
|
||||
const spinShow = ref(true)
|
||||
|
||||
const iframeLoadSuccess = (e: Event) => {
|
||||
spinShow.value = false
|
||||
|
||||
props.success?.(iframeRef.value, e)
|
||||
}
|
||||
|
||||
const iframeLoadError = (e: Event) => {
|
||||
spinShow.value = false
|
||||
|
||||
props.error?.(iframeRef.value, e)
|
||||
}
|
||||
|
||||
const getIframeRef = () => {
|
||||
const iframeEl = iframeRef.value as HTMLElement
|
||||
|
||||
return iframeEl
|
||||
}
|
||||
|
||||
expose()
|
||||
|
||||
onMounted(() => {
|
||||
on(getIframeRef(), 'load', iframeLoadSuccess.bind(this))
|
||||
on(getIframeRef(), 'error', iframeLoadError)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
off(getIframeRef(), 'load', iframeLoadSuccess)
|
||||
off(getIframeRef(), 'error', iframeLoadError)
|
||||
})
|
||||
|
||||
return {
|
||||
cssVars,
|
||||
iframeRef,
|
||||
spinShow,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
class={['ray-iframe', this.iframeWrapperClass]}
|
||||
style={[this.cssVars]}
|
||||
>
|
||||
<NSpin {...this.customSpinProps} show={this.spinShow}>
|
||||
{{
|
||||
...this.$slots,
|
||||
default: () => (
|
||||
<iframe
|
||||
class="ray-iframe__container"
|
||||
ref="iframeRef"
|
||||
src={this.src}
|
||||
allow={this.allow}
|
||||
name={this.name}
|
||||
title={this.title}
|
||||
{...{
|
||||
loading: this.lazy ? 'lazy' : null,
|
||||
}}
|
||||
></iframe>
|
||||
),
|
||||
}}
|
||||
</NSpin>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayIframe
|
95
src/components/RayLink/index.tsx
Normal file
95
src/components/RayLink/index.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { NAvatar, NTooltip, NSpace } from 'naive-ui'
|
||||
|
||||
interface AvatarOptions {
|
||||
key: string
|
||||
src: string
|
||||
tooltip: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const RayLink = defineComponent({
|
||||
name: 'RayLink',
|
||||
setup() {
|
||||
const avatarOptions: AvatarOptions[] = [
|
||||
{
|
||||
key: 'yunhome',
|
||||
src: 'https://yunkuangao.me/',
|
||||
tooltip: '云之家',
|
||||
icon: 'https://yunkuangao.me/wp-content/uploads/2022/05/cropped-cropped-QQ%E5%9B%BE%E7%89%8720220511113928.jpg',
|
||||
},
|
||||
{
|
||||
key: 'yun-cloud-images',
|
||||
src: 'https://yunkuangao.com/',
|
||||
tooltip: '云图床',
|
||||
icon: 'https://yunkuangao.me/wp-content/uploads/2022/05/cropped-cropped-QQ%E5%9B%BE%E7%89%8720220511113928.jpg',
|
||||
},
|
||||
{
|
||||
key: 'ray-js-note',
|
||||
src: 'https://note.youdao.com/s/ObWEe2BB',
|
||||
tooltip: 'Ray的前端学习笔记',
|
||||
icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
|
||||
},
|
||||
{
|
||||
key: 'ray-js-cover',
|
||||
src: 'https://note.youdao.com/s/IC8xKPdB',
|
||||
tooltip: 'Ray的面试题总结',
|
||||
icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
|
||||
},
|
||||
{
|
||||
key: 'ray-template-doc',
|
||||
src: 'https://xiaodaigua-ray.github.io/ray-template-doc/',
|
||||
tooltip: 'Ray Template Doc',
|
||||
icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
|
||||
},
|
||||
{
|
||||
key: 'ray-template-doc-out',
|
||||
src: 'https://ray-template.yunkuangao.com/',
|
||||
tooltip: 'Ray Template Doc (国内地址)',
|
||||
icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
|
||||
},
|
||||
]
|
||||
|
||||
const handleLinkClick = (item: AvatarOptions) => {
|
||||
window.open(item.src)
|
||||
}
|
||||
|
||||
return {
|
||||
handleLinkClick,
|
||||
avatarOptions,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NSpace>
|
||||
{this.avatarOptions.map((curr) => (
|
||||
<NTooltip>
|
||||
{{
|
||||
trigger: () => (
|
||||
<NAvatar
|
||||
round
|
||||
src={curr.icon}
|
||||
style={['cursor: pointer']}
|
||||
{...{
|
||||
onClick: this.handleLinkClick.bind(this, curr),
|
||||
}}
|
||||
objectFit="cover"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
default: () => curr.tooltip,
|
||||
}}
|
||||
</NTooltip>
|
||||
))}
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayLink
|
||||
|
||||
/**
|
||||
*
|
||||
* 友链组件
|
||||
*
|
||||
* 这个组件用作初试模板中, 不喜欢自行删除
|
||||
*/
|
4
src/components/RayTable/index.ts
Normal file
4
src/components/RayTable/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import RayTable from './src/index'
|
||||
|
||||
export default RayTable
|
||||
export type { RayTableInst } from './src/type'
|
132
src/components/RayTable/src/components/TableAction/index.tsx
Normal file
132
src/components/RayTable/src/components/TableAction/index.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-12-22
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import { NPopconfirm, NSpace, NButton, NPopover } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
export type EmitterType = 'positive' | 'negative'
|
||||
|
||||
const TableAction = defineComponent({
|
||||
name: 'TableAction',
|
||||
props: {
|
||||
tooltip: {
|
||||
/**
|
||||
*
|
||||
* 提示内容
|
||||
*/
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
negativeText: {
|
||||
/**
|
||||
*
|
||||
* 取消提示按钮文本内容
|
||||
*
|
||||
* 默认 `取消`
|
||||
*/
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
positiveText: {
|
||||
/**
|
||||
*
|
||||
* 确认提示按钮文本内容
|
||||
*
|
||||
* 默认 `确认`
|
||||
*/
|
||||
type: String,
|
||||
default: '确认',
|
||||
},
|
||||
icon: {
|
||||
/**
|
||||
*
|
||||
* 图标
|
||||
*
|
||||
* 必须为 `icons` 中已包含的
|
||||
*/
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconSize: {
|
||||
/**
|
||||
*
|
||||
* 图标尺寸
|
||||
*
|
||||
* 默认为 `18px`
|
||||
*/
|
||||
type: Number,
|
||||
default: 18,
|
||||
},
|
||||
popoverContent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['positive', 'negative'],
|
||||
setup(_, { emit }) {
|
||||
const showPopoconfirm = ref(false)
|
||||
|
||||
const handleEmit = (type: EmitterType) => {
|
||||
type === 'positive' ? emit('positive') : emit('negative')
|
||||
|
||||
showPopoconfirm.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
handleEmit,
|
||||
showPopoconfirm,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NPopover>
|
||||
{{
|
||||
trigger: () => (
|
||||
<NPopconfirm v-model:show={this.showPopoconfirm} showArrow={true}>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
name={this.icon}
|
||||
size={this.iconSize}
|
||||
customClassName="ray-table-icon"
|
||||
/>
|
||||
),
|
||||
default: () => this.tooltip,
|
||||
action: () => (
|
||||
<NSpace>
|
||||
<NButton
|
||||
size="small"
|
||||
ghost
|
||||
onClick={this.handleEmit.bind(this, 'negative')}
|
||||
>
|
||||
{this.negativeText}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
ghost
|
||||
type="info"
|
||||
onClick={this.handleEmit.bind(this, 'positive')}
|
||||
>
|
||||
{this.positiveText}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
}}
|
||||
</NPopconfirm>
|
||||
),
|
||||
default: () => this.popoverContent,
|
||||
}}
|
||||
</NPopover>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TableAction
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-11
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NPopover } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
import screenfull from 'screenfull'
|
||||
|
||||
import type { TableSettingProvider } from '@/components/RayTable/src/type'
|
||||
|
||||
const TableScreenfull = defineComponent({
|
||||
name: 'TableScreenfull',
|
||||
setup() {
|
||||
const tableSettingProvider = inject(
|
||||
'tableSettingProvider',
|
||||
{} as TableSettingProvider,
|
||||
)
|
||||
|
||||
const rayTableUUID = computed(() => tableSettingProvider.rayTableUUID)
|
||||
let currentTableIsFullscreen = screenfull.isFullscreen // 缓存当前是否处于全屏状态
|
||||
|
||||
const handleScreenfull = () => {
|
||||
const el = document.getElementById(rayTableUUID.value)
|
||||
|
||||
currentTableIsFullscreen = !currentTableIsFullscreen
|
||||
|
||||
if (el && screenfull.isEnabled && currentTableIsFullscreen) {
|
||||
screenfull.request(el)
|
||||
} else {
|
||||
screenfull.exit()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleScreenfull,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NPopover>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
name="fullscreen"
|
||||
size="18"
|
||||
customClassName="ray-table-icon tay-table-icon__screenfull"
|
||||
onClick={this.handleScreenfull.bind(this)}
|
||||
/>
|
||||
),
|
||||
default: () => '全屏表格',
|
||||
}}
|
||||
</NPopover>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TableScreenfull
|
19
src/components/RayTable/src/components/TableSetting/hook.ts
Normal file
19
src/components/RayTable/src/components/TableSetting/hook.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { ActionOptions } from '@/components/RayTable/src/type'
|
||||
|
||||
export const setupSettingOptions = (options: ActionOptions[]) => {
|
||||
const arr = options.map((curr) => {
|
||||
if (curr.fixed) {
|
||||
curr.fixed === 'right'
|
||||
? (curr.rightFixedActivated = true)
|
||||
: (curr.leftFixedActivated = true)
|
||||
}
|
||||
|
||||
if (curr.resizable) {
|
||||
curr.resizeColumnActivated = true
|
||||
}
|
||||
|
||||
return curr
|
||||
})
|
||||
|
||||
return arr
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
.ray-table__setting:hover {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s var(--r-bezier);
|
||||
}
|
||||
|
||||
.table-setting__card {
|
||||
padding: 12px 8px;
|
||||
|
||||
& .n-card__content {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ray-table__setting-option--draggable {
|
||||
display: grid;
|
||||
grid-row-gap: 10px;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
|
||||
& .draggable-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.3s var(--r-bezier);
|
||||
|
||||
&.draggable-item--dark {
|
||||
&:hover {
|
||||
background-color: var(--ray-theme-primary-fade-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ray-theme-primary-fade-color);
|
||||
|
||||
& .draggable-item__d--icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& .draggable-item__d--icon {
|
||||
transition: opacity 0.3s var(--r-bezier), transform 0.3s var(--r-bezier);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& .draggable-item__d--icon,
|
||||
& .draggable-item__icon {
|
||||
padding: 5px;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
& .draggable-item__icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& .draggable-item__icon {
|
||||
&.draggable-item__icon--actived {
|
||||
color: var(--ray-theme-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
& .n-ellipsis {
|
||||
max-width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
233
src/components/RayTable/src/components/TableSetting/index.tsx
Normal file
233
src/components/RayTable/src/components/TableSetting/index.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-12-08
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 依赖 table columns 属性操作
|
||||
*
|
||||
* 支持拖拽修改列顺序、动态修改列宽度、固定(锁列)
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NCard, NPopover, NEllipsis } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
import VueDraggable from 'vuedraggable'
|
||||
|
||||
import { setupSettingOptions } from './hook'
|
||||
import { useSetting } from '@/store'
|
||||
|
||||
import type {
|
||||
TableSettingProvider,
|
||||
ActionOptions,
|
||||
FixedType,
|
||||
TableSettingFixedPopoverIcon,
|
||||
} from '@/components/RayTable/src/type'
|
||||
|
||||
const TableSetting = defineComponent({
|
||||
name: 'TableSetting',
|
||||
emits: ['columnsUpdate'],
|
||||
setup(_, { emit }) {
|
||||
const tableSettingProvider = inject(
|
||||
'tableSettingProvider',
|
||||
{} as TableSettingProvider,
|
||||
)
|
||||
const settingStore = useSetting()
|
||||
|
||||
const settingOptions = ref(
|
||||
setupSettingOptions(tableSettingProvider.modelColumns.value),
|
||||
) // 表格表头
|
||||
const disableDraggable = ref(true) // 拖拽开关(暂时弃用)
|
||||
const { themeValue } = storeToRefs(settingStore)
|
||||
|
||||
/** 拖拽结束后 */
|
||||
const handleDraggableEnd = () => {
|
||||
emit('columnsUpdate', settingOptions.value)
|
||||
}
|
||||
|
||||
const FixedPopoverIcon = (options: TableSettingFixedPopoverIcon) => {
|
||||
const { element, name, tooltip, fn, index, fixed, key } = options
|
||||
|
||||
return (
|
||||
<NPopover>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
customClassName={`draggable-item__icon ray-table-icon ${
|
||||
element[key] ? 'draggable-item__icon--actived' : ''
|
||||
}`}
|
||||
name={name}
|
||||
size="18"
|
||||
onClick={fn.bind(this, fixed, index)}
|
||||
/>
|
||||
),
|
||||
default: () => tooltip,
|
||||
}}
|
||||
</NPopover>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type 列所需固定方向
|
||||
* @param idx 当前操作栏索引位置
|
||||
*
|
||||
* @remark 操作栏锁定列, 不能同时存在两种状态(互斥)
|
||||
*/
|
||||
const handleFixedClick = (type: FixedType, idx: number) => {
|
||||
const key = `${type}FixedActivated`
|
||||
const value = settingOptions.value[idx]
|
||||
|
||||
if (key === 'leftFixedActivated') {
|
||||
value['rightFixedActivated'] = false
|
||||
} else if (key === 'rightFixedActivated') {
|
||||
value['leftFixedActivated'] = false
|
||||
}
|
||||
|
||||
value[key] = !value[key]
|
||||
|
||||
if (value[key]) {
|
||||
value.fixed = type
|
||||
} else {
|
||||
value.fixed = undefined
|
||||
}
|
||||
|
||||
settingOptions.value[idx] = value
|
||||
|
||||
emit('columnsUpdate', settingOptions.value)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param idx 索引
|
||||
*
|
||||
* @remark 动态设置列宽度, 如果表格并未出现横向滚动条则不会启用拖拽修改列按钮
|
||||
*/
|
||||
const handleResizeColumnClick = (idx: number) => {
|
||||
const value = settingOptions.value[idx]
|
||||
|
||||
value['resizeColumnActivated'] = !value['resizeColumnActivated']
|
||||
value['resizable'] = value['resizeColumnActivated']
|
||||
|
||||
settingOptions.value[idx] = value
|
||||
|
||||
emit('columnsUpdate', settingOptions.value)
|
||||
}
|
||||
|
||||
return {
|
||||
settingOptions,
|
||||
handleDraggableEnd,
|
||||
handleFixedClick,
|
||||
disableDraggable,
|
||||
FixedPopoverIcon,
|
||||
handleResizeColumnClick,
|
||||
themeValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NPopover trigger="click" placement="bottom" showArrow={false} raw>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
customClassName="ray-table__setting"
|
||||
name="setting"
|
||||
size="18"
|
||||
/>
|
||||
),
|
||||
default: () => (
|
||||
<NCard bordered={false} class="table-setting__card">
|
||||
{{
|
||||
default: () => (
|
||||
<VueDraggable
|
||||
class={['ray-table__setting-option--draggable']}
|
||||
v-model={this.settingOptions}
|
||||
itemKey="key"
|
||||
{...{
|
||||
disabled: !this.disableDraggable,
|
||||
onEnd: this.handleDraggableEnd.bind(this),
|
||||
}}
|
||||
>
|
||||
{{
|
||||
item: ({
|
||||
element,
|
||||
index,
|
||||
}: {
|
||||
element: ActionOptions
|
||||
index: number
|
||||
}) => (
|
||||
<div
|
||||
class={[
|
||||
'draggable-item',
|
||||
this.themeValue ? 'draggable-item--dark' : '',
|
||||
]}
|
||||
>
|
||||
<RayIcon
|
||||
customClassName={`draggable-item__d--icon`}
|
||||
name="draggable"
|
||||
size="18"
|
||||
/>
|
||||
<NEllipsis>
|
||||
<span>{element.title}</span>
|
||||
</NEllipsis>
|
||||
{this.FixedPopoverIcon({
|
||||
element: element,
|
||||
name: 'left_arrow',
|
||||
tooltip: '左固定',
|
||||
fn: this.handleFixedClick,
|
||||
index,
|
||||
fixed: 'left',
|
||||
key: 'leftFixedActivated',
|
||||
})}
|
||||
<NPopover>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
customClassName={`draggable-item__icon ${
|
||||
element['resizeColumnActivated']
|
||||
? 'draggable-item__icon--actived'
|
||||
: ''
|
||||
}`}
|
||||
name="resize_h"
|
||||
size="18"
|
||||
onClick={this.handleResizeColumnClick.bind(
|
||||
this,
|
||||
index,
|
||||
)}
|
||||
/>
|
||||
),
|
||||
default: () => '修改列宽',
|
||||
}}
|
||||
</NPopover>
|
||||
{this.FixedPopoverIcon({
|
||||
element: element,
|
||||
name: 'right_arrow',
|
||||
tooltip: '右固定',
|
||||
fn: this.handleFixedClick,
|
||||
index,
|
||||
fixed: 'right',
|
||||
key: 'rightFixedActivated',
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
</VueDraggable>
|
||||
),
|
||||
}}
|
||||
</NCard>
|
||||
),
|
||||
}}
|
||||
</NPopover>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TableSetting
|
47
src/components/RayTable/src/components/TableSize/index.scss
Normal file
47
src/components/RayTable/src/components/TableSize/index.scss
Normal file
@ -0,0 +1,47 @@
|
||||
.ray-table__table-size {
|
||||
padding: 0 !important;
|
||||
|
||||
& .n-card__content {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
|
||||
& .table-size__dropdown {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0;
|
||||
background-color: transparent;
|
||||
|
||||
& .table-size__dropdown-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& .dropdown-item {
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 0 16px;
|
||||
transition: background-color 0.3s var(--r-bezier), color 0.3s var(--r-bezier);
|
||||
|
||||
&.dropdown-item--active,
|
||||
&:hover {
|
||||
background-color: var(--ray-theme-primary-fade-color);
|
||||
color: var(--ray-theme-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ray-table__table-size--dark {
|
||||
@include useAppTheme("dark") {
|
||||
& .table-size__dropdown-wrapper {
|
||||
& .dropdown-item:hover {
|
||||
background-color: var(--ray-theme-primary-fade-color);
|
||||
color: var(--ray-theme-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
src/components/RayTable/src/components/TableSize/index.tsx
Normal file
129
src/components/RayTable/src/components/TableSize/index.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-10
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NPopover, NCard } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
import type { TableSettingProvider } from '@/components/RayTable/src/type'
|
||||
import type { ComponentSize } from '@/types/modules/component'
|
||||
|
||||
const TableSize = defineComponent({
|
||||
name: 'TableSize',
|
||||
emits: ['changeSize'],
|
||||
setup(_, { emit }) {
|
||||
const tableSettingProvider = inject(
|
||||
'tableSettingProvider',
|
||||
{} as TableSettingProvider,
|
||||
)
|
||||
|
||||
const popoverShow = ref(false)
|
||||
const currentSize = ref(tableSettingProvider.size)
|
||||
const size = computed({
|
||||
get: () => tableSettingProvider.size,
|
||||
set: (val) => {
|
||||
currentSize.value = val
|
||||
},
|
||||
})
|
||||
const sizeOptions = ref([
|
||||
{
|
||||
label: '默认',
|
||||
key: 'medium',
|
||||
},
|
||||
{
|
||||
label: '紧凑',
|
||||
key: 'small',
|
||||
},
|
||||
{
|
||||
label: '宽松',
|
||||
key: 'large',
|
||||
},
|
||||
])
|
||||
|
||||
const handleDropdownClick = (key: ComponentSize) => {
|
||||
sizeOptions.value.forEach((curr) => {
|
||||
if (curr.key === key) {
|
||||
size.value = key
|
||||
|
||||
popoverShow.value = false
|
||||
|
||||
emit('changeSize', key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
sizeOptions,
|
||||
currentSize,
|
||||
handleDropdownClick,
|
||||
popoverShow,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NPopover
|
||||
v-model:show={this.popoverShow}
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
showArrow={false}
|
||||
raw
|
||||
>
|
||||
{{
|
||||
trigger: () => (
|
||||
<NPopover>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
name="adjustment"
|
||||
size="18"
|
||||
customClassName="ray-table-icon"
|
||||
/>
|
||||
),
|
||||
default: () => '表格密度',
|
||||
}}
|
||||
</NPopover>
|
||||
),
|
||||
default: () => (
|
||||
<NCard
|
||||
bordered={false}
|
||||
class="ray-table__table-size ray-table__table-size--dark ray-table__table-size--light"
|
||||
>
|
||||
<div class="table-size__dropdown">
|
||||
<div class="table-size__dropdown-wrapper">
|
||||
{this.sizeOptions.map((curr) => (
|
||||
<div
|
||||
class={[
|
||||
'dropdown-item',
|
||||
curr.key === this.currentSize
|
||||
? 'dropdown-item--active'
|
||||
: '',
|
||||
]}
|
||||
key={curr.key}
|
||||
onClick={this.handleDropdownClick.bind(
|
||||
this,
|
||||
curr.key as ComponentSize,
|
||||
)}
|
||||
>
|
||||
<div class="drop-item__label">{curr.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
),
|
||||
}}
|
||||
</NPopover>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TableSize
|
39
src/components/RayTable/src/index.scss
Normal file
39
src/components/RayTable/src/index.scss
Normal file
@ -0,0 +1,39 @@
|
||||
@keyframes scaleScreenfull {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.ray-table {
|
||||
& .ray-table-icon {
|
||||
transition: transform 0.3s var(--r-bezier);
|
||||
|
||||
&:hover {
|
||||
animation: scaleScreenfull 0.3s linear;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
}
|
||||
|
||||
& .ray-table__setting,
|
||||
& .ray-table-icon {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
& .n-card-header .n-card-header__main {
|
||||
padding-right: var(--ray-table-header-space);
|
||||
}
|
||||
|
||||
& .ray-table-header-extra__space {
|
||||
display: flex;
|
||||
gap: 0 12px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
328
src/components/RayTable/src/index.tsx
Normal file
328
src/components/RayTable/src/index.tsx
Normal file
@ -0,0 +1,328 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-12-08
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* <https://www.naiveui.com/zh-CN/dark/components/data-table>
|
||||
*
|
||||
* 完全继承 `NDataTable`, 该组件继承 `NDataTable Props` 属性和方法
|
||||
*
|
||||
* 实现: 抬头, 操作栏, 右键菜单功能拓展, 输出 `excel`, 表格尺寸调整
|
||||
*
|
||||
* 右键菜单功能, 需要同时启用 `showMenu` 与配置菜单选项才能正常使用
|
||||
*
|
||||
* 可以通过设置 `action` 为 `false` 隐藏操作栏
|
||||
*
|
||||
* 具体拓展 `props` 方法, 可以查看 `props.ts` 中相关注释与代码
|
||||
*
|
||||
* 基于 `xlsx.js` 实现输出 `excel`
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 为什么有些拓展功能是写在该组件内, 有些则是完全抽离出去呢...
|
||||
* 好问题, 因为我一开始没想到而且我又想偷懒
|
||||
*
|
||||
* 凑合凑合看吧, 等我啥时候有空再抽离出去
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NDataTable, NCard, NDropdown } from 'naive-ui'
|
||||
import TableSetting from './components/TableSetting/index'
|
||||
import TableAction from './components/TableAction/index'
|
||||
import TableSize from './components/TableSize/index'
|
||||
import TableScreenfull from './components/TableScreenfull/index'
|
||||
|
||||
import props from './props'
|
||||
import print from 'print-js'
|
||||
import { uuid } from '@use-utils/hook'
|
||||
import { exportFileToXLSX } from '@use-utils/xlsx'
|
||||
|
||||
import type { ActionOptions } from './type'
|
||||
import type { WritableComputedRef } from 'vue'
|
||||
import type { DropdownOption } from 'naive-ui'
|
||||
import type { ExportExcelHeader } from '@use-utils/xlsx'
|
||||
import type { DataTableInst } from 'naive-ui'
|
||||
import type { ComponentSize } from '@/types/modules/component'
|
||||
|
||||
const RayTable = defineComponent({
|
||||
name: 'RayTable',
|
||||
props: props,
|
||||
emits: ['update:columns', 'menuSelect', 'exportSuccess', 'exportError'],
|
||||
setup(props, { emit, expose }) {
|
||||
const rayTableInstance = ref<DataTableInst>()
|
||||
|
||||
const tableUUID = uuid(16) // 表格 id, 用于打印表格
|
||||
const rayTableUUID = uuid(16) // RayTable id, 用于全屏表格
|
||||
const modelRightClickMenu = computed(() => props.rightClickMenu)
|
||||
const modelColumns = computed({
|
||||
get: () => props.columns,
|
||||
set: (arr) => {
|
||||
emit('update:columns', arr)
|
||||
},
|
||||
}) as unknown as WritableComputedRef<ActionOptions[]>
|
||||
const menuConfig = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
showMenu: false,
|
||||
})
|
||||
let prevRightClickIndex = -1 // 缓存上次点击索引位置
|
||||
const cssVars = computed(() => {
|
||||
const cssVar = {
|
||||
'--ray-table-header-space': props.tableHeaderSpace,
|
||||
}
|
||||
|
||||
return cssVar
|
||||
})
|
||||
const tableSize = ref(props.size)
|
||||
const tableMethods = ref<Omit<DataTableInst, 'clearFilter'>>()
|
||||
|
||||
/** 注入相关属性 */
|
||||
provide('tableSettingProvider', {
|
||||
modelRightClickMenu,
|
||||
modelColumns,
|
||||
size: props.size,
|
||||
rayTableUUID,
|
||||
})
|
||||
|
||||
/** 拖拽更新后的表格列 */
|
||||
const handleColumnsUpdate = (arr: ActionOptions[]) => {
|
||||
modelColumns.value = arr
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key 右键菜单当前选择 `key`
|
||||
* @param option 右键菜单当前 `item`
|
||||
*
|
||||
* @remark (key: string | number, index: number,option: DropdownOption) => void
|
||||
*/
|
||||
const handleRightMenuSelect = (
|
||||
key: string | number,
|
||||
option: DropdownOption,
|
||||
) => {
|
||||
emit('menuSelect', key, prevRightClickIndex, option)
|
||||
|
||||
menuConfig.showMenu = false
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param arr 表格当前行
|
||||
* @param idx 表格当前索引位置
|
||||
* @returns 自定义属性集
|
||||
*
|
||||
* @remark 集成右键菜单属性, 会自动拦截右键方法, 会自动合并自定义行属性
|
||||
*/
|
||||
const handleRowProps = (arr: ActionOptions, idx: number) => {
|
||||
const interceptRowProps = props.rowProps?.(arr, idx)
|
||||
|
||||
return {
|
||||
...interceptRowProps,
|
||||
onContextmenu: (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
prevRightClickIndex = idx
|
||||
|
||||
menuConfig.showMenu = false
|
||||
|
||||
nextTick().then(() => {
|
||||
menuConfig.showMenu = true
|
||||
|
||||
menuConfig.x = e.clientX
|
||||
menuConfig.y = e.clientY
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 导出表格数据为 `excel`
|
||||
*
|
||||
* 基于 `xlsx`
|
||||
*
|
||||
* 按需导入 `xlsx` 减少体积, 不依赖传统 `file save` 插件导出方式
|
||||
*/
|
||||
const handleExportPositive = async () => {
|
||||
if (props.data.length && props.columns.length) {
|
||||
try {
|
||||
await exportFileToXLSX(
|
||||
props.data,
|
||||
props.columns as ExportExcelHeader[],
|
||||
{
|
||||
filename: props.exportFilename,
|
||||
},
|
||||
)
|
||||
|
||||
emit('exportSuccess')
|
||||
} catch (e) {
|
||||
emit('exportError')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 打印输出表格内容
|
||||
*
|
||||
* 默认配置按照 `print-js` 配置
|
||||
*
|
||||
* 会自动合并自定义配置项
|
||||
*
|
||||
* 受到 `print-js` 限制有些样式是无法打印输出的
|
||||
*/
|
||||
const handlePrintPositive = () => {
|
||||
const options = Object.assign({}, props.printOptions, {
|
||||
printable: tableUUID,
|
||||
type: props.printType,
|
||||
documentTitle: props.printOptions.documentTitle
|
||||
? props.printOptions.documentTitle
|
||||
: '表格',
|
||||
})
|
||||
|
||||
print(options)
|
||||
}
|
||||
|
||||
/** 更新后的表格尺寸 */
|
||||
const handleChangeTableSize = (size: ComponentSize) => {
|
||||
tableSize.value = size
|
||||
}
|
||||
|
||||
const registerRayTableMethods = (ins: DataTableInst) => {
|
||||
const {
|
||||
clearFilters,
|
||||
clearSorter,
|
||||
filters,
|
||||
page,
|
||||
scrollTo,
|
||||
sort,
|
||||
filter,
|
||||
} = ins
|
||||
|
||||
tableMethods.value = {
|
||||
clearFilters,
|
||||
clearSorter,
|
||||
filters,
|
||||
page,
|
||||
scrollTo,
|
||||
sort,
|
||||
filter,
|
||||
}
|
||||
}
|
||||
|
||||
expose({
|
||||
tableMethods: computed(() => tableMethods.value),
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
registerRayTableMethods(rayTableInstance.value as DataTableInst)
|
||||
})
|
||||
|
||||
return {
|
||||
tableUUID,
|
||||
rayTableUUID,
|
||||
handleColumnsUpdate,
|
||||
...toRefs(menuConfig),
|
||||
handleRowProps,
|
||||
handleRightMenuSelect,
|
||||
handleExportPositive,
|
||||
handlePrintPositive,
|
||||
cssVars,
|
||||
handleChangeTableSize,
|
||||
tableSize,
|
||||
rayTableInstance,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NCard
|
||||
class="ray-table"
|
||||
bordered={this.bordered}
|
||||
style={[this.cssVars]}
|
||||
{...{ id: this.rayTableUUID }}
|
||||
>
|
||||
{{
|
||||
default: () => (
|
||||
<>
|
||||
<NDataTable
|
||||
ref="rayTableInstance"
|
||||
{...{ id: this.tableUUID }}
|
||||
{...this.$props}
|
||||
rowProps={this.handleRowProps.bind(this)}
|
||||
size={this.tableSize}
|
||||
>
|
||||
{{
|
||||
...this.$slots,
|
||||
}}
|
||||
</NDataTable>
|
||||
{this.showMenu ? (
|
||||
// 右键菜单
|
||||
<NDropdown
|
||||
show={this.showMenu}
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
x={this.x}
|
||||
y={this.y}
|
||||
options={this.rightClickMenu}
|
||||
onClickoutside={() => (this.showMenu = false)}
|
||||
onSelect={this.handleRightMenuSelect.bind(this)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
),
|
||||
header: () => this.title,
|
||||
'header-extra': () =>
|
||||
this.action ? (
|
||||
<div class="ray-table-header-extra__space">
|
||||
{/* 打印输出操作 */}
|
||||
<TableAction
|
||||
icon={this.printIcon}
|
||||
tooltip={this.printTooltip}
|
||||
popoverContent="打印表格"
|
||||
positiveText={this.printPositiveText}
|
||||
negativeText={this.printNegativeText}
|
||||
onPositive={this.handlePrintPositive.bind(this)}
|
||||
/>
|
||||
{/* 输出为Excel表格 */}
|
||||
<TableAction
|
||||
icon={this.exportExcelIcon}
|
||||
tooltip={this.exportTooltip}
|
||||
popoverContent="导出表格"
|
||||
positiveText={this.exportPositiveText}
|
||||
negativeText={this.exportNegativeText}
|
||||
onPositive={this.handleExportPositive.bind(this)}
|
||||
/>
|
||||
{/* 表格尺寸调整 */}
|
||||
<TableSize
|
||||
onChangeSize={this.handleChangeTableSize.bind(this)}
|
||||
/>
|
||||
{/* 全屏表格 */}
|
||||
<TableScreenfull />
|
||||
{/* 表格列操作 */}
|
||||
<TableSetting
|
||||
onColumnsUpdate={this.handleColumnsUpdate.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
footer: () => this.$slots.tableFooter?.(),
|
||||
}}
|
||||
</NCard>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RayTable
|
219
src/components/RayTable/src/props.ts
Normal file
219
src/components/RayTable/src/props.ts
Normal file
@ -0,0 +1,219 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2022-12-08
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import { dataTableProps } from 'naive-ui'
|
||||
|
||||
import type { PropType, VNode, VNodeChild } from 'vue'
|
||||
import type { DropdownMixedOption } from './type'
|
||||
import type PrintConfiguration from 'print-js'
|
||||
|
||||
const rayTableProps = {
|
||||
...dataTableProps, // 继承 `data table props`
|
||||
rightClickMenu: {
|
||||
/**
|
||||
*
|
||||
* 表格右键菜单, 基于 `NDropdown` 实现
|
||||
*
|
||||
* 如果菜单内容长度为 `0` 则不会渲染
|
||||
*
|
||||
* 只需要传入对应的菜单配置项, 即可自动开启右键菜单功能
|
||||
*/
|
||||
type: Array as PropType<DropdownMixedOption[]>,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
/**
|
||||
*
|
||||
* 表格标题
|
||||
*
|
||||
* 可以自定义渲染
|
||||
*/
|
||||
type: [String, Object] as PropType<string | VNodeChild>,
|
||||
default: '',
|
||||
},
|
||||
action: {
|
||||
/**
|
||||
*
|
||||
* 是否开启操作栏
|
||||
*
|
||||
* 默认开启
|
||||
*/
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
actionExtra: {
|
||||
/**
|
||||
*
|
||||
* 自定义拓展操作栏
|
||||
*
|
||||
* 暂时不开放
|
||||
*/
|
||||
type: Object as PropType<VNode>,
|
||||
default: () => ({}),
|
||||
},
|
||||
showMenu: {
|
||||
/**
|
||||
*
|
||||
* 是否展示右键菜单
|
||||
*
|
||||
* 默认启用
|
||||
*/
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
exportTooltip: {
|
||||
/**
|
||||
*
|
||||
* 导出表格提示
|
||||
*/
|
||||
type: String,
|
||||
default: '是否导出为Excel表格?',
|
||||
},
|
||||
exportType: {
|
||||
/**
|
||||
*
|
||||
* 导出类型
|
||||
*
|
||||
* 默认为 `xlsx`
|
||||
*
|
||||
* 暂时只支持导出为 `xlsx`
|
||||
*/
|
||||
type: String,
|
||||
default: 'xlsx',
|
||||
},
|
||||
exportPositiveText: {
|
||||
/**
|
||||
*
|
||||
* 导出确认按钮文字
|
||||
*
|
||||
* 默认为 `确认`
|
||||
*/
|
||||
type: String,
|
||||
default: '确认',
|
||||
},
|
||||
exportNegativeText: {
|
||||
/**
|
||||
*
|
||||
* 导出取消按钮文字
|
||||
*
|
||||
* 默认为 `取消`
|
||||
*/
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
exportFilename: {
|
||||
/**
|
||||
*
|
||||
* 导出表格名称
|
||||
*/
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
printPositiveText: {
|
||||
/**
|
||||
*
|
||||
* 打印确认按钮文字
|
||||
*
|
||||
* 默认为 `确认`
|
||||
*/
|
||||
type: String,
|
||||
default: '确认',
|
||||
},
|
||||
printNegativeText: {
|
||||
/**
|
||||
*
|
||||
* 打印取消按钮文字
|
||||
*
|
||||
* 默认为 `取消`
|
||||
*/
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
printTooltip: {
|
||||
/**
|
||||
*
|
||||
* 打印表格提示
|
||||
*/
|
||||
type: String,
|
||||
default: '是否打印该表格?',
|
||||
},
|
||||
printType: {
|
||||
/**
|
||||
*
|
||||
* 打印输出类型: 'pdf' | 'html' | 'image' | 'json'
|
||||
*
|
||||
* 默认为 `html`
|
||||
*/
|
||||
type: String as PropType<PrintConfiguration.PrintTypes>,
|
||||
default: 'html',
|
||||
},
|
||||
printOptions: {
|
||||
/**
|
||||
*
|
||||
* `print-js` 打印配置项
|
||||
*
|
||||
* 会自动过滤: `printable`, 'type'
|
||||
*/
|
||||
type: Object as PropType<
|
||||
Omit<PrintConfiguration.Configuration, 'printable' | 'type'>
|
||||
>,
|
||||
default: () => ({}),
|
||||
},
|
||||
printIcon: {
|
||||
/**
|
||||
*
|
||||
* 打印按钮自定义图标名称
|
||||
*
|
||||
* 需要结合 `RayIcon` 组件使用
|
||||
*
|
||||
* 如果需要自定义图标, 则需要在 `src/icons` 中添加后使用
|
||||
*/
|
||||
type: String,
|
||||
default: 'print',
|
||||
},
|
||||
exportExcelIcon: {
|
||||
/**
|
||||
*
|
||||
* 导出为表格按钮自定义图标名称
|
||||
*
|
||||
* 需要结合 `RayIcon` 组件使用
|
||||
*
|
||||
* 如果需要自定义图标, 则需要在 `src/icons` 中添加后使用
|
||||
*/
|
||||
type: String,
|
||||
default: 'export_excel',
|
||||
},
|
||||
tableHeaderSpace: {
|
||||
/**
|
||||
*
|
||||
* 表格头部操作栏, 主要操作栏与额外操作栏之间间隔
|
||||
*/
|
||||
type: String,
|
||||
default: '10px',
|
||||
},
|
||||
bordered: {
|
||||
/**
|
||||
*
|
||||
* 表格边框
|
||||
*/
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
export default rayTableProps
|
||||
|
||||
/**
|
||||
*
|
||||
* `Ray Table Props`
|
||||
*
|
||||
* 继承 `Naive UI Data Table` <https://www.naiveui.com/zh-CN/dark/components/data-table>
|
||||
*/
|
78
src/components/RayTable/src/type.ts
Normal file
78
src/components/RayTable/src/type.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import type {
|
||||
DropdownOption,
|
||||
DropdownGroupOption,
|
||||
DropdownDividerOption,
|
||||
DropdownRenderOption,
|
||||
DataTableBaseColumn,
|
||||
DataTableInst,
|
||||
} from 'naive-ui'
|
||||
import type { ComputedRef, WritableComputedRef, VNode } from 'vue'
|
||||
import type { ComponentSize } from '@/types/modules/component'
|
||||
|
||||
export interface ActionOptions extends DataTableBaseColumn {
|
||||
leftFixedActivated?: boolean // 向左固定
|
||||
rightFixedActivated?: boolean // 向右固定
|
||||
resizeColumnActivated?: boolean // 拖拽表格列
|
||||
}
|
||||
|
||||
export type FixedType = 'left' | 'right' | undefined
|
||||
|
||||
export interface TableSettingFixedPopoverIcon {
|
||||
element: ActionOptions
|
||||
name: string
|
||||
tooltip: string
|
||||
fn: Function
|
||||
index: number
|
||||
fixed: FixedType
|
||||
key: 'leftFixedActivated' | 'rightFixedActivated'
|
||||
}
|
||||
|
||||
export type DropdownMixedOption =
|
||||
| DropdownOption
|
||||
| DropdownGroupOption
|
||||
| DropdownDividerOption
|
||||
| DropdownRenderOption
|
||||
|
||||
export type SettingOptions = WritableComputedRef<ActionOptions[]>
|
||||
|
||||
export type RightClickMenu = ComputedRef<DropdownMixedOption[]>
|
||||
|
||||
export interface TableSettingProvider {
|
||||
modelRightClickMenu: RightClickMenu
|
||||
modelColumns: SettingOptions
|
||||
size: ComponentSize
|
||||
rayTableUUID: string
|
||||
}
|
||||
|
||||
export interface ExportExcelProvider {
|
||||
exportTooltip: string
|
||||
exportType: string
|
||||
exportPositiveText: string
|
||||
exportNegativeText: string
|
||||
exportFilename: string
|
||||
}
|
||||
|
||||
export type ColumnKey = string | number
|
||||
|
||||
declare type VNodeChildAtom =
|
||||
| VNode
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| void
|
||||
|
||||
export declare type VNodeArrayChildren = Array<
|
||||
VNodeArrayChildren | VNodeChildAtom
|
||||
>
|
||||
|
||||
export declare type VNodeChild = VNodeChildAtom | VNodeArrayChildren
|
||||
|
||||
export declare type TableColumnTitle =
|
||||
| string
|
||||
| ((column: DataTableBaseColumn) => VNodeChild)
|
||||
|
||||
export declare type RayTableInst = {
|
||||
tableMethods: Omit<DataTableInst, 'clearFilter'>
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<RouterView>
|
||||
<template #default="{ Component, route }">
|
||||
<transition
|
||||
:name="transitionPropName"
|
||||
:mode="transitionMode"
|
||||
:appear="transitionAppear"
|
||||
>
|
||||
<keep-alive
|
||||
v-if="setupKeepAlive"
|
||||
:max="maxKeepAliveLength"
|
||||
:include="keepAliveInclude"
|
||||
:exclude="keepAliveExclude"
|
||||
>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</transition>
|
||||
</template>
|
||||
</RouterView>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useKeepAlive } from '@/store'
|
||||
import { APP_KEEP_ALIVE } from '@/appConfig/appConfig'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
transitionPropName: {
|
||||
type: String,
|
||||
default: 'fade',
|
||||
},
|
||||
transitionMode: {
|
||||
type: String as PropType<'default' | 'out-in' | 'in-out' | undefined>,
|
||||
default: 'out-in',
|
||||
},
|
||||
transitionAppear: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const keepAliveStore = useKeepAlive()
|
||||
const { keepAliveInclude } = storeToRefs(keepAliveStore)
|
||||
const { setupKeepAlive, maxKeepAliveLength, keepAliveExclude } = APP_KEEP_ALIVE
|
||||
</script>
|
39
src/dayjs/index.ts
Normal file
39
src/dayjs/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-02
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { DEFAULT_DAYJS_LOCAL, DAYJS_LOCAL_MAP } from '@/appConfig/localConfig'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
import type { DayjsLocal } from './type'
|
||||
|
||||
export const setupDayjs = () => {
|
||||
dayjs.locale(DEFAULT_DAYJS_LOCAL)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* dayjs hook
|
||||
*
|
||||
* 说明:
|
||||
* - locale: 切换 dayjs 语言配置
|
||||
*/
|
||||
export const useDayjs = () => {
|
||||
const locale = (key: DayjsLocal) => {
|
||||
const mapkey = DAYJS_LOCAL_MAP[key]
|
||||
|
||||
mapkey ? dayjs.locale(mapkey) : dayjs.locale(DEFAULT_DAYJS_LOCAL)
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
}
|
||||
}
|
6
src/dayjs/type.ts
Normal file
6
src/dayjs/type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type DayjsLocal = 'zh-cn' | 'en'
|
||||
|
||||
export interface DayjsLocalMap {
|
||||
'zh-CN': 'zh-cn'
|
||||
'en-US': 'en'
|
||||
}
|
38
src/directives/README.md
Normal file
38
src/directives/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
## 说明
|
||||
|
||||
> 全局自定义指令入口。
|
||||
|
||||
## 规范
|
||||
|
||||
- 指令应该为全局的通用性指令
|
||||
- 如果指令需要与系统的数据进行关联,应该注意数据的管理与指令注册使用时机
|
||||
|
||||
## 添加指令说明
|
||||
|
||||
- 模板视 modules 中每一个文件包为一个模板的指令(全局),并且每个文件包的名称,也被视为该指令名称
|
||||
- 添加文件包后,强制要求 index.ts 为指令的输出文件名
|
||||
- modules 包中所有指令都会被自动合并到模板中
|
||||
|
||||
```ts
|
||||
/**
|
||||
*
|
||||
* 示例添加 demo 指令
|
||||
*/
|
||||
|
||||
// 1. modules 中添加文件包
|
||||
// 2. modules/demo 目录下创建 index.ts 文件
|
||||
// 3. 进行自定义指令开发
|
||||
|
||||
import type { Directive } from 'vue'
|
||||
import type { RoleBindingValue } from './type'
|
||||
|
||||
const demoDirective: Directive<HTMLElement, RoleBindingValue> = {
|
||||
beforeMount: (el, binding) => {
|
||||
console.log(el, binding)
|
||||
},
|
||||
}
|
||||
|
||||
export default demoDirective
|
||||
|
||||
// 4. 按照上述步骤执行后,会自动在模板中创建一个 v-demo 的指令供全局使用
|
||||
```
|
215
src/directives/README_DIR.md
Normal file
215
src/directives/README_DIR.md
Normal file
@ -0,0 +1,215 @@
|
||||
## 全局自定义指令
|
||||
|
||||
### v-copy
|
||||
|
||||
- 参数类型: any(参数会强制被 String 方法强制转换)
|
||||
- 默认值: ''
|
||||
|
||||
#### 示例
|
||||
|
||||
```tsx
|
||||
import { NSpace, NCard, NInput, NInputGroup, NButton, NSwitch } from 'naive-ui'
|
||||
|
||||
const Demo = defineComponent({
|
||||
name: 'Demo',
|
||||
setup() {
|
||||
const dmeoCopyValue = ref('hello copy')
|
||||
|
||||
return {
|
||||
dmeoCopyValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NInputGroup>
|
||||
<NInput v-model:value={this.dmeoCopyValue} />
|
||||
<NButton v-copy={this.dmeoCopyValue}>复制</NButton>
|
||||
</NInputGroup>
|
||||
)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### v-debounce
|
||||
|
||||
- 参数类型: DebounceBindingOptions
|
||||
- 默认值:
|
||||
|
||||
```ts
|
||||
{
|
||||
trigger: 'click',
|
||||
wait: 500,
|
||||
options: null
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例
|
||||
|
||||
```tsx
|
||||
import { NSpace, NCard, NInput, NInputGroup, NButton, NSwitch } from 'naive-ui'
|
||||
|
||||
const Demo = defineComponent({
|
||||
name: 'Demo',
|
||||
setup() {
|
||||
const demoValue = ref(0)
|
||||
|
||||
const updateDemoValue = () => {
|
||||
demoValue.value++
|
||||
}
|
||||
|
||||
return {
|
||||
demoValue,
|
||||
updateDemoValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NSpace wrapItem={true} vertical>
|
||||
<NButton
|
||||
v-debounce={{
|
||||
func: this.updateDemoValue,
|
||||
trigger: 'click',
|
||||
wait: 1000,
|
||||
options: {},
|
||||
}}
|
||||
>
|
||||
点击执行
|
||||
</NButton>
|
||||
<p>我执行了{this.demoValue}次</p>
|
||||
<p>该方法将延迟 1s 执行</p>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### v-throttle
|
||||
|
||||
- 参数类型: ThrottleBindingOptions
|
||||
- 默认值:
|
||||
|
||||
```ts
|
||||
{
|
||||
trigger: 'click',
|
||||
wait: 500,
|
||||
options: null
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例
|
||||
|
||||
```tsx
|
||||
import { NSpace, NCard, NInput, NInputGroup, NButton, NSwitch } from 'naive-ui'
|
||||
|
||||
const Demo = defineComponent({
|
||||
name: 'Demo',
|
||||
setup() {
|
||||
const demoValue = ref(0)
|
||||
|
||||
const updateDemoValue = () => {
|
||||
demoValue.value++
|
||||
}
|
||||
|
||||
return {
|
||||
demoValue,
|
||||
updateDemoValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NSpace wrapItem={true} vertical>
|
||||
<NButton
|
||||
v-throttle={{
|
||||
func: this.updateDemoValue,
|
||||
trigger: 'click',
|
||||
wait: 1000,
|
||||
options: {},
|
||||
}}
|
||||
>
|
||||
点击执行
|
||||
</NButton>
|
||||
<p>我执行了{this.demoValue}次</p>
|
||||
<p>该方法 1s 内仅会执行一次</p>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### v-disabled
|
||||
|
||||
- 参数类型: boolean
|
||||
- 默认值: false
|
||||
|
||||
#### Tip
|
||||
|
||||
> 该指令基于 css 层面进行禁用操作,如果元素含有 `disabled` 属性方法,会尝试使用原生 `disabled` 属性。对于组件库而言,本身就提供了丰富的 `disabled props`,所以应该是优先使用组件自带属性。
|
||||
|
||||
#### 示例
|
||||
|
||||
```tsx
|
||||
import {
|
||||
NSpace,
|
||||
NCard,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NButton,
|
||||
NSwitch,
|
||||
NForm,
|
||||
NFormItem,
|
||||
} from 'naive-ui'
|
||||
|
||||
const Demo = defineComponent({
|
||||
name: 'Demo',
|
||||
setup() {
|
||||
const disabledValue = ref(false)
|
||||
|
||||
return {
|
||||
disabledValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NSpace vertical>
|
||||
<NSwitch v-model:value={this.disabledValue}>
|
||||
{{
|
||||
checked: () => '取消',
|
||||
unchecked: () => '禁用',
|
||||
}}
|
||||
</NSwitch>
|
||||
<p>
|
||||
该指令会强制禁用(通过 css 层面)禁用元素交互。但是 naive ui
|
||||
组件提供了完整的 disabled
|
||||
属性,所以在组件库有禁用需求时,直接调用组件库 disabled 属性即可
|
||||
</p>
|
||||
<NSpace vertical wrapItem={false}>
|
||||
<NCard title="原生表单" bordered={false}>
|
||||
<NSpace vertical wrapItem={false}>
|
||||
<form v-disabled={this.disabledValue}>
|
||||
<input type="text" placeholder="请输入" />
|
||||
<button>提交</button>
|
||||
</form>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<NCard title="文本内容" bordered={false}>
|
||||
<NSpace vertical wrapItem={false}>
|
||||
<p v-disabled={this.disabledValue}>我是可以被禁用的文本内容</p>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<NCard title="naive 组件" bordered={false}>
|
||||
<NSpace vertical wrapItem={false} justify="start">
|
||||
<NButton v-disabled={this.disabledValue}>按钮</NButton>
|
||||
<NForm v-disabled={this.disabledValue}>
|
||||
<NFormItem label="名称">
|
||||
<NInput />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NSwitch v-disabled={this.disabledValue}></NSwitch>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
})
|
||||
```
|
33
src/directives/helper/combine.ts
Normal file
33
src/directives/helper/combine.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-24
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import type { DirectiveModules } from '@/directives/type'
|
||||
|
||||
export const combineDirective = <
|
||||
T extends Record<string, DirectiveModules>,
|
||||
K extends keyof T,
|
||||
>(
|
||||
directiveModules: T,
|
||||
) => {
|
||||
const directives = Object.keys(directiveModules).reduce((pre, curr) => {
|
||||
if (directiveModules[curr]?.default) {
|
||||
const value = directiveModules[curr]?.default
|
||||
|
||||
pre[curr] = value
|
||||
|
||||
return pre
|
||||
} else {
|
||||
throw new Error('directiveModules[curr]?.default is undefined')
|
||||
}
|
||||
}, {} as Record<K, T[K]['default']>)
|
||||
|
||||
return directives
|
||||
}
|
49
src/directives/index.ts
Normal file
49
src/directives/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-24
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import { combineDirective } from './helper/combine'
|
||||
import { forIn } from 'lodash-es'
|
||||
import { isValueType } from '@/utils/hook'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { DirectiveModules } from '@/directives/type'
|
||||
|
||||
/**
|
||||
*
|
||||
* 初始化全局自定义指令
|
||||
*
|
||||
* 该方法会将 modules 下每个文件夹视为一个指令
|
||||
* 并且会将文件夹名称识别为指令名称
|
||||
* 每个文件下的 index.ts 文件视为每个指令的入口(也就是指令的处理逻辑, 需要暴露出一个 Directive 类型的对象)
|
||||
*/
|
||||
export const setupDirective = (app: App<Element>) => {
|
||||
// 获取 modules 包下所有的 index.ts 文件
|
||||
const directiveRawModules: Record<string, DirectiveModules> =
|
||||
import.meta.glob('./modules/**/index.ts', {
|
||||
eager: true,
|
||||
})
|
||||
// 将所有的包提取出来(./modules/[file-name]/index.ts)
|
||||
const directivesModules = combineDirective(directiveRawModules)
|
||||
// 提取文件名(./modules/copy/index.ts => copy)
|
||||
const reg = /(?<=modules\/).*(?=\/index\.ts)/
|
||||
|
||||
forIn(directivesModules, (value, key) => {
|
||||
const dname = key.match(reg)?.[0]
|
||||
|
||||
if (isValueType<string>(dname, 'String')) {
|
||||
app.directive(dname, value)
|
||||
} else {
|
||||
throw new Error(
|
||||
'directiveName is not string, please check your directive file name',
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
54
src/directives/modules/copy/index.ts
Normal file
54
src/directives/modules/copy/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-24
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* directive name: copy
|
||||
*/
|
||||
|
||||
import ClipboardJS from 'clipboard'
|
||||
|
||||
import type { Directive } from 'vue'
|
||||
import type { CopyElement } from './type'
|
||||
|
||||
let clipboard: ClipboardJS | null
|
||||
|
||||
const copyDirective: Directive<CopyElement, string> = {
|
||||
mounted: (el, binding) => {
|
||||
const value = binding.value
|
||||
|
||||
clipboard = new ClipboardJS(el, {
|
||||
text: () => String(value),
|
||||
})
|
||||
|
||||
clipboard?.on('success', () => {
|
||||
window.$message.success('复制成功')
|
||||
})
|
||||
clipboard?.on('error', () => {
|
||||
window.$message.error('复制失败')
|
||||
})
|
||||
},
|
||||
updated: (el, binding) => {
|
||||
/** 其实这块代码写的挺蠢的, 但是我目前不知道怎么去优化, 阿巴阿巴阿巴 */
|
||||
const value = binding.value
|
||||
|
||||
clipboard = new ClipboardJS(el, {
|
||||
text: () => String(value),
|
||||
})
|
||||
},
|
||||
beforeUnmount: () => {
|
||||
clipboard?.destroy()
|
||||
|
||||
clipboard = null
|
||||
},
|
||||
}
|
||||
|
||||
export default copyDirective
|
3
src/directives/modules/copy/type.ts
Normal file
3
src/directives/modules/copy/type.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface CopyElement extends Element, UnknownObjectKey {
|
||||
$value: string
|
||||
}
|
49
src/directives/modules/debounce/index.ts
Normal file
49
src/directives/modules/debounce/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-24
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* directive name: debounce
|
||||
*/
|
||||
|
||||
import { debounce } from 'lodash-es'
|
||||
import { on, off } from '@use-utils/element'
|
||||
|
||||
import type { Directive } from 'vue'
|
||||
import type { DebounceBindingOptions } from './type'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
let debounceFunction: AnyFunc | null
|
||||
|
||||
const debounceDirective: Directive<HTMLElement, DebounceBindingOptions> = {
|
||||
beforeMount: (el, binding) => {
|
||||
const { func, trigger = 'click', wait = 500, options } = binding.value
|
||||
|
||||
if (typeof func !== 'function') {
|
||||
throw new Error('debounce directive value must be a function')
|
||||
}
|
||||
|
||||
debounceFunction = debounce(func, wait, Object.assign({}, {}, options))
|
||||
|
||||
on(el, trigger, debounceFunction)
|
||||
},
|
||||
beforeUnmount: (el, binding) => {
|
||||
const { trigger = 'click' } = binding.value
|
||||
|
||||
if (debounceFunction) {
|
||||
off(el, trigger, debounceFunction)
|
||||
}
|
||||
|
||||
debounceFunction = null
|
||||
},
|
||||
}
|
||||
|
||||
export default debounceDirective
|
9
src/directives/modules/debounce/type.ts
Normal file
9
src/directives/modules/debounce/type.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { DebounceSettings } from 'lodash-es'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
export interface DebounceBindingOptions {
|
||||
func: AnyFunc
|
||||
trigger: string
|
||||
wait: number
|
||||
options: DebounceSettings
|
||||
}
|
43
src/directives/modules/disabled/index.ts
Normal file
43
src/directives/modules/disabled/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-26
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* directive name: disabled
|
||||
*/
|
||||
|
||||
import { addClass, removeClass } from '@/utils/element'
|
||||
|
||||
import type { Directive } from 'vue'
|
||||
|
||||
const updateElementDisabledType = (el: HTMLElement, value: boolean) => {
|
||||
if (el) {
|
||||
const classes = 'ray-template__directive--disabled'
|
||||
|
||||
value ? addClass(el, classes) : removeClass(el, classes)
|
||||
el?.setAttribute('disabled', value ? 'disabled' : '')
|
||||
}
|
||||
}
|
||||
|
||||
const disabledDirective: Directive<HTMLElement, boolean> = {
|
||||
mounted: (el, binding) => {
|
||||
const value = binding.value
|
||||
|
||||
updateElementDisabledType(el, value)
|
||||
},
|
||||
updated: (el, binding) => {
|
||||
const value = binding.value
|
||||
|
||||
updateElementDisabledType(el, value)
|
||||
},
|
||||
}
|
||||
|
||||
export default disabledDirective
|
49
src/directives/modules/throttle/index.ts
Normal file
49
src/directives/modules/throttle/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-24
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* directive name: throttle
|
||||
*/
|
||||
|
||||
import { throttle } from 'lodash-es'
|
||||
import { on, off } from '@use-utils/element'
|
||||
|
||||
import type { Directive } from 'vue'
|
||||
import type { ThrottleBindingOptions } from './type'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
let throttleFunction: AnyFunc | null
|
||||
|
||||
const throttleDirective: Directive<HTMLElement, ThrottleBindingOptions> = {
|
||||
beforeMount: (el, binding) => {
|
||||
const { func, trigger = 'click', wait = 500, options } = binding.value
|
||||
|
||||
if (typeof func !== 'function') {
|
||||
throw new Error('throttle directive value must be a function')
|
||||
}
|
||||
|
||||
throttleFunction = throttle(func, wait, Object.assign({}, {}, options))
|
||||
|
||||
on(el, trigger, throttleFunction)
|
||||
},
|
||||
beforeUnmount: (el, binding) => {
|
||||
const { trigger = 'click' } = binding.value
|
||||
|
||||
if (throttleFunction) {
|
||||
off(el, trigger, throttleFunction)
|
||||
}
|
||||
|
||||
throttleFunction = null
|
||||
},
|
||||
}
|
||||
|
||||
export default throttleDirective
|
9
src/directives/modules/throttle/type.ts
Normal file
9
src/directives/modules/throttle/type.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { ThrottleSettings } from 'lodash-es'
|
||||
import type { AnyFunc } from '@/types/modules/utils'
|
||||
|
||||
export interface ThrottleBindingOptions {
|
||||
func: AnyFunc
|
||||
trigger: string
|
||||
wait: number
|
||||
options: ThrottleSettings
|
||||
}
|
5
src/directives/type.ts
Normal file
5
src/directives/type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { Directive } from 'vue'
|
||||
|
||||
export interface DirectiveModules extends Object {
|
||||
default: Directive
|
||||
}
|
5
src/error/PageResult/index.scss
Normal file
5
src/error/PageResult/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.error-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
@include flexCenter;
|
||||
}
|
50
src/error/PageResult/index.tsx
Normal file
50
src/error/PageResult/index.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-02
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 错误页面
|
||||
*
|
||||
* 基于 NResult 组件实现, 继承该组件所有 props 与 slots
|
||||
* 可以当作一个组件使用, 也可以当作一个页面调用
|
||||
*/
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { NResult, NButton } from 'naive-ui'
|
||||
|
||||
import { redirectRouterToDashboard } from '@/router/helper/routerCopilot'
|
||||
import { resultProps } from 'naive-ui'
|
||||
|
||||
const PageResult = defineComponent({
|
||||
name: 'PageResult',
|
||||
props: {
|
||||
...resultProps,
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class="error-page">
|
||||
<NResult {...this.$props} status="500" title="小调皮你走错地方了">
|
||||
{{
|
||||
...this.$slots,
|
||||
footer: () => (
|
||||
<NButton onClick={redirectRouterToDashboard.bind(this, false)}>
|
||||
返回首页
|
||||
</NButton>
|
||||
),
|
||||
}}
|
||||
</NResult>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default PageResult
|
10
src/error/README.md
Normal file
10
src/error/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
## 异常
|
||||
|
||||
### 说明
|
||||
|
||||
- 当前系统使用 Error404 作为系统的默认异常页重定向页面(可自行替换或者更改)
|
||||
- 提供公共的入口组件,虽然没啥大用,强迫症患者典型
|
||||
|
||||
### 更改与替换
|
||||
|
||||
> 如果需要自己更改,或者补充更多的异常页面,可以在当前文件的 views 下补充(建议这么做哈)。也可以直接摒弃现有的东西,全部重新折腾一次,因为这个东西,一般不会太复杂。
|
3
src/error/index.ts
Normal file
3
src/error/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PageResult from './PageResult/index'
|
||||
|
||||
export default PageResult
|
24
src/error/views/Error404/index.tsx
Normal file
24
src/error/views/Error404/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-06-02
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import PageResult from '@/error/index'
|
||||
|
||||
const ErrorPage404 = defineComponent({
|
||||
name: 'ErrorPage404',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return <PageResult status="404" />
|
||||
},
|
||||
})
|
||||
|
||||
export default ErrorPage404
|
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