Compare commits

..

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

600 changed files with 10195 additions and 37943 deletions

View File

@ -1,5 +0,0 @@
node_modules
.git
.gitignore
*.md
dist

View File

@ -1,4 +1,6 @@
#生产环境 #生产环境
NODE_ENV = 'production'
VITE_APP_URL = '/' VITE_APP_URL = '/'
# office 服务代理地址 # office 服务代理地址

17
.eslintignore Normal file
View File

@ -0,0 +1,17 @@
dist/*
node_modules/*
auto-imports.d.ts
components.d.ts
.gitignore
.vscode
public
yarn.*
vite-env.*
.prettierrc.*
visualizer.*
visualizer.html
.env.*
src/locales/lang
.depcheckrc
src/components/RayChart/theme
*.md

201
.eslintrc.cjs Normal file
View File

@ -0,0 +1,201 @@
/* eslint-env node */
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
ignorePatterns: ['node_modules/', 'dist/'],
extends: [
'eslint-config-prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
'prettier',
'./unplugin/.eslintrc-auto-import.json',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
plugins: ['vue', '@typescript-eslint', 'prettier'],
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
defineOptions: 'readonly',
},
rules: {
'linebreak-style': ['error', 'unix'],
'@typescript-eslint/no-explicit-any': [
'error',
{
ignoreRestArgs: true,
},
],
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false },
], // 强制导入类型显示标注 `import type xxx from 'xxx'`
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'accessor-pairs': 2, // 强制同时存在 `get` 与 `set`
'constructor-super': 0, // 强制子类构造函数中使用 `super` 调用父类的构造函数
'default-case': 2, // `switch` 中强制含有 `default`
eqeqeq: [2, 'allow-null'], // 强制使用严格判断 `===`
'no-alert': 0, // 禁止使用 `alert`、`confirm`
'no-array-constructor': 2, // 禁止使用数组构造器
'no-bitwise': 0, // 禁止使用按位运算符
'no-caller': 1, // 禁止使用 `arguments.caller`、`arguments.callee`
'no-catch-shadow': 2, // 禁止 `catch` 子句参数与外部作用域变量同名
'no-class-assign': 2, // 禁止给类赋值
'no-cond-assign': 2, // 禁止在条件表达式中使用赋值语句
'no-const-assign': 2, // 禁止修改 `const` 声明的变量
'no-constant-condition': 2, // 禁止在条件中使用常量表达式 `if(true)`、`if(1)`
'no-dupe-keys': 2, // 在创建对象字面量时不允许 `key` 重复
'no-dupe-args': 2, // 函数参数不能重复
'no-duplicate-case': 2, // `switch` 中的 `case` 标签不能重复
'no-eval': 1, // 禁止使用 `eval`
'no-ex-assign': 2, // 禁止给 `catch` 语句中的异常参数赋值
'no-extend-native': 2, // 禁止扩展 `native` 对象
'no-extra-bind': 2, // 禁止不必要的函数绑定
'no-extra-boolean-cast': [
'error',
{
enforceForLogicalOperands: true,
},
], // 禁止不必要的 `bool` 转换
'no-extra-parens': 0, // 禁止非必要的括号
semi: ['error', 'never', { beforeStatementContinuationChars: 'always' }],
'no-fallthrough': 1, // 禁止 `switch` 穿透
'no-func-assign': 2, // 禁止重复的函数声明
'no-implicit-coercion': [
'error',
{
allow: ['!!', '~'],
},
], // 禁止隐式转换
'no-implied-eval': 2, // 禁止使用隐式 `eval`
'no-invalid-regexp': 2, // 禁止无效的正则表达式
'no-invalid-this': 2, // 禁止无效的 `this`
'no-irregular-whitespace': 2, // 禁止含有不合法空格
'no-iterator': 2, // 禁止使用 `__iterator__ ` 属性
'no-label-var': 2, // `label` 名不能与 `var` 声明的变量名相同
'no-labels': 2, // 禁止标签声明
'no-lone-blocks': 2, // 禁止不必要的嵌套块
'no-multi-spaces': 1, // 禁止使用多余的空格
'no-multiple-empty-lines': [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: false,
},
],
'vue/no-unused-refs': ['error'],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/attribute-hyphenation': [
'error',
'never',
{
ignore: [],
},
],
'vue/no-restricted-static-attribute': [
'error',
{
key: 'key',
message: 'Disallow using key as a custom attribute',
},
],
},
}

2
.evnrc
View File

@ -1,2 +0,0 @@
layout shell zsh
layout_fnm

12
.gitattributes vendored
View File

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

View File

@ -1,38 +0,0 @@
name: ray-template documents deploy
on:
push:
pull_request:
types:
- closed
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 22.x
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
run_install: false
- name: Install dependencies
run: pnpm install
- name: Pnpm build
run: pnpm build
- name: Deploy to dist branch
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: dist
folder: dist/production

View File

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

2
.gitignore vendored
View File

@ -15,7 +15,6 @@ dist/
*.local *.local
visualizer.* visualizer.*
.eslintcache .eslintcache
.history
# Editor directories and files # Editor directories and files
.idea .idea
@ -28,3 +27,4 @@ visualizer.*
yarn-*.* yarn-*.*
yarn.* yarn.*
pnpm.* pnpm.*
pnpm-lock.yaml

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1" pnpm lint-staged --allow-empty "$1"

View File

@ -1,8 +0,0 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

View File

@ -1,7 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ -n "$CI" ] && exit 0
pnpm lint-staged --allow-empty "$1" pnpm lint-staged --allow-empty "$1"

View File

