Compare commits

..

No commits in common. "main" and "v3.3.2" have entirely different histories.
main ... v3.3.2

646 changed files with 10035 additions and 42864 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 = '/'
# office 服务代理地址

View File

@ -1,12 +1,15 @@
dist/*
node_modules/*
dist
node_modules
auto-imports.d.ts
components.d.ts
.gitignore
.vscode
public
yarn.*
vite-env.*
.prettierrc.*
.eslintrc
visualizer.*
visualizer.html
.env.*
*-lock.yaml
src/locales/lang

170
.eslintrc.cjs Normal file
View File

@ -0,0 +1,170 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: [
'eslint-config-prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
plugins: ['vue', '@typescript-eslint', 'prettier'],
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
rules: {
'@typescript-eslint/no-explicit-any': [
'error',
{
ignoreRestArgs: true,
},
],
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false },
], // 强制导入类型显示标注 `import type xxx from 'xxx'`
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'accessor-pairs': 2, // 强制同时存在 `get` 与 `set`
'constructor-super': 0, // 强制子类构造函数中使用 `super` 调用父类的构造函数
curly: [2, 'all'], // `if`、`else` 强制使用 `{}` 包裹
'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': 2, // 禁止不必要的 `bool` 转换
'no-extra-parens': 0, // 禁止非必要的括号
semi: ['error', 'never', { beforeStatementContinuationChars: 'always' }],
'no-fallthrough': 1, // 禁止 `switch` 穿透
'no-func-assign': 2, // 禁止重复的函数声明
'no-implicit-coercion': 1, // 禁止隐式转换
'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, // 强制使用箭头函数作为回调
'vue/multi-word-component-names': [
'error',
{
ignores: [],
},
],
'vue/no-use-v-if-with-v-for': [
'error',
{
allowUsingIterationVar: false,
},
],
'vue/require-v-for-key': ['error'],
'vue/require-valid-default-prop': ['error'],
'no-use-before-define': [
'error',
{
functions: true,
classes: true,
variables: false,
allowNamedExports: false,
},
],
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
'vue/v-on-event-hyphenation': ['error', 'never'],
'vue/component-tags-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'vue/no-v-html': ['error'],
'vue/no-v-text': ['error'],
'vue/component-api-style': [
'error',
['script-setup', 'composition', 'composition-vue2'],
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
globals: ['RouterView'],
},
],
'vue/no-unused-refs': ['error'],
},
}

2
.evnrc
View File

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

14
.gitattributes vendored
View File

@ -1,14 +0,0 @@
# 将换行符设置为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: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

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

@ -1,47 +0,0 @@
on:
- push
- pull_request
jobs:
cache-and-install:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version: [22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [true]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Pnpm build
run: pnpm build

5
.gitignore vendored
View File

@ -14,8 +14,8 @@ dist
dist/
*.local
visualizer.*
.eslintcache
.history
components.d.ts
auto-imports.d.ts
# Editor directories and files
.idea
@ -27,4 +27,3 @@ visualizer.*
*.sw?
yarn-*.*
yarn.*
pnpm.*

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
yarn 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
. "$(dirname -- "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
. "$(dirname "$0")/_/husky.sh"
[ -n "$CI" ] && exit 0
pnpm lint-staged --allow-empty "$1"
yarn lint-staged --allow-empty "$1"

View File

@ -1,7 +0,0 @@
node_modules/*
dist/*
visualizer.html
.idea
auto-imports.d.ts
components.d.ts
visualizer.html

4
.npmrc
View File

@ -1,4 +0,0 @@
package-lock=true
prefer-offline=true
save-exact=true
engine-strict=true

1
.nvmrc
View File

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

View File

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

45
.vscode/settings.json vendored
View File

@ -1,52 +1,11 @@
{
"editor.formatOnSave": true,
"i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"typescript.tsdk": "node_modules/typescript/lib",
"alias-skip.mappings": {
"@": "/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"
]
"i18n-ally.enabledFrameworks": ["vue", "react"]
}

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" ]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020-present, Ray
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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`文件中进行配置。

274
README.md
View File

@ -1,154 +1,230 @@
<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>
# `Ray Template`
<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>
> 感谢 <https://me.yka.moe/> 对于本人的支持。
## 🌻 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.
- [点击预览](https://xiaodaigua-ray.github.io/ray-template/#/)
- [点击预览(加速地址)](https://ray-template.yunkuangao.com/#/)
## ✨ Features
## 文档地址
- `New technology stack:` using ts(x), vite5. x, vue3. x, pinia and other front-end cutting-edge technology development
- `Theme:` configurable theme
- `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
- [文档](https://xiaodaigua-ray.github.io/ray-template-doc/)
- [文档(加速地址)](https://ray-template.yunkuangao.com/ray-template-doc/)
## 👀 Preview
## 更新日志
- [Preview](https://xiaodaigua-ray.github.io/ray-template/#/)
- [日志](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md)
## 📌 Documentation
## 常见问题
- [Documentation](https://xiaodaigua-ray.github.io/ray-template-doc/)
- [常见问题](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/COMMONPROBLEM.md)
## 🔋 Change Log
## 功能
- [Change Log](https://github.com/XiaoDaiGua-Ray/xiaodaigua-ray.github.io/blob/main/CHANGELOG.md)
- 主题切换
- 任意深度页面缓存
- 系统配置化
- 锁屏
- 自动化路由
- 带有拓展功能的表格
- 封装 `axios` 自动取消重复请求
- 动态菜单(多级菜单)
- 主题色切换
- 错误页
- 面包屑
- 标签页
- 国际化(允许按模块管理语言包)
- 权限路由
- 动态切换主题、贴花的 `EChart`
- 最佳构建体验
- 体积分析
- 还有一些不值一提的小东西...
## 🪴 Prepare
## 未来
- [Node](http://nodejs.org/) and [git](https://git-scm.com/) - project development environment
- [Vite](https://vitejs.dev/) - familiar with vite features
- [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
- [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
- [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
> 根据个人时间空余情况,会不定时对该模板进行更新和迭代。希望将该工具的功能不断补全(虽然现在已经是足够日常开发和使用),将该模板打造为一个更加健全的中后台模板。如果你有好的想法和建议,可以直接联系我或者直接提 `issues` 即可。
## 📦 Setup
## 前言
### Get Project
> 该项目模板采用 `vue3.x` `vite4.0` `pinia` `tsx` 进行开发。
> 使用 `naive ui` 作为组件库。
> 预设了最佳构建体验的配置与常用搬砖工具。意在提供一个简洁、快速上手的模板。
## 提示
> 项目默认启用严格模式 `eslint`,但是由于 `vite-plugin-eslint` 插件优先级最高,所以如果出现自动导入类型错误提示,请优先解决其他问题。
> 建议开启 `vscode` 保存自动修复功能。
## 版本说明
> 做了一些大的改动升级,让模板更加好用了一点,默认主题色也做了变更更好看了一点。啰嗦两句,好像也没啥其他的了...
## 拉取依赖
```sh
# github
git clone https://github.com/XiaoDaiGua-Ray/ray-template.git
```
# yarn
### Pull dependencies
yarn
```
```sh
pnpm i
# npm
npm install
```
### Test project
## 启动项目
```sh
# yarn
pnpm test
yarn dev
```
### Startup project
```sh
pnpm dev
# npm
npm run dev
```
### Build project
## 项目打包
```sh
pnpm build
```
# yarn
### Preview project
yarn build
```
```sh
pnpm preview
# npm
npm run build
```
### Report project
## 预览项目
```sh
pnpm report
# yarn
yarn preview
```
### Development
```sh
# npm
Just delete the files under `views/demo`, `router/modules/demo` to get a clean project template.
npm run preview
```
## 🪴 Project Activities
## 体积分析
![Alt](https://repobeats.axiom.co/api/embed/fab6071297ab281913a42f07a2779b488cfd62b8.svg 'Repobeats analytics image')
```sh
# yarn
### Contributors
yarn report
```
Thanks for all their contributions 🐝 !
```sh
# npm
<a href="https://github.com/XiaoDaiGua-Ray/ray-template/graphs/contributors">
<img src="https://contrib.rocks/image?repo=XiaoDaiGua-Ray/ray-template" />
</a>
npm run report
```
## Browser Support
## 项目依赖
| [<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 |
- [pinia](https://pinia.vuejs.org/) `全局状态管理器`
- [@vueuse](https://vueuse.org/) `vue3 hooks`
- [vue-router](https://router.vuejs.org/zh/) `router`
- [axios](http://axios-js.com/zh-cn/docs/index.html) `ajax request`
- [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html) `国际化`
- [scrollreveal.js](https://scrollrevealjs.org/) `滚动加载动画`(暂时移除)
- [crypto-js](https://github.com/brix/crypto-js) `加密`
- [vite-svg-loader](https://github.com/jpkleemans/vite-svg-loader) `svg组件化`
- [vite-plugin-svg-icons](https://github.com/vbenjs/vite-plugin-svg-icons/blob/main/README.zh_CN.md) `svg雪碧图`
- [echarts5](https://echarts.apache.org/examples/zh/index.html#chart-type-line) `可视化`
- [lodash-es](https://www.lodashjs.com/) `拓展方法`
- 还有一些后续补充的,懒得写了。。。自己看项目依赖页面
## 📄 License
## 基础组件
[MIT License](https://github.com/XiaoDaiGua-Ray/ray-template/blob/main/LICENSE) © 2022-PRESENT [Ray](https://github.com/XiaoDaiGua-Ray)
- `RayIcon` `svg icon`
- `RayChart` 基于 `echarts5.x` 封装可视化组件
- `RayTransitionComponent` 带过渡动画路由组件,效果与 `RouterView` 相同
- `RayTable` 基于 `Naive UI DataTable` 组件封装,实现了一些小功能
- `RayCollapseGrid` 基于 `Naive UI NGrid` 组件封装的可折叠操作栏
## 项目结构
```
- locales: 国际化多语言入口(本项目采用 json 格式)
- assets: 项目静态资源入口
- component: 全局共用组件
- icons: 项目svg图标资源需要配合 RayIcon 组件使用
- language: 国际化
- layout: 全局页面结构入口
- router: 路由表
- store: 全局状态管理入口
- styles: 全局公共样式入口
- types: 全局 type
- utils: 工具包
- views: 页面入口
- vite-plugin: 插件注册
```
## 浏览器支持
> 仅支持现代浏览器,不支持 `IE`
## 最后,希望大家搬砖愉快
## 贡献者
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://heartofyun.com"><img src="https://avatars.githubusercontent.com/u/40163747?v=4?s=100" width="100px;" alt="Cloud"/><br /><sub><b>Cloud</b></sub></a><br /><a href="#tool-yunkuangao" title="Tools">🔧</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

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')
})
})

287
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,287 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDialog: typeof import('naive-ui')['useDialog']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMessage: typeof import('naive-ui')['useMessage']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNotification: typeof import('naive-ui')['useNotification']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}

151
cfg.ts Normal file
View File

@ -0,0 +1,151 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-04-06
*
* @workspace ray-template
*
* @remark
*/
/**
*
*
*
* :
* - 构建: 开发构建
* - 系统: 根路由
* - 请求: 代理配置
*
* , src/types/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_PRIMARY_COLOR } from './src/appConfig/designConfig'
import {
PRE_LOADING_CONFIG,
ROOT_ROUTE,
SIDE_BAR_LOGO,
} from './src/appConfig/appConfig'
import type { AppConfigExport } from './src/types/cfg'
const config: AppConfigExport = {
/** 公共基础路径配置, 如果为空则会默认以 '/' 填充 */
base: '/ray-template/',
/** 配置首屏加载信息 */
preloadingConfig: PRE_LOADING_CONFIG,
/** 默认主题色(不可省略, 必填), 也用于 ejs 注入 */
appPrimaryColor: 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',
'./src/styles/theme.scss',
]),
/**
*
*
*
* ,
*/
copyright: 'Copyright © 2022-present Ray',
/**
*
*
*/
title: HTMLTitlePlugin('Ray Template'),
/**
*
* HMR ()
*/
server: {
host: '0.0.0.0',
port: 9527,
open: false,
https: false,
strictPort: false,
fs: {
strict: false,
allow: [],
},
proxy: {
'/api': {
target: 'url',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/office': {
target: 'https://office.yka.one/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/office/, ''),
},
},
},
/**
*
*
*/
buildOptions: buildOptions,
/**
*
*
* - `@`: `src`
* - `@use-utils`: `src/utils`
* - `@use-api`: `src/axios/api`
* - `@use-images`: `src/assets/images`
*/
alias: [
{
find: '@',
replacement: path.resolve(__dirname, './src'),
},
{
find: '@use-utils',
replacement: path.resolve(__dirname, './src/utils'),
},
{
find: '@use-api',
replacement: path.resolve(__dirname, './src/axios/api'),
},
{
find: '@use-images',
replacement: path.resolve(__dirname, './src/assets/images'),
},
],
}
export default config

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 = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
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': [
2,
'always',
[
'update',
'fix',
'bug',
'feat',
'chore',
'fix',
'docs',
'style',
'refactor',
'test',
'style',
'version',
'build',
'plugin',
'chore',
'revert',
'merge',
],
],
},