@ -1,6 +1,9 @@
node_modules/* node_modules/*
dist/* dist/*
yarn.lock
yarn-error.log
visualizer.html visualizer.html
pnpm-lock.yaml
.idea .idea
auto-imports.d.ts auto-imports.d.ts
components.d.ts components.d.ts

1
.nvmrc
View File

@ -1 +0,0 @@
v22.12.0

View File

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

View File

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

41
.vscode/settings.json vendored
View File

@ -1,5 +1,4 @@
{ {
"editor.formatOnSave": true,
"i18n-ally.localesPaths": ["src/locales/lang"], "i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
@ -10,43 +9,5 @@
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"], "i18n-ally.enabledFrameworks": ["vue", "react"],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"alias-skip.mappings": { "synthwave84.disableGlow": true
"@": "/src",
"@use-utils": "/src/utils",
"@use-api": "/src/axios/api",
"@use-images": "/src/assets/images",
"@mock": "/mock"
},
"alias-skip.allowedsuffix": ["ts", "tsx"],
"alias-skip.rootpath": "package.json",
"cSpell.words": [
"baomitu",
"bezier",
"Cascader",
"Clickoutside",
"codabar",
"commitmsg",
"crossorigin",
"datetimerange",
"domtoimage",
"EDITMSG",
"iife",
"internalkey",
"jsbarcode",
"linebreak",
"logicflow",
"macarons",
"menutag",
"ndata",
"persistedstate",
"pharmacode",
"Popselect",
"precommit",
"siderbar",
"snapline",
"stylelint",
"unocss",
"WUJIE",
"zlevel"
]
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,221 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<443547225@qq.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
# 贡献者公约
## 我们的承诺
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
## 我们的准则
有助于为我们的社区创造积极环境的行为例子包括但不限于:
* 表现出对他人的同情和善意
* 尊重不同的主张、观点和感受
* 提出和大方接受建设性意见
* 承担责任并向受我们错误影响的人道歉
* 注重社区共同诉求,而非个人得失
不当行为例子包括:
* 使用情色化的语言或图像,及性引诱或挑逗
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
* 公开或私下的骚扰行为
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
* 其他有理由认定为违反职业操守的不当行为
## 责任和权力
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论comment、提交commits、代码、维基wiki编辑、议题issues或其他贡献并在适当时机知采取措施的理由。
## 适用范围
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
## 监督
辱骂、骚扰或其他不可接受的行为可通过 <443547225@qq.com> 向负责监督的社区领袖报告。
所有投诉都将得到及时和公平的审查和调查。
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
## 处理方针
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
### 1. 纠正
**社区影响**:使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
**处理意见**:由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
### 2. 警告
**社区影响**:单个或一系列违规行为。
**处理意见**:警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
### 3. 临时封禁
**社区影响**: 严重违反社区准则,包括持续的不当行为。
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
### 4. 永久封禁
**社区影响**:行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
**处理意见**:永久禁止在社区内进行任何形式的公开互动。
## 参见
本行为准则改编自 [Contributor Covenant][homepage] 2.1 版, 参见 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]。
社区处理方针灵感来源于 [Mozilla's code of conduct enforcement ladder][Mozilla CoC]。
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。
其他语言翻译参见 [https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

21
COMMONPROBLEM.md Normal file
View 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) 方法,首选该方法作为国际化语言切换方法。

View File

@ -1,11 +0,0 @@
FROM debian:11
COPY . /app
WORKDIR /app
RUN apt-get update
RUN apt-get install -y wget curl make sudo unzip
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
RUN apt-get install -y nodejs
RUN npm i -g pnpm
RUN pnpm install
EXPOSE 9527
CMD [ "pnpm", "dev" ]

View File

@ -1,22 +0,0 @@
# commit 规范
## commit message 格式
在提交代码时,`commit message` 遵循以下格式:
- feat: 新功能`feature`
- fix: 修补 `bug`
- update: 更新代码
- docs: 文档documentation
- style: 格式(不影响代码运行的变动)
- refactor: 重构即不是新增功能也不是修改bug的代码变动
- test: 增加测试
- chore: 构建过程或辅助工具的变动
- revert: 撤销
- merge: 合并分支
- perf: 优化相关,比如提升性能、体验
- build: 构建
- plugin: 插件更新
- publish: 发布
当你需要定制化自己的`commit message`格式时,可以在`commitlint.config.cjs`文件中进行配置。

230
README.md
View File

@ -1,154 +1,198 @@
<div align="center"> <div align="center"> <a href="https://github.com/XiaoDaiGua-Ray/ray-template"> <img alt="Ray Template" width="200" height="200" src="https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:alist/ray/ray.svg?sign=ZklU9Bh5b6oKp1X0LOhGwkx4g5mW4wk_w9Jt5zlZ5EQ=:0"> </a> <br> <br>
<a href="https://github.com/XiaoDaiGua-Ray/ray-template">
<img <h1>Ray Template</h1>
alt="Ray Template"
width="200"
height="200"
src="https://avatars.githubusercontent.com/u/51957438?v=4"
/>
</a>
<br />
<br />
<a href="https://nodejs.org/en/about/previous-releases"><img src="https://img.shields.io/node/v/vite.svg" alt="node compatibility"></a>
<a href="https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/LICENSE"
><img
src="https://img.shields.io/github/license/XiaoDaiGua-Ray/ray-template"
alt="LICENSE"
/></a>
<a href="#badge"><img src="https://img.shields.io/github/languages/top/XiaoDaiGua-Ray/ray-template" alt="language"></a>
<a href="https://www.npmjs.com/package/ray-template"><img src="https://img.shields.io/npm/v/ray-template" alt="npm package"></a>
</div> </div>
<div align="center"> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
# Ray Template [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
English | [简体中文](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/README.zh-CN.md) <!-- ALL-CONTRIBUTORS-BADGE:END -->
A `completely free`, `efficient`, `feature complete` and based on vite5. x & ts(x) & pinia & vue3. x and other latest technology in the background template. ## 前言
</div> > 该项目模板采用 `vue3.x` `vite4.x` `pinia` `tsx` 进行开发。
> 使用 `naive ui` 作为组件库。
> 预设了最佳构建体验的配置与常用搬砖工具。意在提供一个简洁、快速上手的模板。
> 该模板不支持移动端设备。
## 🌻 Intro ## 感谢
`Ray Template` uses cutting-edge front-end technology, abandoning complexity and bloat, using modular design, decoupling data, methods and views, focusing on business development. Provide rich configuration and rich template `Hooks`, support personalized customization, to meet your project needs. > 感谢 [`yun`](https://me.yka.moe/) 对于本人的支持。
## ✨ Features ## 预览地址
- `New technology stack:` using ts(x), vite5. x, vue3. x, pinia and other front-end cutting-edge technology development - [点击预览](https://xiaodaigua-ray.github.io/ray-template/#/)
- `Theme:` configurable theme - [点击预览(加速地址)](https://ray-template.yunkuangao.com/#/)
- `Internationalization:` built-in perfect internationalization solution
- `Permissions:` built-in perfect dynamic routing permission generation solution
- `Components:` secondary encapsulation of multiple common components
- `Toolkit:` common tool function packaging
- `Cache:` arbitrary depth page caching
- `Modular design:` decoupling management data, methods, views, rest assured secondary development
- `Configurable:` support rich configuration items
- `Code style:` built-in prettier, eslint and other code style tools
- `Multi-terminal adaptation:` support pc, phone, pad
- `Documentation:` complete documentation
- `Mock data:` built-in Mock data solution
- `Axios request:` the plug-in design is used to encapsulate the axios library interceptor twice, which makes the interceptor more flexible
- `SVG:` built-in svg icon solution
- `Hooks:` based on the template characteristics of the encapsulated hooks to make it easier to use some functions of the template
- `TypeScript:` provide a complete type
- `Vitest:` built-in vitest test solution
## 👀 Preview ## 文档地址
- [Preview](https://xiaodaigua-ray.github.io/ray-template/#/) - [文档](https://xiaodaigua-ray.github.io/ray-template-doc/)
- [文档(加速地址)](https://ray-template.yunkuangao.com/ray-template-doc/)
## 📌 Documentation ## 更新日志
- [Documentation](https://xiaodaigua-ray.github.io/ray-template-doc/) - [日志](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md)
## 🔋 Change Log ## 常见问题
- [Change Log](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md) - [常见问题](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/COMMONPROBLEM.md)
## 🪴 Prepare ## 特性
- [Node](http://nodejs.org/) and [git](https://git-scm.com/) - project development environment - **最新技术栈**:使用 Vue3.x/vite4.x 等前端前沿技术开发
- [Vite](https://vitejs.dev/) - familiar with vite features - **TypeScript**:应用程序级 JavaScript 的语言
- [Vue3](https://v3.vuejs.org/) - familiar with Vue basic syntax - **主题**:可配置的主题
- [TypeScript](https://www.typescriptlang.org/) - familiar with TypeScript basic syntax - **国际化**:内置完善的国际化方案
- [ES6+](http://es6.ruanyifeng.com/) - familiar with es6 basic syntax - **Mock 数据**:内置 Mock 数据方案
- [Vue-Hooks-Plus](https://inhiblabcore.github.io/docs/hooks/) - familiar with vue-hooks-plus useRequest method basic use - **权限**:内置完善的动态路由权限生成方案
- [Vue-Router-Next](https://next.router.vuejs.org/) - familiar with vue-router4.x basic use - **组件**:二次封装了多个常用的组件
- [Naive-UI](https://www.naiveui.com) - naive ui basic use - **Axios 请求**:二次封装 axios 库
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs basic syntax
- [Pinia](https://pinia.vuejs.org/zh/introduction.html) - state manager pinia usage
- [TSX](https://github.com/vuejs/babel-plugin-jsx/blob/main/packages/babel-plugin-jsx/README-zh_CN.md) - tsx basic syntax
- [Vitest](https://cn.vitest.dev/guide/) - vitest basic use
## 📦 Setup ## 准备
### Get Project - [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性
- [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法
- [TypeScript](https://www.typescriptlang.org/) - 熟悉 TypeScript 基本语法
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router4.x 基本使用
- [Naive-UI](https://www.naiveui.com) - ui 基本使用
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
- [Pinia](https://pinia.vuejs.org/zh/introduction.html) - 状态管理器 pinia 使用
- [TSX](https://github.com/vuejs/babel-plugin-jsx/blob/main/packages/babel-plugin-jsx/README-zh_CN.md) - tsx 基本语法
## 未来
> 根据个人时间空余情况,会不定时对该模板进行更新和迭代。希望将该工具的功能不断补全(虽然现在已经是足够日常开发和使用),将该模板打造为一个更加健全的中后台模板。如果你有好的想法和建议,可以直接联系我或者直接提 `issues` 即可。
## 提示
> 项目默认启用严格模式 `eslint`,但是由于 `vite-plugin-eslint` 插件优先级最高,所以如果出现自动导入类型错误提示,请优先解决其他问题。
> 建议开启 `vscode` 保存自动修复功能。
## 项目安装
```sh ```sh
# github # github
git clone https://github.com/XiaoDaiGua-Ray/ray-template.git git clone https://github.com/XiaoDaiGua-Ray/ray-template.git
# 如果你的下载速度很慢,可以切换到下面的代理地址
git clone https://gh.yka.moe/https://github.com/XiaoDaiGua-Ray/ray-template.git
``` ```
### Pull dependencies ## 拉取依赖
```sh ```sh
pnpm i # yarn
```
### Test project yarn
```
```sh ```sh
# npm
pnpm test npm install
``` ```
### Startup project ## 启动项目
```sh ```sh
pnpm dev # yarn
```
### Build project yarn dev
```
```sh ```sh
pnpm build # npm
npm run dev
``` ```
### Preview project ## 项目打包
```sh ```sh
pnpm preview # yarn
```
### Report project yarn build
```
```sh ```sh
pnpm report # npm
npm run build
``` ```
### Development ## 预览项目
Just delete the files under `views/demo`, `router/modules/demo` to get a clean project template. ```sh
# yarn
## 🪴 Project Activities yarn preview
```
![Alt](https://repobeats.axiom.co/api/embed/fab6071297ab281913a42f07a2779b488cfd62b8.svg 'Repobeats analytics image') ```sh
# npm
### Contributors npm run preview
```
Thanks for all their contributions 🐝 ! ## 体积分析
<a href="https://github.com/XiaoDaiGua-Ray/ray-template/graphs/contributors"> ```sh
<img src="https://contrib.rocks/image?repo=XiaoDaiGua-Ray/ray-template" /> # yarn
</a>
## Browser Support yarn report
```
```sh
# npm
npm run report
```
## 浏览器支持
> 仅支持现代浏览器,不支持 `IE`
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 📄 License ## 最后,希望大家搬砖愉快
[MIT License](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/LICENSE) © 2022-PRESENT [Ray](https://github.com/XiaoDaiGua-Ray) ## 贡献者
<!-- 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!
## License
[MIT © Ray-2020](./LICENSE)

View File

@ -1,154 +0,0 @@
<div align="center">
<a href="https://github.com/XiaoDaiGua-Ray/ray-template">
<img
alt="Ray Template"
width="200"
height="200"
src="https://avatars.githubusercontent.com/u/51957438?v=4"
/>
</a>
<br />
<br />
<a href="https://nodejs.org/en/about/previous-releases"><img src="https://img.shields.io/node/v/vite.svg" alt="node compatibility"></a>
<a href="https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/LICENSE"
><img
src="https://img.shields.io/github/license/XiaoDaiGua-Ray/ray-template"
alt="LICENSE"
/></a>
<a href="#badge"><img src="https://img.shields.io/github/languages/top/XiaoDaiGua-Ray/ray-template" alt="language"></a>
<a href="https://www.npmjs.com/package/ray-template"><img src="https://img.shields.io/npm/v/ray-template" alt="npm package"></a>
</div>
<div align="center">
# Ray Template
简体中文 | [English](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/README.md)
一个 `完全免费``高效``特性完整` 并且基于 vite5.x & ts(x) & pinia & vue3.x 等最新技术的中后台模板。
</div>
## 🌻 简介
`Ray Template`采用前沿前端技术,摒弃繁杂与臃肿,采用模块化设计,解耦数据、方法和视图,专注业务开发。提供丰富配置和丰富的模板 `Hooks`,支持个性化定制,满足你的项目需求。
## ✨ 特性
- `全新技术栈:`使用 ts(x), vite5.x, vue3.x, pinia 等前端前沿技术开发
- `主题:`可配置的主题
- `国际化:`内置完善的国际化方案
- `权限:`内置完善的动态路由权限生成方案
- `组件:`二次封装了多个常用的组件
- `工具包:`常用的工具函数封装
- `缓存:`任意深度页面缓存
- `模块化设计:`解耦管理的数据、方法、视图,放心二次开发
- `配置化:`支持丰富的配置项
- `代码风格:`内置 prettier, eslint 等代码风格工具
- `多端适配:`支持 pc, phone, pad
- `文档:`完善的文档
- `Mock 数据:`内置 Mock 数据方案
- `Axios 请求:`采用插件式设计二次封装 axios 库拦截器,让拦截器更加灵活
- `SVG`内置 svg icon 解决方案
- `Hooks`基于模板特性封装的 hooks 让你更加方便的使用模板一些功能
- `TypeScript`提供完整的类型
- `Vitest`内置 vitest 测试方案
## 👀 预览地址
- [点击预览](https://xiaodaigua-ray.github.io/ray-template/#/)
## 📌 文档地址
- [文档](https://xiaodaigua-ray.github.io/ray-template-doc/)
## 🔋 更新日志
- [更新日志](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md)
## 🪴 准备
- [Node](http://nodejs.org/) 和 [git](https://git-scm.com/) - 项目开发环境
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性
- [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法
- [TypeScript](https://www.typescriptlang.org/) - 熟悉 TypeScript 基本语法
- [ES6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法
- [Vue-Hooks-Plus](https://inhiblabcore.github.io/docs/hooks/) - 熟悉 vue-hooks-plus useRequest 方法的基本使用
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router4.x 基本使用
- [Naive-UI](https://www.naiveui.com) - naive ui 基本使用
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
- [Pinia](https://pinia.vuejs.org/zh/introduction.html) - 状态管理器 pinia 使用
- [TSX](https://github.com/vuejs/babel-plugin-jsx/blob/main/packages/babel-plugin-jsx/README-zh_CN.md) - tsx 基本语法
- [Vitest](https://cn.vitest.dev/guide/) - vitest 基本使用
## 📦 起步
### 获取项目
```sh
# github
git clone https://github.com/XiaoDaiGua-Ray/ray-template.git
```
### 拉取依赖
```sh
pnpm i
```
### 测试项目
```sh
pnpm test
```
### 启动项目
```sh
pnpm dev
```
### 项目打包
```sh
pnpm build
```
### 预览项目
```sh
pnpm preview
```
### 体积分析
```sh
pnpm report
```
### 快速开发
只需要删除 `views/demo`, `router/modules/demo` 下的文件即可得到一个干净的项目模板。
## 🪴 项目活动
![Alt](https://repobeats.axiom.co/api/embed/fab6071297ab281913a42f07a2779b488cfd62b8.svg 'Repobeats analytics image')
### 贡献者
感谢他们的所做的一切贡献 🐝
<a href="https://github.com/XiaoDaiGua-Ray/ray-template/graphs/contributors">
<img src="https://contrib.rocks/image?repo=XiaoDaiGua-Ray/ray-template" />
</a>
## 浏览器支持
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 📄 证书
[MIT License](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/LICENSE) © 2022-PRESENT [Ray](https://github.com/XiaoDaiGua-Ray)

View File

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

View File

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

View File

@ -1,27 +0,0 @@
import { callWithAsyncErrorHandling } from '../../src/utils/basic'
describe('callWithAsyncErrorHandling', () => {
it('should call the function and return the result', () => {
const fn = (x: number) => x
const callbackFn = () => {}
expect(callWithAsyncErrorHandling(fn, callbackFn, [1])).resolves.toBe(1)
})
it('should call the callback function when the function throws an error', () => {
let callbackFnExecuted = 1
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}
callWithAsyncErrorHandling(fn, callbackFn)
expect(callbackFnExecuted).toBe(2)
})
})

View File

@ -1,27 +0,0 @@
import { callWithErrorHandling } from '../../src/utils/basic'
describe('callWithErrorHandling', () => {
it('should call the function and return the result', () => {
const fn = (x: number) => x
const callbackFn = () => {}
expect(callWithErrorHandling(fn, callbackFn, [1])).toBe(1)
})
it('should call the callback function when the function throws an error', () => {
let callbackFnExecuted = 1
const fn = () => {
throw new Error('test error')
}
const callbackFn = () => {
callbackFnExecuted = 2
}
callWithErrorHandling(fn, callbackFn)
expect(callbackFnExecuted).toBe(2)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,48 +0,0 @@
import { isValueType } from '../../src/utils/basic'
describe('isValueType', () => {
it('should return true for string', () => {
expect(isValueType<string>('string', 'String')).toBe(true)
})
it('should return true for number', () => {
expect(isValueType<number>(123, 'Number')).toBe(true)
})
it('should return true for array', () => {
expect(isValueType<unknown[]>([], 'Array')).toBe(true)
})
it('should return true for null', () => {
expect(isValueType<null>(null, 'Null')).toBe(true)
})
it('should return true for undefined', () => {
expect(isValueType<undefined>(void 0, 'Undefined')).toBe(true)
})
it('should return true for object', () => {
expect(isValueType<object>({}, 'Object')).toBe(true)
})
it('should return true for Map', () => {
expect(isValueType<Map<unknown, unknown>>(new Map(), 'Map')).toBe(true)
})
it('should return true for Set', () => {
expect(isValueType<Set<unknown>>(new Set(), 'Set')).toBe(true)
})
it('should return true for Date', () => {
expect(isValueType<Date>(new Date(), 'Date')).toBe(true)
})
it('should return true for RegExp', () => {
expect(isValueType<RegExp>(/a/i, 'RegExp')).toBe(true)
})
it('should return false for Function', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
expect(isValueType<Function>(/a/i, 'Function')).toBe(false)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +0,0 @@
import { mount } from '@vue/test-utils'
/**
*
* @param slots
*
* @description
* ref domRef
*
*
* @example
* const wrapper = createRefElement({ default: () => <div>hello</div> })
*
* const text = wrapper.find('div').text() // hello
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const createRefElement = (slots?: Record<string, Function>) => {
const wrapper = mount(
defineComponent({
setup() {
const domRef = ref<HTMLElement>()
return {
domRef,
}
},
render() {
return <div ref="domRef">{{ ...slots }}</div>
},
}),
)
return wrapper
}
export default createRefElement

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
import { renderNode } from '../../src/utils/vue/render-node'
import createRefElement from '../utils/createRefElement'
describe('renderNode', () => {
it('should render string', () => {
const wrapper = createRefElement({
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
default: renderNode('hello world') as Function,
})
const text = wrapper.text()
expect(text).toBe('hello world')
})
})

141
cfg.ts Normal file
View File

@ -0,0 +1,141 @@
/**
*
* @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, 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: 'http://localhost',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
/**
*
*
*/
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

View File

@ -1,40 +1,20 @@
// update: 更新代码 | Update code
// fix: 修复 bug | Fix bug
// feat: 新功能 | New feature
// chore: 构建过程或辅助工具的变动 | Build process or auxiliary tool changes
// docs: 文档 | Documentation
// refactor: 重构(即不是新增功能,也不是修改 bug 的代码变动) | Refactor (i.e. code changes that are neither new features nor bug fixes)
// test: 增加测试 | Add test
// style: 代码格式(不影响功能,例如空格、分号等格式修正) | Code format (no functional impact, such as space, semicolon, etc.)
// version: 更新迭代 package.json 版本号 | Update the package.json version number
// build: 构建 | Build
// plugin: 更新插件版本 | Update plugin version
module.exports = { module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/config-conventional'],
rules: { rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [ 'type-enum': [
2, 2,
'always', 'always',
[ [
'update', 'bug',
'fix',
'feat', 'feat',
'chore', 'fix',
'docs', 'docs',
'style',
'refactor', 'refactor',
'test', 'test',
'style', 'chore',
'version', 'revert',
'build', 'merge',
'plugin',
], ],
], ],
}, },

View File

@ -1,14 +0,0 @@
version: '3'
services:
ray-template:
build: .
container_name: ray-template
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
ports:
- "9527:9527"
# if you want to persist
# volumes:
# - ./app:/app