14
components.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TransitionComponent: typeof import('./src/components/RayTransitionComponent/TransitionComponent.vue')['default']
}
}

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

View File

@ -1,42 +0,0 @@
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'
export const getPersonList = defineMock({
url: '/api/list',
method: 'GET',
delay: 500,
response: (req, res) => {
const {
query: { page, pageSize, email },
} = req
let list = array(100).map(() => tableMock())
let length = list.length
if (!page || !pageSize) {
res.end(
stringify(
response(list, 200, '请求成功', {
total: length,
}),
),
)
} else {
list = pagination(page, pageSize, list)
if (email) {
list = list.filter((curr) => curr.email.includes(email))
length = list.length
}
res.end(
stringify(
response(list, 200, '请求成功', {
total: length,
}),
),
)
}
},
})

View File

@ -1,19 +0,0 @@
import Mock from 'mockjs'
/**
*
* @param option
*
*
*/
export function tableMock(option?: object) {
return {
...option,
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,57 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function array(length: number) {
return new Array(length).fill(0)
}
/**
*
* @param pageCurrent
* @param pageSize
* @param array
*
* mock
*/
export function pagination<T = any>(
pageCurrent: number,
pageSize: number,
array: T[],
): T[] {
const offset = (pageCurrent - 1) * Number(pageSize)
return offset + Number(pageSize) >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + Number(pageSize))
}
/**
*
* @param value
*
* json
*/
export function stringify<T = unknown>(value: T) {
return JSON.stringify(value)
}
/**
*
* @param res
* @param code
* @param msg
*/
export function response<T = any>(
res: T,
code: number,
msg: string,
params?: object,
) {
const basic = {
code,
msg,
data: res,
...params,
}
return basic
}

172
package.json Executable file → Normal file
View File

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

10101
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,29 +1,96 @@
import RayGlobalProvider from '@/components/RayGlobalProvider/index'
import { RouterView } from 'vue-router'
import AppNaiveGlobalProvider from '@/app-components/provider/AppNaiveGlobalProvider'
import AppStyleProvider from '@/app-components/provider/AppStyleProvider'
import AppLockScreen from '@/app-components/app/AppLockScreen'
import AppWatermarkProvider from '@/app-components/provider/AppWatermarkProvider'
import AppGlobalSpin from '@/app-components/app/AppGlobalSpin'
import AppVersionProvider from '@/app-components/provider/AppVersionProvider'
import GlobalSpin from '@/spin/index'
import LockScreen from '@/components/AppComponents/AppLockScreen/index'
import { APP_GLOBAL_LOADING } from '@/app-config'
import { getCache } from '@/utils/cache'
import { get } from 'lodash-es'
import { useSetting } from '@/store'
import { addClass, removeClass, addStyle, colorToRgba } from '@/utils/element'
export default defineComponent({
const App = defineComponent({
name: 'App',
setup() {
const settingStore = useSetting()
const { themeValue } = storeToRefs(settingStore)
/** 同步主题色变量至 body, 如果未获取到缓存值则已默认值填充 */
const syncPrimaryColorToBody = () => {
const {
appPrimaryColor: { primaryColor, primaryFadeColor },
} = __APP_CFG__ // 默认主题色
const body = document.body
const primaryColorOverride = getCache('piniaSettingStore', 'localStorage')
const _p = get(
primaryColorOverride,
'primaryColorOverride.common.primaryColor',
)
const _fp = colorToRgba(_p, 0.3)
/** 设置全局主题色 css 变量 */
body.style.setProperty('--ray-theme-primary-color', _p || primaryColor)
body.style.setProperty(
'--ray-theme-primary-fade-color',
_fp || primaryFadeColor,
)
}
/** 隐藏加载动画 */
const hiddenLoadingAnimation = () => {
/** pre-loading-animation 是默认 id */
const el = document.getElementById('pre-loading-animation')
if (el) {
addStyle(el, {
display: 'none',
})
}
}
syncPrimaryColorToBody()
hiddenLoadingAnimation()
/** 切换主题时, 同步更新 body class 以便于进行自定义 css 配置 */
watch(
() => themeValue.value,
(newData) => {
/**
*
* body class
*
* themeValue
*/
const body = document.body
const darkClassName = 'ray-template--dark'
const lightClassName = 'ray-template--light'
newData
? removeClass(body, lightClassName)
: removeClass(body, darkClassName)
addClass(body, newData ? darkClassName : lightClassName)
},
{
immediate: true,
},
)
},
render() {
return (
<AppNaiveGlobalProvider>
<AppVersionProvider />
<AppLockScreen />
<AppStyleProvider />
<AppWatermarkProvider />
<AppGlobalSpin>
<RayGlobalProvider>
<LockScreen />
<GlobalSpin>
{{
default: () => <RouterView />,
description: () => APP_GLOBAL_LOADING,
description: () => 'lodaing...',
}}
</AppGlobalSpin>
</AppNaiveGlobalProvider>
</GlobalSpin>
</RayGlobalProvider>
)
},
})
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

@ -1,27 +0,0 @@
import { request } from '@/axios'
import type { PaginationResponse } from '@/types'
export interface MockListParams {
page: number
pageSize: number
}
export interface Person {
name: string
id: string
age: number
address: string
createDate: string
email: string | null
}
export const getPersonList = (
params: Partial<Pick<MockListParams & Person, 'email' | 'pageSize' | 'page'>>,
) => {
return request<PaginationResponse<Person[]>>({
url: '/api/list',
method: 'get',
params,
})
}

View File

@ -1,45 +0,0 @@
/**
*
*
*
*
* 1. demo
* 2.
* 3. setup 使使 useHookPlusRequest 便使 setup 使使
*/
import { request } from '@/axios'
import type { BasicResponse } from '@/types'
interface AxiosTestResponse extends UnknownObjectKey {
data: UnknownObjectKey[]
city?: string
}
interface JSONPlaceholder {
completed: boolean
id: number
title: string
userId: number
}
/**
*
* @returns
*
* @method get
*/
export const getWeather = (city: string) => {
return request<AxiosTestResponse>({
url: `https://www.tianqiapi.com/api?version=v9&appid=23035354&appsecret=8YvlPNrz&city=${city}`,
method: 'get',
})
}
export const getTypicCode = () => {
return request<JSONPlaceholder>({
url: 'https://jsonplaceholder.typicode.com/todos/1',
method: 'get',
})
}

View File

@ -1,6 +0,0 @@
## 描述
该包存放与模板深度绑定的组件:
- app存放与模板数据绑定的组件
- provider存放模板注入类组件

View File

@ -1,72 +0,0 @@
/**
*
*
*
* naive ui Avatar ,
* session catch
*/
import { NAvatar, NButton, NFlex } from 'naive-ui'
import { avatarProps } from 'naive-ui'
import { useSigningGetters } from '@/store'
import type { PropType } from 'vue'
import type { AvatarProps, FlexProps } from 'naive-ui'
const AppAvatar = defineComponent({
name: 'AppAvatar',
props: {
...avatarProps,
cursor: {
type: String,
default: 'auto',
},
spaceSize: {
type: [String, Number, Array] as PropType<FlexProps['size']>,
default: 'medium',
},
avatarSize: {
type: [String, Number] as PropType<AvatarProps['size']>,
default: 'medium',
},
vertical: {
type: Boolean,
default: false,
},
},
setup() {
const { getSigningCallback } = useSigningGetters()
return {
getSigningCallback,
}
},
render() {
const { getSigningCallback, avatarSize, spaceSize, $props, vertical } = this
return (
<NButton quaternary strong focusable={false}>
<NFlex align="center" size={spaceSize} vertical={vertical}>
<NAvatar
{...($props as AvatarProps)}
src={getSigningCallback?.avatar}
objectFit="cover"
round
size={avatarSize}
>
{{
default: () =>
getSigningCallback.avatar
? null
: getSigningCallback?.name?.[0],
}}
</NAvatar>
{getSigningCallback?.name}
</NFlex>
</NButton>
)
},
})
export default AppAvatar

View File

@ -1,26 +0,0 @@
import { useStorage } from '@vueuse/core'
import { APP_CATCH_KEY } from '@/app-config'
const appLockScreen = useStorage(
APP_CATCH_KEY.isAppLockScreen,
false,
window.localStorage,
{
mergeDefaults: true,
},
)
const useAppLockScreen = () => {
const setLockAppScreen = (bool: boolean) => {
appLockScreen.value = bool
}
const getLockAppScreen = () => appLockScreen.value
return {
setLockAppScreen,
getLockAppScreen,
}
}
export default useAppLockScreen

View File

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

View File

@ -1,193 +0,0 @@
import '../../index.scss'
import { NInput, NFormItem, NButton, NFlex } from 'naive-ui'
import AppAvatar from '@/app-components/app/AppAvatar'
import { RForm } from '@/components'
import dayjs from 'dayjs'
import { useSigningActions, useSettingActions } from '@/store'
import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useDevice } from '@/hooks'
import { useForm } from '@/components'
import { APP_CATCH_KEY } from '@/app-config'
import { removeStorage, decrypt, getStorage } from '@/utils'
export default defineComponent({
name: 'UnlockScreen',
setup() {
const [register, { validate }] = useForm()
const { logout } = useSigningActions()
const { updateSettingState } = useSettingActions()
const { setLockAppScreen } = useAppLockScreen()
const { isTabletOrSmaller } = useDevice()
const HH_MM_FORMAT = 'HH:mm'
const AM_PM_FORMAT = 'A'
const YY_MM_DD_FORMAT = 'YYYY-MM-DD'
const DDD_FORMAT = 'ddd'
const state = reactive({
lockCondition: useCondition(),
HH_MM: dayjs().format(HH_MM_FORMAT),
AM_PM: dayjs().format(AM_PM_FORMAT),
YY_MM_DD: dayjs().format(YY_MM_DD_FORMAT),
DDD: dayjs().format(DDD_FORMAT),
})
const dayInterval = setInterval(() => {
state.HH_MM = dayjs().format(HH_MM_FORMAT)
state.AM_PM = dayjs().format(AM_PM_FORMAT)
}, 6_000)
const yearInterval = setInterval(() => {
state.YY_MM_DD = dayjs().format(YY_MM_DD_FORMAT)
state.DDD = dayjs().format(DDD_FORMAT)
}, 86_400_000)
const toSigningFn = () => {
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
updateSettingState('lockScreenSwitch', false)
setTimeout(() => {
logout()
}, 100)
}
const backToSigning = () => {
window.$dialog.warning({
title: '警告',
content: '是否返回到登陆页并且重新登录',
positiveText: '确定',
negativeText: '重新登录',
onPositiveClick: toSigningFn,
})
}
const unlockScreen = () => {
const catchPassword = getStorage<string>(
APP_CATCH_KEY.appLockScreenPasswordKey,
'localStorage',
)
if (!catchPassword) {
window.$dialog.warning({
title: '警告',
content: () => '检测到锁屏密码被修改,请重新登录',
closable: false,
maskClosable: false,
closeOnEsc: false,
positiveText: '重新登录',
onPositiveClick: toSigningFn,
})
return
}
const dCatchPassword = decrypt(catchPassword)
validate().then(() => {
if (dCatchPassword === state.lockCondition.lockPassword) {
setLockAppScreen(false)
updateSettingState('lockScreenSwitch', false)
removeStorage(APP_CATCH_KEY.appLockScreenPasswordKey, 'localStorage')
state.lockCondition = useCondition()
} else {
window.$message.warning('密码错误,请重新输入')
}
})
}
onBeforeUnmount(() => {
clearInterval(dayInterval)
clearInterval(yearInterval)
})
return {
...toRefs(state),
backToSigning,
unlockScreen,
isTabletOrSmaller,
register,
}
},
render() {
const { isTabletOrSmaller } = this
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
const hmSplit = HH_MM.split(':')
const { unlockScreen, backToSigning, register } = this
return (
<div class="app-lock-screen__content app-lock-screen__content--full">
<div class="app-lock-screen__unlock">
<div class="app-lock-screen__unlock__content">
<div class="app-lock-screen__unlock__content-wrapper">
<div
class={[
'app-lock-screen__unlock__content-bg__wrapper',
'app-lock-screen__unlock__content-bg',
isTabletOrSmaller
? 'app-lock-screen__unlock__content-bg--smaller'
: '',
]}
>
<div class="left">{hmSplit[0]}</div>
<div class="right">{hmSplit[1]}</div>
</div>
</div>
<div class="app-lock-screen__unlock__content-avatar">
<AppAvatar
avatarSize={52}
style="pointer-events: none;"
vertical
/>
</div>
<div class="app-lock-screen__unlock__content-input">
<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>
)
},
})

View File

@ -1,107 +0,0 @@
.app-lock-screen__content {
&.app-lock-screen__content--full {
width: 100%;
height: var(--html-height);
@include flexCenter;
}
& .app-lock-screen__input {
& button[class*='n-button'] {
width: 100%;
}
& form[class*='n-form'] {
margin: 24px 0px;
}
}
& .app-lock-screen__unlock {
.app-lock-screen__unlock__content {
position: relative;
@include flexCenter;
flex-direction: column;
& .app-lock-screen__unlock__content-wrapper {
position: fixed;
inset: 0px;
& .app-lock-screen__unlock__content-bg__wrapper {
width: 100%;
height: 100%;
background-color: rgb(16, 16, 20);
}
& .app-lock-screen__unlock__content-bg {
position: absolute;
width: 100%;
height: 100%;
@include flexCenter;
font-size: 16.67rem;
gap: 80px;
z-index: 0;
&.app-lock-screen__unlock__content-bg--smaller {
& .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 {
margin-top: 5px;
color: #bababa;
font-weight: bolder;
z-index: 1;
}
& .app-lock-screen__unlock__content-input {
width: 260px;
z-index: 1;
}
& .app-lock-screen__unlock__content-date {
position: fixed;
width: 100%;
font-size: 3rem;
text-align: center;
font-weight: 500;
bottom: 24px;
z-index: 1;
& .current-year,
& .current-date span {
font-size: 1.875rem;
line-height: 2.25rem;
}
}
}
}
}
.ray-template--light {
.app-lock-screen__unlock__content-bg__wrapper {
background-color: #fff !important;
}
.app-lock-screen__unlock__content-bg {
& .left,
& .right {
background-color: rgba(244, 244, 245, 1) !important;
}
}
}

View File

@ -1,40 +0,0 @@
import { RModal } from '@/components'
import LockScreen from './components/LockScreen'
import { useSettingGetters, useSettingActions } from '@/store'
const AppLockScreen = defineComponent({
name: 'AppLockScreen',
setup() {
const { updateSettingState } = useSettingActions()
const { getLockScreenSwitch } = useSettingGetters()
const lockScreenSwitchRef = computed({
get: () => getLockScreenSwitch.value,
set: (val) => {
updateSettingState('lockScreenSwitch', val)
},
})
return {
lockScreenSwitchRef,
}
},
render() {
return (
<RModal
v-model:show={this.lockScreenSwitchRef}
transformOrigin="center"
show
autoFocus={false}
maskClosable={false}
closeOnEsc={false}
preset="dialog"
title="锁定屏幕"
>
<LockScreen />
</RModal>
)
},
})
export default AppLockScreen

View File

@ -1,20 +0,0 @@
import type { InputInst } from 'naive-ui'
import type { Ref } from 'vue'
/** 统一的校验锁屏密码校验规则 */
export const rules = {
lockPassword: {
required: true,
message: '请输入正确格式密码',
min: 6,
max: 12,
trigger: ['input'],
},
}
/** 锁屏密码参数 */
export const useCondition = () => {
return {
lockPassword: null,
}
}

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