View File

@ -1,365 +0,0 @@
import vue from 'eslint-plugin-vue'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import prettier from 'eslint-plugin-prettier'
import globals from 'globals'
import parser from 'vue-eslint-parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
export default [
{
ignores: [
'**/node_modules/',
'**/dist/',
'dist/*',
'node_modules/*',
'**/auto-imports.d.ts',
'**/components.d.ts',
'**/.gitignore',
'**/.vscode',
'**/public',
'**/yarn.*',
'**/vite-env.*',
'**/.prettierrc.*',
'**/visualizer.*',
'**/visualizer.html',
'**/.env.*',
'src/locales/lang',
'**/.depcheckrc',
'src/app-config/echart-themes/**/*.json',
'**/*.md',
'src/icons/*.svg',
],
},
{
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.vue'],
},
...compat.extends(
'eslint-config-prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
'prettier',
'./unplugin/.eslintrc-auto-import.json',
),
{
plugins: {
vue,
'@typescript-eslint': typescriptEslint,
prettier,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
defineOptions: 'readonly',
defineModel: 'readonly',
},
parser: parser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
},
rules: {
'no-undefined': ['error'],
'linebreak-style': ['error', 'unix'],
'@typescript-eslint/no-explicit-any': [
'error',
{
ignoreRestArgs: true,
},
],
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
disallowTypeAnnotations: false,
},
],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'accessor-pairs': 2,
'constructor-super': 0,
'default-case': 2,
eqeqeq: [2, 'allow-null'],
'no-alert': 0,
'no-array-constructor': 2,
'no-bitwise': 0,
'no-caller': 1,
'no-catch-shadow': 2,
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-constant-condition': 2,
'no-dupe-keys': 2,
'no-dupe-args': 2,
'no-duplicate-case': 2,
'no-eval': 1,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': [
'error',
{
enforceForLogicalOperands: true,
},
],
'no-extra-parens': 0,
semi: [
'error',
'never',
{
beforeStatementContinuationChars: 'always',
},
],
'no-fallthrough': 1,
'no-func-assign': 2,
'no-implicit-coercion': [
'error',
{
allow: ['!!', '~'],
},
],
'no-implied-eval': 2,
'no-invalid-regexp': 2,
'no-invalid-this': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': 2,
'no-lone-blocks': 2,
'no-multi-spaces': 1,
'no-multiple-empty-lines': [
'error',
{
max: 2,
},
],
'no-new-func': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-sparse-arrays': 2,
'no-trailing-spaces': 1,
'no-unreachable': 2,
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
enforceForJSX: true,
},
],
'no-useless-call': 2,
'no-var': 'error',
'no-with': 2,
'use-isnan': 2,
'no-multi-assign': 2,
'prefer-arrow-callback': 2,
curly: ['error', 'all'],
'vue/multi-word-component-names': [
'error',
{
ignores: [],
},
],
'vue/no-use-v-if-with-v-for': [
'error',
{
allowUsingIterationVar: false,
},
],
'vue/require-v-for-key': ['error'],
'vue/require-valid-default-prop': ['error'],
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
'vue/v-on-event-hyphenation': ['error', 'never'],
'vue/component-tags-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'vue/no-v-html': ['error'],
'vue/no-v-text': ['error'],
'vue/component-api-style': [
'error',
['script-setup', 'composition', 'composition-vue2'],
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: false,
},
],
'vue/no-unused-refs': ['error'],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/attribute-hyphenation': [
'error',
'never',
{
ignore: [],
},
],
'vue/no-restricted-static-attribute': [
'error',
{
key: 'key',
message: 'Disallow using key as a custom attribute',
},
],
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='deprecated']",
message: 'Using deprecated API is not allowed.',
},
],
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: ['import'],
next: '*',
},
{
blankLine: 'any',
prev: 'import',
next: 'import',
},
{
blankLine: 'always',
prev: '*',
next: 'export',
},
{
blankLine: 'any',
prev: 'export',
next: 'export',
},
{
blankLine: 'always',
prev: ['const', 'let', 'var'],
next: '*',
},
{
blankLine: 'any',
prev: ['const', 'let', 'var'],
next: ['const', 'let', 'var'],
},
{
blankLine: 'always',
prev: 'directive',
next: '*',
},
{
blankLine: 'any',
prev: 'directive',
next: 'directive',
},
{
blankLine: 'always',
prev: '*',
next: [
'if',
'class',
'for',
'do',
'while',
'switch',
'try',
'with',
'function',
'block',
'block-like',
'break',
'case',
'continue',
'return',
'throw',
'debugger',
],
},
{
blankLine: 'always',
prev: [
'if',
'class',
'for',
'do',
'while',
'switch',
'try',
'with',
'function',
'block',
'block-like',
'break',
'case',
'continue',
'return',
'throw',
'debugger',
],
next: '*',
},
],
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'with-single-extends',
allowObjectTypes: 'always',
},
],
},
},
]

View File

@ -1,12 +1,9 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ray.svg" /> <link rel="icon" type="image/svg+xml" href="/ray.svg" />
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0" />
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Vite + Vue + TS</title> <title>Vite + Vue + TS</title>
</head> </head>
<style> <style>
@ -15,27 +12,6 @@
--preloading-title-color: <%= preloadingConfig.titleColor %>; --preloading-title-color: <%= preloadingConfig.titleColor %>;
--ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>; --ray-theme-primary-fade-color: <%= appPrimaryColor.primaryFadeColor %>;
--ray-theme-primary-color: <%= appPrimaryColor.primaryColor %>; --ray-theme-primary-color: <%= appPrimaryColor.primaryColor %>;
--global-loading-bg-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
#pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
}
@media (prefers-color-scheme: light) {
#pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
}
html.dark #pre-loading-animation {
background-color: var(--global-loading-bg-color);
}
html.light #pre-loading-animation {
background-color: var(--global-loading-bg-color);
} }
#pre-loading-animation { #pre-loading-animation {
@ -44,9 +20,13 @@
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
background-color: #ffffff;
color: var(--preloading-title-color); color: var(--preloading-title-color);
text-align: center; text-align: center;
background-color: var(--global-loading-bg-color); }
.ray-template--dark #pre-loading-animation {
background-color: #2a3146;
} }
#pre-loading-animation .pre-loading-animation__wrapper { #pre-loading-animation .pre-loading-animation__wrapper {
@ -61,9 +41,8 @@
#pre-loading-animation #pre-loading-animation
.pre-loading-animation__wrapper .pre-loading-animation__wrapper
.pre-loading-animation__wrapper-title { .pre-loading-animation__wrapper-title {
font-size: 32px; font-size: 30px;
padding-bottom: 48px; padding-bottom: 48px;
font-weight: 500;
} }
.pre-loading-animation__wrapper-loading { .pre-loading-animation__wrapper-loading {
@ -112,18 +91,6 @@
} }
} }
</style> </style>
<script>
;(function () {
const html = document.documentElement
const store = window.localStorage.getItem('piniaSettingStore')
const { _appTheme = false } = store ? JSON.parse(store) : {}
const loadingBgColor = _appTheme ? '#1c1e23' : '#ffffff'
html.classList.add(_appTheme ? 'dark' : 'light')
html.style.setProperty('--global-loading-bg-color', loadingBgColor)
html.style.setProperty('background-color', loadingBgColor)
})()
</script>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="pre-loading-animation"> <div id="pre-loading-animation">

View File

@ -1,17 +1,26 @@
import { defineMock } from 'vite-plugin-mock-dev-server' import { defineMock } from 'vite-plugin-mock-dev-server'
import { pagination, stringify, response, array } from '@mock/shared/utils'
import { tableMock } from '@mock/shared/database'
import Mock from 'mockjs' import Mock from 'mockjs'
import { pagination, stringify, response } from '../shared/utils'
import { array } from '../shared/database'
export const getPersonList = defineMock({ export const getPersonList = defineMock({
url: '/api/list', url: '/api/list',
method: 'GET', method: 'GET',
delay: 500, delay: 500,
response: (req, res) => { response: (req, res) => {
const person = () => ({
id: Mock.Random.guid(),
address: Mock.Random.county(true),
email: Mock.Random.email(),
name: Mock.Random.cname(),
age: Mock.Random.integer(18, 60),
createDate: Mock.Random.date(),
})
const { const {
query: { page, pageSize, email }, query: { page, pageSize, email },
} = req } = req
let list = array(100).map(() => tableMock()) let list = array(100).map(() => person())
let length = list.length let length = list.length
if (!page || !pageSize) { if (!page || !pageSize) {

View File

@ -1,19 +1,14 @@
import Mock from 'mockjs'
/** /**
* *
* @param option * @author Ray <https://github.com/XiaoDaiGua-Ray>
* *
* * @date 2023-08-11
*
* @workspace ray-template
*
* @remark
*/ */
export function tableMock(option?: object) {
return { export function array(length: number) {
...option, return new Array(length).fill(0)
id: Mock.Random.guid(),
address: Mock.Random.county(true),
email: Mock.Random.email(),
name: Mock.Random.cname(),
age: Mock.Random.integer(18, 60),
createDate: Mock.Random.date(),
}
} }

View File

@ -1,8 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-08-11
*
* @workspace ray-template
*
* @remark
*/
export function array(length: number) { /* eslint-disable @typescript-eslint/no-explicit-any */
return new Array(length).fill(0)
}
/** /**
* *

161
package.json Executable file → Normal file
View File

@ -1,113 +1,100 @@
{ {
"name": "ray-template", "name": "ray-template",
"private": false, "private": false,
"version": "5.2.2", "version": "4.1.7",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0", "node": ">=16.0.0",
"pnpm": ">=9.0.0" "pnpm": ">=8.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build --mode production",
"preview": "vite preview", "preview": "vite preview",
"test": "vue-tsc --noEmit && vite build --mode test",
"dev-build": "vue-tsc --noEmit && vite build --mode development", "dev-build": "vue-tsc --noEmit && vite build --mode development",
"report": "vite build --mode report", "report": "vue-tsc --noEmit && vite build --mode report",
"prepare": "husky install", "prepare": "husky install"
"test": "vitest",
"test:ui": "vitest --ui",
"lint": "vue-tsc --noEmit && eslint --fix && prettier --write \"**/*.{ts,tsx,json,.vue}\""
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,json}": [ "src/**/*.{vue,jsx,ts,tsx,json}": [
"prettier --write" "prettier --write",
], "eslint src",
"*.{ts,tsx,vue}": [ "git add"
"eslint --fix"
] ]
}, },
"dependencies": { "dependencies": {
"@logicflow/core": "2.0.10", "@vueuse/core": "^9.1.0",
"@logicflow/extension": "2.0.14", "axios": "^1.2.0",
"@vueuse/core": "^13.1.0",
"axios": "^1.9.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "4.2.0", "crypto-js": "^4.1.1",
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"dayjs": "^1.11.13", "dayjs": "^1.11.7",
"echarts": "^5.6.0", "echarts": "^5.4.0",
"html-to-image": "1.11.13",
"interactjs": "1.10.27",
"jsbarcode": "3.11.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mockjs": "1.1.0", "mockjs": "1.1.0",
"naive-ui": "^2.42.0", "naive-ui": "^2.34.4",
"pinia": "^3.0.3", "pinia": "^2.1.4",
"pinia-plugin-persistedstate": "^4.4.1", "pinia-plugin-persistedstate": "^3.1.0",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"vue": "^3.5.17", "qrcode.vue": "^3.3.4",
"vue-demi": "0.14.10", "sass": "^1.54.3",
"vue-hooks-plus": "2.4.0", "screenfull": "^6.0.2",
"vue-i18n": "^9.13.1", "vue": "^3.3.4",
"vue-router": "^4.5.1", "vue-hooks-plus": "1.7.6",
"vue3-next-qrcode": "3.0.2" "vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "19.7.1", "@babel/core": "^7.20.2",
"@commitlint/config-conventional": "19.7.1", "@babel/eslint-parser": "^7.19.1",
"@eslint/js": "9.28.0", "@commitlint/cli": "^17.4.2",
"@interactjs/types": "1.10.27", "@commitlint/config-conventional": "^17.4.2",
"@intlify/unplugin-vue-i18n": "4.0.0", "@intlify/unplugin-vue-i18n": "^0.12.1",
"@types/crypto-js": "4.2.2", "@types/crypto-js": "^4.1.1",
"@types/jsbarcode": "3.11.4", "@types/lodash-es": "^4.17.7",
"@types/lodash-es": "4.17.12", "@types/mockjs": "1.0.7",
"@types/mockjs": "1.0.10", "@types/scrollreveal": "^0.0.8",
"@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "8.24.0", "@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-vue": "5.2.3", "@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "4.1.2", "@vitejs/plugin-vue-jsx": "^3.0.1",
"@vitest/ui": "2.1.8", "@vue-hooks-plus/resolvers": "1.2.4",
"@vue/eslint-config-prettier": "10.1.0", "@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "14.2.0", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "2.4.6", "autoprefixer": "^10.4.8",
"autoprefixer": "10.4.21", "depcheck": "^1.4.3",
"depcheck": "1.4.7", "eslint": "^8.44.0",
"eslint": "9.20.1", "eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "10.1.2", "eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-prettier": "5.2.6", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-n": "^15.0.0",
"globals": "16.0.0", "eslint-plugin-prettier": "^4.2.1",
"happy-dom": "17.1.0", "eslint-plugin-promise": "^6.0.0",
"husky": "8.0.3", "eslint-plugin-react": "^7.31.10",
"lint-staged": "15.4.3", "eslint-plugin-vue": "^9.15.1",
"postcss": "8.5.4", "husky": "^8.0.3",
"postcss-px-to-viewport-8-with-include": "1.2.2", "lint-staged": "^13.1.0",
"prettier": "3.5.3", "postcss": "^8.1.0",
"rollup-plugin-gzip": "4.0.1", "postcss-px-to-viewport-8-plugin": "1.2.2",
"sass": "1.86.3", "prettier": "^2.7.1",
"svg-sprite-loader": "6.0.11", "rollup-plugin-visualizer": "^5.8.3",
"typescript": "5.8.3", "svg-sprite-loader": "^6.0.11",
"unocss": "66.3.3", "typescript": "^5.0.2",
"unplugin-auto-import": "19.1.2", "unplugin-auto-import": "^0.15.0",
"unplugin-vue-components": "0.28.0", "unplugin-vue-components": "^0.25.1",
"vite": "6.3.5", "vite": "^4.3.9",
"vite-bundle-analyzer": "0.16.0", "vite-plugin-compression": "^0.5.1",
"vite-plugin-cdn2": "1.1.0", "vite-plugin-ejs": "^1.6.4",
"vite-plugin-ejs": "1.7.0",
"vite-plugin-eslint": "1.8.1", "vite-plugin-eslint": "1.8.1",
"vite-plugin-inspect": "0.8.4", "vite-plugin-imp": "^2.3.1",
"vite-plugin-mock-dev-server": "1.8.3", "vite-plugin-inspect": "^0.7.26",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-mock-dev-server": "1.3.0",
"vite-svg-loader": "5.1.0", "vite-plugin-svg-icons": "^2.0.1",
"vitest": "2.1.8", "vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "9.4.3", "vue-tsc": "^1.8.4"
"vue-tsc": "2.2.8"
}, },
"description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->", "description": "<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->",
"main": "index.ts", "main": "index.ts",

10101
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,29 +1,26 @@
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import AppNaiveGlobalProvider from '@/app-components/provider/AppNaiveGlobalProvider' import AppNaiveGlobalProvider from '@/app-components/provider/AppNaiveGlobalProvider/index'
import AppStyleProvider from '@/app-components/provider/AppStyleProvider' import AppStyleProvider from '@/app-components/provider/AppStyleProvider/index'
import AppLockScreen from '@/app-components/app/AppLockScreen' import AppLockScreen from '@/app-components/app/AppLockScreen/index'
import AppWatermarkProvider from '@/app-components/provider/AppWatermarkProvider' import AppGlobalSpin from '@/spin/index'
import AppGlobalSpin from '@/app-components/app/AppGlobalSpin'
import AppVersionProvider from '@/app-components/provider/AppVersionProvider'
import { APP_GLOBAL_LOADING } from '@/app-config' const App = defineComponent({
export default defineComponent({
name: 'App', name: 'App',
render() { render() {
return ( return (
<AppNaiveGlobalProvider> <AppNaiveGlobalProvider>
<AppVersionProvider />
<AppLockScreen /> <AppLockScreen />
<AppStyleProvider /> <AppStyleProvider />
<AppWatermarkProvider />
<AppGlobalSpin> <AppGlobalSpin>
{{ {{
default: () => <RouterView />, default: () => <RouterView />,
description: () => APP_GLOBAL_LOADING, description: () => 'lodaing...',
}} }}
</AppGlobalSpin> </AppGlobalSpin>
</AppNaiveGlobalProvider> </AppNaiveGlobalProvider>
) )
}, },
}) })
export default App

View File

@ -1,14 +0,0 @@
# \_\_ray-template
该包用于管理一些模板的管理,例如:
- validAppRootPath: 检查模板 `appRootPath` 是否配置正确
- validLocal: 检查模板 `localConfig` 是否配置正确
## 拓展
当你需要在做一些定制化操作的时候,可以尝试在这个包里做一些事情。
最后在 `main.ts` 中导入并且调用即可。
> 出于一些考虑,并没有做自动化导入调用,所以需要自己手动来。(好吧,其实就是我懒--

View File

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

View File

@ -1,28 +0,0 @@
import { useSettingGetters } from '@/store'
import { useVueRouter } from '@/hooks'
/**
*
* @description
* appRootRoute.path
*
* : [src/store/modules/setting/index.ts] appRootRoute
*
* getRoutes
*/
export const validAppRootPath = async () => {
const { getAppRootRoute } = useSettingGetters()
const {
router: { getRoutes },
} = useVueRouter()
const find = getRoutes().find(
(curr) => curr.path === getAppRootRoute.value.path,
)
if (!find) {
throw new Error(
`[validAppRootPath]: 'store setting appRootRoute path: ' '${getAppRootRoute.value.path}' not found in router, please check the 'appRootRoute' setting in the store setting module.`,
)
}
}

View File

@ -1,99 +0,0 @@
import {
LOCAL_OPTIONS,
SYSTEM_DEFAULT_LOCAL,
SYSTEM_FALLBACK_LOCALE,
DAYJS_LOCAL_MAP,
DEFAULT_DAYJS_LOCAL,
} from '@/app-config'
/**
*
* @description
* LOCAL_OPTIONS key
*/
const getLocalOptionKeys = () => {
return LOCAL_OPTIONS.map((curr) => curr.key)
}
/**
*
* @description
* SYSTEM_DEFAULT_LOCAL LOCAL_OPTIONS
*/
const validSystemDefaultLocal = () => {
const localOptionKeys = getLocalOptionKeys()
if (!localOptionKeys.includes(SYSTEM_DEFAULT_LOCAL)) {
throw new Error(
`[validLocal validSystemDefaultLocal:] SYSTEM_DEFAULT_LOCAL: '${SYSTEM_DEFAULT_LOCAL}' is not in LOCAL_OPTIONS: [${localOptionKeys.join(
', ',
)}]`,
)
}
}
/**
*
* @description
* SYSTEM_FALLBACK_LOCALE LOCAL_OPTIONS
*/
const validSystemFallbackLocale = () => {
const localOptionKeys = getLocalOptionKeys()
if (!localOptionKeys.includes(SYSTEM_FALLBACK_LOCALE)) {
throw new Error(
`[validLocal validSystemFallbackLocale:] SYSTEM_FALLBACK_LOCALE: '${SYSTEM_FALLBACK_LOCALE}' is not in LOCAL_OPTIONS: [${localOptionKeys.join(
', ',
)}]`,
)
}
}
/**
*
* @description
* DAYJS_LOCAL_MAP LOCAL_OPTIONS
*/
const validDayjsLocalMap = () => {
const localOptionKeys = getLocalOptionKeys() as string[]
const dayjsLocalKeys = Object.keys(DAYJS_LOCAL_MAP)
dayjsLocalKeys.forEach((key) => {
if (!localOptionKeys.includes(key)) {
throw new Error(
`[validLocal validDayjsLocalMap:] DAYJS_LOCAL_MAP: '${key}' is not in LOCAL_OPTIONS: [${localOptionKeys.join(
', ',
)}]`,
)
}
})
}
/**
*
* @description
* DEFAULT_DAYJS_LOCAL DAYJS_LOCAL_MAP
*/
const validDefaultDayjsLocal = () => {
const dayjsLocalKeys = Object.values(DAYJS_LOCAL_MAP)
if (!dayjsLocalKeys.includes(DEFAULT_DAYJS_LOCAL)) {
throw new Error(
`[validLocal validDefaultDayjsLocal:] DEFAULT_DAYJS_LOCAL: '${DEFAULT_DAYJS_LOCAL}' is not in DAYJS_LOCAL_MAP: [${dayjsLocalKeys.join(
', ',
)}]`,
)
}
}
/**
*
* @description
* localConfig
*/
export const validLocal = async () => {
validSystemDefaultLocal()
validSystemFallbackLocale()
validDayjsLocalMap()
validDefaultDayjsLocal()
}

View File

@ -3,4 +3,4 @@
该包存放与模板深度绑定的组件: 该包存放与模板深度绑定的组件:
- app存放与模板数据绑定的组件 - app存放与模板数据绑定的组件
- provider:存放模板注入类组件 - sys:存放模板注入类组件

View File

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

View File

@ -1,14 +1,24 @@
import { useStorage } from '@vueuse/core' /**
import { APP_CATCH_KEY } from '@/app-config' *
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
const appLockScreen = useStorage( /**
APP_CATCH_KEY.isAppLockScreen, *
false, *
window.localStorage, *
{ * ,
mergeDefaults: true, */
},
) const appLockScreen = useStorage('isAppLockScreen', false, sessionStorage, {
mergeDefaults: true,
})
const useAppLockScreen = () => { const useAppLockScreen = () => {
const setLockAppScreen = (bool: boolean) => { const setLockAppScreen = (bool: boolean) => {

View File

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

View File

@ -1,37 +1,45 @@
import '../../index.scss' /**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-20
*
* @workspace ray-template
*
* @remark
*/
import { NInput, NFormItem, NButton, NFlex } from 'naive-ui' /** 解锁界面 */
import AppAvatar from '@/app-components/app/AppAvatar'
import { RForm } from '@/components' import { NInput, NForm, NFormItem, NButton, NSpace } from 'naive-ui'
import AppAvatar from '@/app-components/app/AppAvatar/index'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useSigningActions, useSettingActions } from '@/store' import { useSetting, useSignin } from '@/store'
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared' import { rules, useCondition } from '@/app-components/app/AppLockScreen/hook'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useDevice } from '@/hooks'
import { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { removeStorage, decrypt, getStorage } from '@/utils'
export default defineComponent({ import type { FormInst, InputInst } from 'naive-ui'
const UnlockScreen = defineComponent({
name: 'UnlockScreen', name: 'UnlockScreen',
setup() { setup() {
const [register, { validate }] = useForm() const formRef = ref<FormInst | null>(null)
const inputInstRef = ref<InputInst | null>(null)
const { logout } = useSigningActions() const { logout } = useSignin()
const { updateSettingState } = useSettingActions() const { changeSwitcher } = useSetting()
const { setLockAppScreen } = useAppLockScreen() const { setLockAppScreen } = useAppLockScreen()
const { isTabletOrSmaller } = useDevice()
const HH_MM_FORMAT = 'HH:mm' const HH_MM_FORMAT = 'HH:mm'
const AM_PM_FORMAT = 'A' const AM_PM_FORMAT = 'A'
const YY_MM_DD_FORMAT = 'YYYY-MM-DD' const YY_MM_DD_FORMAT = 'YY年MM月DD日'
const DDD_FORMAT = 'ddd' const DDD_FORMAT = 'ddd'
const state = reactive({ const state = reactive({
lockCondition: useCondition(), lockCondition: useCondition(),
HH_MM: dayjs().format(HH_MM_FORMAT), HH_MM: dayjs().format(HH_MM_FORMAT),
AM_PM: dayjs().format(AM_PM_FORMAT), AM_PM: dayjs().locale('en').format(AM_PM_FORMAT),
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT), YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
DDD: dayjs().format(DDD_FORMAT), DDD: dayjs().format(DDD_FORMAT),
}) })
@ -44,55 +52,30 @@ export default defineComponent({
state.DDD = dayjs().format(DDD_FORMAT) state.DDD = dayjs().format(DDD_FORMAT)
}, 86_400_000) }, 86_400_000)
const toSigningFn = () => { /** 退出登陆并且回到登陆页 */
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage') const backToSignin = () => {
updateSettingState('lockScreenSwitch', false)
setTimeout(() => {
logout()
}, 100)
}
const backToSigning = () => {
window.$dialog.warning({ window.$dialog.warning({
title: '警告', title: '警告',
content: '是否返回到登陆页并且重新登录', content: '是否返回到登陆页?',
positiveText: '确定', positiveText: '确定',
negativeText: '重新登录', negativeText: '取消',
onPositiveClick: toSigningFn, onPositiveClick: () => {
logout()
setTimeout(() => {
changeSwitcher(false, 'lockScreenSwitch')
})
},
}) })
} }
/** 解锁 */
const unlockScreen = () => { const unlockScreen = () => {
const catchPassword = getStorage<string>( formRef.value?.validate((error) => {
APP_CATCH_KEY.appLockScreenPasswordKey, if (!error) {
'localStorage',
)
if (!catchPassword) {
window.$dialog.warning({
title: '警告',
content: () => '检测到锁屏密码被修改,请重新登录',
closable: false,
maskClosable: false,
closeOnEsc: false,
positiveText: '重新登录',
onPositiveClick: toSigningFn,
})
return
}
const dCatchPassword = decrypt(catchPassword)
validate().then(() => {
if (dCatchPassword === state.lockCondition.lockPassword) {
setLockAppScreen(false) setLockAppScreen(false)
updateSettingState('lockScreenSwitch', false) changeSwitcher(false, 'lockScreenSwitch')
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
state.lockCondition = useCondition() state.lockCondition = useCondition()
} else {
window.$message.warning('密码错误,请重新输入')
} }
}) })
} }
@ -104,86 +87,60 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
backToSigning, backToSignin,
unlockScreen, unlockScreen,
isTabletOrSmaller, formRef,
register, inputInstRef,
} }
}, },
render() { render() {
const { isTabletOrSmaller } = this
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
const hmSplit = HH_MM.split(':')
const { unlockScreen, backToSigning, register } = this
return ( return (
<div class="app-lock-screen__content app-lock-screen__content--full"> <div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock"> <div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content"> <div class="app-lock-screen__unlock__content-bg">
<div class="app-lock-screen__unlock__content-wrapper"> <div class="left">{this.HH_MM?.split(':')[0]}</div>
<div <div class="right">{this.HH_MM?.split(':')[1]}</div>
class={[ </div>
'app-lock-screen__unlock__content-bg__wrapper', <div class="app-lock-screen__unlock__content-avatar">
'app-lock-screen__unlock__content-bg', <AppAvatar vertical align="center" avatarSize={52} />
isTabletOrSmaller </div>
? 'app-lock-screen__unlock__content-bg--smaller' <div class="app-lock-screen__unlock__content-input">
: '', <NForm ref="formRef" model={this.lockCondition} rules={rules}>
]} <NFormItem path="lockPassword">
> <NInput
<div class="left">{hmSplit[0]}</div> ref="inputInstRef"
<div class="right">{hmSplit[1]}</div> v-model:value={this.lockCondition.lockPassword}
</div> 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}&nbsp;<span>{this.AM_PM}</span>
</div> </div>
<div class="app-lock-screen__unlock__content-avatar"> <div class="current-year">
<AppAvatar {this.YY_MM_DD}&nbsp;<span>{this.DDD}</span>
avatarSize={52}
style="pointer-events: none;"
vertical
/>
</div>
<div class="app-lock-screen__unlock__content-input">
<RForm
onRegister={register}
model={this.lockCondition}
rules={rules}
>
<NFormItem path="lockPassword">
<NInput
autofocus
v-model:value={this.lockCondition.lockPassword}
type="password"
placeholder="请输入解锁密码"
clearable
minlength={6}
onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') {
unlockScreen()
}
}}
/>
</NFormItem>
<NFlex justify="space-between">
<NButton
type="primary"
text
onClick={backToSigning.bind(this)}
>
</NButton>
<NButton
type="primary"
text
onClick={unlockScreen.bind(this)}
>
</NButton>
</NFlex>
</RForm>
</div>
<div class="app-lock-screen__unlock__content-date">
<div class="current-year">
{YY_MM_DD}&nbsp;<span>{DDD}</span>&nbsp;<span>{AM_PM}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -191,3 +148,5 @@ export default defineComponent({
) )
}, },
}) })
export default UnlockScreen

View File

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

View File

@ -1,16 +1,10 @@
.app-lock-screen__content { .app-lock-screen__content {
&.app-lock-screen__content--full {
width: 100%;
height: var(--html-height);
@include flexCenter;
}
& .app-lock-screen__input { & .app-lock-screen__input {
& button[class*='n-button'] { & button[class*="n-button"] {
width: 100%; width: 100%;
} }
& form[class*='n-form'] { & form[class*="n-form"] {
margin: 24px 0px; margin: 24px 0px;
} }
} }
@ -18,54 +12,35 @@
& .app-lock-screen__unlock { & .app-lock-screen__unlock {
.app-lock-screen__unlock__content { .app-lock-screen__unlock__content {
position: relative; position: relative;
width: 100%;
height: 100%;
@include flexCenter; @include flexCenter;
flex-direction: column; flex-direction: column;
& .app-lock-screen__unlock__content-wrapper { & .app-lock-screen__unlock__content-bg {
position: fixed; position: absolute;
inset: 0px; width: 100%;
height: 100%;
@include flexCenter;
font-size: 220px;
gap: 80px;
z-index: 0;
& .app-lock-screen__unlock__content-bg__wrapper { & .left,
width: 100%; & .right {
height: 100%;
background-color: rgb(16, 16, 20);
}
& .app-lock-screen__unlock__content-bg {
position: absolute;
width: 100%;
height: 100%;
@include flexCenter; @include flexCenter;
font-size: 16.67rem; border-radius: 30px;
gap: 80px; background-color: #141313;
z-index: 0; font-weight: 700;
padding: 80px;
&.app-lock-screen__unlock__content-bg--smaller { filter: blur(4px);
& .left,
& .right {
padding: 0px;
font-size: 90px;
padding: 24px;
border-radius: 4px;
}
}
& .left,
& .right {
@include flexCenter;
border-radius: 30px;
background-color: #141313;
font-weight: 700;
padding: 80px;
filter: blur(4px);
}
} }
} }
& .app-lock-screen__unlock__content-avatar { & .app-lock-screen__unlock__content-avatar {
margin-top: 5px; margin-top: 5px;
color: #bababa; color: #bababa;
font-weight: bolder; font-weight: 500;
z-index: 1; z-index: 1;
} }
@ -85,23 +60,9 @@
& .current-year, & .current-year,
& .current-date span { & .current-date span {
font-size: 1.875rem; font-size: 1.5rem;
line-height: 2.25rem;
} }
} }
} }
} }
} }
.ray-template--light {
.app-lock-screen__unlock__content-bg__wrapper {
background-color: #fff !important;
}
.app-lock-screen__unlock__content-bg {
& .left,
& .right {
background-color: rgba(244, 244, 245, 1) !important;
}
}
}

View File

@ -1,38 +1,57 @@
import { RModal } from '@/components' /**
import LockScreen from './components/LockScreen' *
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-05-13
*
* @workspace ray-template
*
* @remark
*/
import { useSettingGetters, useSettingActions } from '@/store' /**
*
* ,
*
*/
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 '@/app-components/app/AppLockScreen/appLockVar'
const AppLockScreen = defineComponent({ const AppLockScreen = defineComponent({
name: 'AppLockScreen', name: 'AppLockScreen',
setup() { setup() {
const { updateSettingState } = useSettingActions() const settingStore = useSetting()
const { getLockScreenSwitch } = useSettingGetters() const { lockScreenSwitch } = storeToRefs(settingStore)
const lockScreenSwitchRef = computed({
get: () => getLockScreenSwitch.value, const { getLockAppScreen } = useAppLockScreen()
set: (val) => {
updateSettingState('lockScreenSwitch', val)
},
})
return { return {
lockScreenSwitchRef, lockScreenSwitch,
getLockAppScreen,
} }
}, },
render() { render() {
return ( return (
<RModal <NModal
v-model:show={this.lockScreenSwitchRef} v-model:show={this.lockScreenSwitch}
transformOrigin="center" transformOrigin="center"
show show
autoFocus={false}
maskClosable={false} maskClosable={false}
closeOnEsc={false} closeOnEsc={false}
preset="dialog" preset={!this.getLockAppScreen() ? 'dialog' : undefined}
title="锁定屏幕" title="锁定屏幕"
> >
<LockScreen /> <div class="app-lock-screen__content">
</RModal> {!this.getLockAppScreen() ? <LockScreen /> : <UnlockScreen />}
</div>
</NModal>
) )
}, },
}) })

View File

@ -1,4 +1,4 @@
import { NAvatar, NTooltip, NFlex } from 'naive-ui' import { NAvatar, NTooltip, NSpace } from 'naive-ui'
interface AvatarOptions { interface AvatarOptions {
key: string key: string
@ -7,33 +7,45 @@ interface AvatarOptions {
icon: string icon: string
} }
export default defineComponent({ const RayLink = defineComponent({
name: 'AppShareLink', name: 'RayLink',
setup() { setup() {
const avatarOptions: AvatarOptions[] = [ 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', key: 'ray-js-note',
src: 'https://note.youdao.com/s/ObWEe2BB', src: 'https://note.youdao.com/s/ObWEe2BB',
tooltip: 'Ray的前端学习笔记', tooltip: 'Ray的前端学习笔记',
icon: 'https://avatars.githubusercontent.com/u/51957438?v=4', icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
}, },
{ {
key: 'ray-js-cover', key: 'ray-js-cover',
src: 'https://note.youdao.com/s/IC8xKPdB', src: 'https://note.youdao.com/s/IC8xKPdB',
tooltip: 'Ray的面试题总结', tooltip: 'Ray的面试题总结',
icon: 'https://avatars.githubusercontent.com/u/51957438?v=4', icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
}, },
{ {
key: 'ray-template-doc', key: 'ray-template-doc',
src: 'https://xiaodaigua-ray.github.io/ray-template-doc/', src: 'https://xiaodaigua-ray.github.io/ray-template-doc/',
tooltip: 'Ray Template Doc', tooltip: 'Ray Template Doc',
icon: 'https://avatars.githubusercontent.com/u/51957438?v=4', icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
}, },
{ {
key: 'ray-template-doc-out', key: 'ray-template-doc-out',
src: 'https://ray-template.yunkuangao.com/', src: 'https://ray-template.yunkuangao.com/',
tooltip: 'Ray Template Doc (国内地址)', tooltip: 'Ray Template Doc (国内地址)',
icon: 'https://avatars.githubusercontent.com/u/51957438?v=4', icon: 'https://usc1.contabostorage.com/c2e495d7890844d392e8ec0c6e5d77eb:image/longmao.jpeg',
}, },
] ]
@ -48,9 +60,9 @@ export default defineComponent({
}, },
render() { render() {
return ( return (
<NFlex> <NSpace>
{this.avatarOptions.map((curr) => ( {this.avatarOptions.map((curr) => (
<NTooltip key={curr.key}> <NTooltip>
{{ {{
trigger: () => ( trigger: () => (
<NAvatar <NAvatar
@ -68,7 +80,16 @@ export default defineComponent({
}} }}
</NTooltip> </NTooltip>
))} ))}
</NFlex> </NSpace>
) )
}, },
}) })
export default RayLink
/**
*
*
*
* ,
*/

View File

@ -1,3 +1,14 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-14
*
* @workspace ray-template
*
* @remark
*/
/** /**
* *
* naive ui * naive ui
@ -15,82 +26,63 @@ import {
createDiscreteApi, createDiscreteApi,
darkTheme, darkTheme,
NGlobalStyle, NGlobalStyle,
NModalProvider,
} from 'naive-ui' } from 'naive-ui'
import { getNaiveLocales } from '@/locales/utils' import { useSetting } from '@/store'
import { useSettingGetters } from '@/store' import { naiveLocales } from '@/locales/helper'
import { MESSAGE_PROVIDER } from '@/app-config'
export default defineComponent({ const GlobalProvider = defineComponent({
name: 'GlobalProvider', name: 'GlobalProvider',
setup(_, { expose }) { setup() {
const { getPrimaryColorOverride, getAppTheme, getLocaleLanguage } = const settingStore = useSetting()
useSettingGetters()
const modelPrimaryColorOverride = computed(
() => settingStore.primaryColorOverride,
)
const modelThemeValue = computed(() =>
settingStore.themeValue ? darkTheme : null,
)
const localePackage = computed(() => { const localePackage = computed(() => {
const key = getLocaleLanguage.value const key = settingStore.localeLanguage
return getNaiveLocales(key) return naiveLocales(key)
}) })
/** const { message, notification, dialog, loadingBar } = createDiscreteApi(
* ['message', 'dialog', 'notification', 'loadingBar'],
* 使 createDiscreteApi api {
* window.$messagewindow.$notificationwindow.$dialogwindow.$loadingBar 访 configProviderProps: computed(() => ({
* 使使 window.$notification placement theme: modelThemeValue.value,
* })),
* `slot default invoked outside of render` },
*/ )
const discreteApi = () => {
const { message, notification, dialog, loadingBar } = createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{
configProviderProps: computed(() => ({
theme: getAppTheme.value ? darkTheme : null,
})),
},
)
window.$dialog = dialog // 注入 `dialog` window.$dialog = dialog // 注入 `dialog`
window.$message = message // 注入 `message` window.$message = message // 注入 `message`
window.$loadingBar = loadingBar // 注入 `loadingBar` window.$loadingBar = loadingBar // 注入 `loadingBar`
window.$notification = notification // 注入 `notification` window.$notification = notification // 注入 `notification`
}
expose()
return { return {
getPrimaryColorOverride, modelPrimaryColorOverride,
modelThemeValue,
localePackage, localePackage,
getAppTheme,
discreteApi,
} }
}, },
render() { render() {
const {
$slots: { default: slotDefault },
discreteApi,
} = this
const { getPrimaryColorOverride, getAppTheme, localePackage } = this
return ( return (
<NConfigProvider <NConfigProvider
themeOverrides={getPrimaryColorOverride} themeOverrides={this.modelPrimaryColorOverride}
theme={getAppTheme ? darkTheme : null} theme={this.modelThemeValue}
locale={localePackage.locale} locale={this.localePackage.locale}
dateLocale={localePackage.dateLocal} dateLocale={this.localePackage.dateLocal}
> >
<NLoadingBarProvider> <NLoadingBarProvider>
<NMessageProvider {...MESSAGE_PROVIDER}> <NMessageProvider>
<NDialogProvider> <NDialogProvider>
<NModalProvider> <NNotificationProvider>
<NNotificationProvider> <NGlobalStyle />
<NGlobalStyle /> {this.$slots.default?.()}
{slotDefault?.()} </NNotificationProvider>
{discreteApi()}
</NNotificationProvider>
</NModalProvider>
</NDialogProvider> </NDialogProvider>
</NMessageProvider> </NMessageProvider>
</NLoadingBarProvider> </NLoadingBarProvider>
@ -98,3 +90,5 @@ export default defineComponent({
) )
}, },
}) })
export default GlobalProvider

View File

@ -0,0 +1,40 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-07-21
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
*
*
* beforeRouteUpdate -> cancelAllRequest -> routeUpdate
*/
import { axiosCanceler } from '@/axios/helper/interceptor'
const AppRequestCanceler = defineComponent({
name: 'AppRequestCanceler',
setup() {
onBeforeRouteUpdate(() => {
axiosCanceler.cancelAllRequest()
})
},
render() {
return (
<div
style={{
display: 'none',
}}
></div>
)
},
})
export default AppRequestCanceler

View File

@ -1,25 +0,0 @@
/**
*
*
*
*
* beforeRouteUpdate -> cancelAllRequest -> routeUpdate
*/
import { axiosCanceler } from '@/axios/utils/interceptor'
const AppRequestCancelerProvider = defineComponent({
name: 'AppRequestCancelerProvider',
setup(_, { expose }) {
onBeforeRouteUpdate(() => {
axiosCanceler.cancelAllRequest()
})
expose()
},
render() {
return <div class="app-style-provider"></div>
},
})
export default AppRequestCancelerProvider

View File

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

View File

@ -1,67 +0,0 @@
/**
*
*
* 退
*/
import { RModal } from '@/components'
import { getStorage, setStorage } from '@/utils'
import { useSigningActions } from '@/store'
import { APP_CATCH_KEY } from '@/app-config'
export default defineComponent({
name: 'AppVersionProvider',
setup() {
const {
pkg: { version },
} = __APP_CFG__
const cacheVersion = getStorage<string>(
APP_CATCH_KEY.appVersionProvider,
'localStorage',
)
const modalShow = ref(false)
const { logout } = useSigningActions()
// 如果获取缓存版本号为 null则说明是第一次访问直接缓存版本号
if (cacheVersion !== null) {
if (version !== cacheVersion) {
modalShow.value = true
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
}
} else {
setStorage(APP_CATCH_KEY.appVersionProvider, version, 'localStorage')
}
return {
modalShow,
logout,
}
},
render() {
const { logout } = this
return (
<div class="app-style-provider">
<RModal
v-model:show={this.modalShow}
closeOnEsc={false}
maskClosable={false}
preset="dialog"
closable={false}
title="发现新版本"
content="当前版本已更新,点击确认加载新版本~"
zIndex={999999999}
draggable
positiveText="确认"
negativeText="取消"
onPositiveClick={logout}
onNegativeClick={() => {
this.modalShow = false
}}
/>
</div>
)
},
})

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