初始化多样青春搭+ 前后端分离后台
This commit is contained in:
commit
ee48b8c93d
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
56
.env
Normal file
56
.env
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# the base url of the application, the default is "/"
|
||||||
|
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
|
||||||
|
VITE_BASE_URL=/
|
||||||
|
|
||||||
|
VITE_APP_TITLE=SoybeanAdmin
|
||||||
|
|
||||||
|
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
|
||||||
|
|
||||||
|
# the prefix of the icon name
|
||||||
|
VITE_ICON_PREFIX=icon
|
||||||
|
|
||||||
|
# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
|
||||||
|
# format {VITE_ICON_PREFIX}-{local icon name}
|
||||||
|
VITE_ICON_LOCAL_PREFIX=icon-local
|
||||||
|
|
||||||
|
# auth route mode: static | dynamic
|
||||||
|
VITE_AUTH_ROUTE_MODE=dynamic
|
||||||
|
|
||||||
|
# static auth route home
|
||||||
|
VITE_ROUTE_HOME=home
|
||||||
|
|
||||||
|
# default menu icon
|
||||||
|
VITE_MENU_ICON=mdi:menu
|
||||||
|
|
||||||
|
# whether to enable http proxy when is dev mode
|
||||||
|
VITE_HTTP_PROXY=Y
|
||||||
|
|
||||||
|
# vue-router mode: hash | history | memory
|
||||||
|
VITE_ROUTER_HISTORY_MODE=history
|
||||||
|
|
||||||
|
# success code of backend service, when the code is received, the request is successful
|
||||||
|
VITE_SERVICE_SUCCESS_CODE=1
|
||||||
|
|
||||||
|
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
|
||||||
|
VITE_SERVICE_LOGOUT_CODES=8888,8889
|
||||||
|
|
||||||
|
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
|
||||||
|
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
||||||
|
|
||||||
|
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
|
||||||
|
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333
|
||||||
|
|
||||||
|
# when the route mode is static, the defined super role
|
||||||
|
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||||
|
|
||||||
|
# sourcemap
|
||||||
|
VITE_SOURCE_MAP=N
|
||||||
|
|
||||||
|
# Used to differentiate storage across different domains
|
||||||
|
VITE_STORAGE_PREFIX=SOY_
|
||||||
|
|
||||||
|
# used to control whether the program automatically detects updates
|
||||||
|
VITE_AUTOMATICALLY_DETECT_UPDATE=Y
|
||||||
|
|
||||||
|
# show proxy url log in terminal
|
||||||
|
VITE_PROXY_LOG=Y
|
7
.env.prod
Normal file
7
.env.prod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# backend service base url, prod environment
|
||||||
|
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
||||||
|
|
||||||
|
# other backend service base url, prod environment
|
||||||
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
|
"demo": "http://localhost:9529"
|
||||||
|
}`
|
8
.env.test
Normal file
8
.env.test
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# backend service base url, test environment
|
||||||
|
VITE_SERVICE_BASE_URL=http://192.168.10.137:8080/
|
||||||
|
# VITE_SERVICE_BASE_URL=https://tsw.hschool.com.cn/
|
||||||
|
|
||||||
|
# other backend service base url, test environment
|
||||||
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
|
"demo": "http://localhost:9528"
|
||||||
|
}`
|
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"*.vue" eol=lf
|
||||||
|
"*.js" eol=lf
|
||||||
|
"*.ts" eol=lf
|
||||||
|
"*.jsx" eol=lf
|
||||||
|
"*.tsx" eol=lf
|
||||||
|
"*.mjs" eol=lf
|
||||||
|
"*.json" eol=lf
|
||||||
|
"*.html" eol=lf
|
||||||
|
"*.css" eol=lf
|
||||||
|
"*.scss" eol=lf
|
||||||
|
"*.md" eol=lf
|
||||||
|
"*.yaml" eol=lf
|
||||||
|
"*.yml" eol=lf
|
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
.VSCodeCounter
|
4
.npmrc
Normal file
4
.npmrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
registry=https://registry.npmmirror.com/
|
||||||
|
shamefully-hoist=true
|
||||||
|
ignore-workspace-root-check=true
|
||||||
|
link-workspace-packages=true
|
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "启动程序",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}\\vite.config.ts",
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"src/locales",
|
||||||
|
"src/locales/langs",
|
||||||
|
"packages/scripts/src/locales"
|
||||||
|
]
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Soybean
|
||||||
|
|
||||||
|
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.
|
181
README.md
Normal file
181
README.md
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="./public/favicon.svg" width="160" />
|
||||||
|
<h1>SoybeanAdmin</h1>
|
||||||
|
<span>中文 | <a href="./README.en_US.md">English</a></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[](./LICENSE)
|
||||||
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
|
[](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
|
||||||
|
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 如果您觉得 `SoybeanAdmin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **前沿技术应用**:采用 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
||||||
|
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
|
||||||
|
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
|
||||||
|
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
|
||||||
|
- **丰富的主题配置**:内置多样的主题配置,与 UnoCSS 完美结合。
|
||||||
|
- **内置国际化方案**:轻松实现多语言支持。
|
||||||
|
- **自动化文件路由系统**:自动生成路由导入、声明和类型。更多细节请查看 [Elegant Router](https://github.com/soybeanjs/elegant-router)。
|
||||||
|
- **灵活的权限路由**:同时支持前端静态路由和后端动态路由。
|
||||||
|
- **丰富的页面组件**:内置多样页面和组件,包括403、404、500页面,以及布局组件、标签组件、主题配置组件等。
|
||||||
|
- **命令行工具**:内置高效的命令行工具,git提交、删除文件、发布等。
|
||||||
|
- **移动端适配**:完美支持移动端,实现自适应布局。
|
||||||
|
|
||||||
|
|
||||||
|
## 版本
|
||||||
|
|
||||||
|
- **NaiveUI 版本:**
|
||||||
|
- [预览地址](https://naive.soybeanjs.cn/)
|
||||||
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin)
|
||||||
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
- **AntDesignVue 版本:**
|
||||||
|
- [预览地址](https://antd.soybeanjs.cn/)
|
||||||
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-antd)
|
||||||
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||||
|
- **ElementPlus 版本:**
|
||||||
|
- [预览地址](https://elp.soybeanjs.cn/)
|
||||||
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-element-plus)
|
||||||
|
- **旧版:**
|
||||||
|
- [预览地址](https://legacy.soybeanjs.cn/)
|
||||||
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
||||||
|
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [地址](https://docs.soybeanjs.cn)
|
||||||
|
- [旧版文档](https://legacy-docs.soybeanjs.cn)
|
||||||
|
|
||||||
|
|
||||||
|
## 合作事项
|
||||||
|
|
||||||
|
我们非常感谢大家对 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的支持!为了进一步回馈社区,并助力企业和开发者实现个性化需求,我们现提供多种合作服务,期待与您携手共赢。
|
||||||
|
|
||||||
|
##### 1、定制化管理后台开发
|
||||||
|
|
||||||
|
针对企业和开发者的特定业务需求,我们提供基于 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的定制化管理后台开发服务。我们的团队具备丰富的行业经验,能够迅速理解并实现您的需求,打造高效、灵活且安全的定制化解决方案。
|
||||||
|
|
||||||
|
- **定制开发**:我们将根据您的具体需求,提供从需求分析、UI设计到功能实现的全方位服务,确保项目高效交付。
|
||||||
|
- **功能扩展**:在 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 基础上,扩展您所需的特定功能模块,提升管理后台的功能和用户体验。
|
||||||
|
|
||||||
|
##### 2、企业外包服务
|
||||||
|
|
||||||
|
我们承接各类企业级外包项目,特别是在管理后台系统的开发、集成与运维方面。我们以精益求精的态度,确保项目的质量和进度,为您的业务提供强有力的技术支持。
|
||||||
|
|
||||||
|
- **项目开发**:无论是全新的项目,还是现有系统的优化与集成,我们都将为您量身打造高效可靠的解决方案。
|
||||||
|
- **系统集成与维护**:我们也提供基于 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的系统集成与长期维护服务,确保您的系统稳定、安全地运行。
|
||||||
|
|
||||||
|
##### 3、联系方式
|
||||||
|
|
||||||
|
如有合作意向或项目咨询,请通过以下方式与我们联系:
|
||||||
|
|
||||||
|
- **Email**: [soybeanjs@outlook.com](mailto:soybeanjs@outlook.com)
|
||||||
|
- **GitHub Issues**: 欢迎通过 [GitHub Issues](https://github.com/soybeanjs/soybean-admin/issues/new) 联系我们,进行初步的合作洽谈。
|
||||||
|
- **商务合作微信**: honghuangdc
|
||||||
|
|
||||||
|
期待与您开展深入合作,共同推动 SoybeanAdmin 项目及其在更多领域的成功应用!
|
||||||
|
|
||||||
|
|
||||||
|
## 示例图片
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
**环境准备**
|
||||||
|
|
||||||
|
确保你的环境满足以下要求:
|
||||||
|
|
||||||
|
- **git**: 你需要git来克隆和管理项目版本。
|
||||||
|
- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
|
||||||
|
- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
|
||||||
|
|
||||||
|
**克隆项目**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/soybeanjs/soybean-admin.git
|
||||||
|
```
|
||||||
|
|
||||||
|
**安装依赖**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
> 由于本项目采用了 pnpm monorepo 的管理方式,因此请不要使用 npm 或 yarn 来安装依赖。
|
||||||
|
|
||||||
|
**启动项目**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**构建项目**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码同步**
|
||||||
|
|
||||||
|
参考 [代码同步](https://docs.soybeanjs.cn/zh/guide/sync) 文档。
|
||||||
|
|
||||||
|
## 周边生态
|
||||||
|
|
||||||
|
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): 基于SoybeanAdmin的React版本.
|
||||||
|
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): 一个 Mock Api 管理系统,帮助前端开发伙伴快速实现接口的 mock。
|
||||||
|
- [T-Shell](https://github.com/TheBlindM/T-Shell): 是一个可配置命令提示的终端模拟器和 SSH 客户端。
|
||||||
|
- [pea](https://github.com/haitang1894/pea) : 采用SpringBoot3.2 + JDK21、MyBatis-Plus、SpringSecurity安全框架等,适配 [soybean-admin](https://gitee.com/honghuangdc/soybean-admin) 开发的简单权限系统。
|
||||||
|
- [MalusAdmin](https://github.com/pridejoy/MalusAdmin): 基于 Vue3/TypeScript/NaiveUI 和 NET7 & Sqlsugar 开发的后台管理框架。采用最原生最简洁的方式来实现, 前端清新优雅高颜值,后端 结构清晰,优雅易懂,功能强大。
|
||||||
|
- [PanisAdmin](https://github.com/paynezhuang/panis-admin): 采用SpringBoot3、SaToken、MySQL等框架开发,二次修改 [soybean-admin](https://github.com/soybeanjs/soybean-admin),适配动态菜单/按钮级别的鉴权,保留原汁原味、清新优雅、高颜值的后台管理系统脚手架。
|
||||||
|
- [snail-job](https://github.com/aizuda/snail-job): 一款兼具 “高性能、高颜值、高活跃” 的分布式任务重试和分布式任务调度平台。
|
||||||
|
- [SuperApi](https://github.com/TmmTop/SuperApi): 快速将你的 idea 变成线上稳定运行的产品! 无实体建库建表,对无实体库表进行增删改查,支持 15 种条件查询,以及分页,列表,无限级树形列表 等功能的 API 部署! 拥有接口文档,Auth 授权,接口限流,获取客户端真实 IP,先进的服务器缓存组件,动态 API 等功能,期待您的体验!
|
||||||
|
- [FastSoyAdmin](https://github.com/sleep1223/fast-soy-admin): 基于 FastAPI+Vue3+Naive UI 的现代化轻量管理平台.
|
||||||
|
|
||||||
|
|
||||||
|
## 如何贡献
|
||||||
|
|
||||||
|
我们热烈欢迎并感谢所有形式的贡献。如果您有任何想法或建议,欢迎通过提交 [pull requests](https://github.com/soybeanjs/soybean-admin/pulls) 或创建 GitHub [issue](https://github.com/soybeanjs/soybean-admin/issues/new) 来分享。
|
||||||
|
|
||||||
|
## Git 提交规范
|
||||||
|
|
||||||
|
本项目已内置 `commit` 命令,您可以通过执行 `pnpm commit` 来生成符合 [Conventional Commits]([conventionalcommits](https://www.conventionalcommits.org/)) 规范的提交信息。在提交PR时,请务必使用 `commit` 命令来创建提交信息,以确保信息的规范性。
|
||||||
|
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
推荐使用最新版的 Chrome 浏览器进行开发,以获得更好的体验。
|
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<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/) | [<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/) | [<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/) | [<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/) |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||||
|
|
||||||
|
## Star 趋势
|
||||||
|
|
||||||
|
[](https://star-history.com/#soybeanjs/soybean-admin&Date)
|
||||||
|
|
||||||
|
## 开源协议
|
||||||
|
|
||||||
|
项目基于 [MIT © 2021 Soybean](./LICENSE) 协议,仅供学习参考,商业使用请保留作者版权信息,作者不保证也不承担任何软件的使用风险。
|
2
build/config/index.ts
Normal file
2
build/config/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './proxy';
|
||||||
|
export * from './time';
|
55
build/config/proxy.ts
Normal file
55
build/config/proxy.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { HttpProxy, ProxyOptions } from 'vite';
|
||||||
|
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||||
|
import { consola } from 'consola';
|
||||||
|
import { createServiceConfig } from '../../src/utils/service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set http proxy
|
||||||
|
*
|
||||||
|
* @param env - The current env
|
||||||
|
* @param enable - If enable http proxy
|
||||||
|
*/
|
||||||
|
export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||||
|
const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y';
|
||||||
|
|
||||||
|
if (!isEnableHttpProxy) return undefined;
|
||||||
|
|
||||||
|
const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y';
|
||||||
|
|
||||||
|
const { baseURL, proxyPattern, other } = createServiceConfig(env);
|
||||||
|
|
||||||
|
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog);
|
||||||
|
|
||||||
|
other.forEach(item => {
|
||||||
|
Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
|
||||||
|
});
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) {
|
||||||
|
const proxy: Record<string, ProxyOptions> = {};
|
||||||
|
|
||||||
|
proxy[item.proxyPattern] = {
|
||||||
|
target: item.baseURL,
|
||||||
|
changeOrigin: true,
|
||||||
|
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
|
||||||
|
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||||
|
if (!enableLog) return;
|
||||||
|
|
||||||
|
const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`;
|
||||||
|
|
||||||
|
const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`;
|
||||||
|
|
||||||
|
consola.log(`${requestUrl}\n${proxyUrl}`);
|
||||||
|
});
|
||||||
|
_proxy.on('error', (_err, req, _res) => {
|
||||||
|
if (!enableLog) return;
|
||||||
|
consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
|
||||||
|
};
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
12
build/config/time.ts
Normal file
12
build/config/time.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
|
||||||
|
export function getBuildTime() {
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
|
||||||
|
return buildTime;
|
||||||
|
}
|
13
build/plugins/html.ts
Normal file
13
build/plugins/html.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
|
export function setupHtmlPlugin(buildTime: string) {
|
||||||
|
const plugin: Plugin = {
|
||||||
|
name: 'html-plugin',
|
||||||
|
apply: 'build',
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
return html.replace('<head>', `<head>\n <meta name="buildTime" content="${buildTime}">`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
}
|
24
build/plugins/index.ts
Normal file
24
build/plugins/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||||
|
import progress from 'vite-plugin-progress';
|
||||||
|
import { setupElegantRouter } from './router';
|
||||||
|
import { setupUnocss } from './unocss';
|
||||||
|
import { setupUnplugin } from './unplugin';
|
||||||
|
import { setupHtmlPlugin } from './html';
|
||||||
|
|
||||||
|
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||||
|
const plugins: PluginOption = [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
VueDevtools(),
|
||||||
|
setupElegantRouter(),
|
||||||
|
setupUnocss(viteEnv),
|
||||||
|
...setupUnplugin(viteEnv),
|
||||||
|
progress(),
|
||||||
|
setupHtmlPlugin(buildTime)
|
||||||
|
];
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
41
build/plugins/router.ts
Normal file
41
build/plugins/router.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
import ElegantVueRouter from '@elegant-router/vue/vite';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
|
||||||
|
export function setupElegantRouter() {
|
||||||
|
return ElegantVueRouter({
|
||||||
|
layouts: {
|
||||||
|
base: 'src/layouts/base-layout/index.vue',
|
||||||
|
blank: 'src/layouts/blank-layout/index.vue'
|
||||||
|
},
|
||||||
|
routePathTransformer(routeName, routePath) {
|
||||||
|
const key = routeName as RouteKey;
|
||||||
|
|
||||||
|
if (key === 'login') {
|
||||||
|
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
|
||||||
|
|
||||||
|
const moduleReg = modules.join('|');
|
||||||
|
|
||||||
|
return `/login/:module(${moduleReg})?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return routePath;
|
||||||
|
},
|
||||||
|
onRouteMetaGen(routeName) {
|
||||||
|
const key = routeName as RouteKey;
|
||||||
|
|
||||||
|
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
|
||||||
|
|
||||||
|
const meta: Partial<RouteMeta> = {
|
||||||
|
title: key,
|
||||||
|
i18nKey: `route.${key}` as App.I18n.I18nKey
|
||||||
|
};
|
||||||
|
|
||||||
|
if (constantRoutes.includes(key)) {
|
||||||
|
meta.constant = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
32
build/plugins/unocss.ts
Normal file
32
build/plugins/unocss.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import unocss from '@unocss/vite';
|
||||||
|
import presetIcons from '@unocss/preset-icons';
|
||||||
|
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
|
||||||
|
|
||||||
|
export function setupUnocss(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||||
|
|
||||||
|
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||||
|
|
||||||
|
/** The name of the local icon collection */
|
||||||
|
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||||
|
|
||||||
|
return unocss({
|
||||||
|
presets: [
|
||||||
|
presetIcons({
|
||||||
|
prefix: `${VITE_ICON_PREFIX}-`,
|
||||||
|
scale: 1,
|
||||||
|
extraProperties: {
|
||||||
|
display: 'inline-block'
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||||
|
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
warn: true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
47
build/plugins/unplugin.ts
Normal file
47
build/plugins/unplugin.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||||
|
|
||||||
|
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||||
|
|
||||||
|
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||||
|
|
||||||
|
/** The name of the local icon collection */
|
||||||
|
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||||
|
|
||||||
|
const plugins: PluginOption[] = [
|
||||||
|
Icons({
|
||||||
|
compiler: 'vue3',
|
||||||
|
customCollections: {
|
||||||
|
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||||
|
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
defaultClass: 'inline-block'
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
dts: 'src/typings/components.d.ts',
|
||||||
|
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||||
|
resolvers: [
|
||||||
|
NaiveUiResolver(),
|
||||||
|
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
iconDirs: [localIconPath],
|
||||||
|
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
|
||||||
|
inject: 'body-last',
|
||||||
|
customDomId: '__SVG_ICON_LOCAL__'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
24
eslint.config.js
Normal file
24
eslint.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from '@soybeanjs/eslint-config';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
{ vue: true, unocss: true },
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
ignores: ['index', 'App', 'Register', '[id]', '[url]']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'vue/component-name-in-template-casing': [
|
||||||
|
'warn',
|
||||||
|
'PascalCase',
|
||||||
|
{
|
||||||
|
registeredComponentsOnly: false,
|
||||||
|
ignores: ['/^icon-/']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'unocss/order-attributify': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-cmn-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
114
package.json
Normal file
114
package.json
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"name": "soybean-admin",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||||
|
"author": {
|
||||||
|
"name": "Soybean",
|
||||||
|
"email": "soybeanjs@outlook.com",
|
||||||
|
"url": "https://github.com/soybeanjs"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/soybeanjs/soybean-admin",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/soybeanjs/soybean-admin.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/soybeanjs/soybean-admin/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Vue3 admin ",
|
||||||
|
"vue-admin-template",
|
||||||
|
"Vite5",
|
||||||
|
"TypeScript",
|
||||||
|
"naive-ui",
|
||||||
|
"naive-ui-admin",
|
||||||
|
"ant-design-vue v4",
|
||||||
|
"UnoCSS"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.0",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --mode prod",
|
||||||
|
"build:test": "vite build --mode test",
|
||||||
|
"cleanup": "sa cleanup",
|
||||||
|
"commit": "sa git-commit",
|
||||||
|
"commit:zh": "sa git-commit -l=zh-cn",
|
||||||
|
"dev": "vite --mode test",
|
||||||
|
"dev:prod": "vite --mode prod",
|
||||||
|
"gen-route": "sa gen-route",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"prepare": "simple-git-hooks",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"release": "sa release",
|
||||||
|
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"update-pkg": "sa update-pkg"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@better-scroll/core": "2.5.1",
|
||||||
|
"@iconify/vue": "4.3.0",
|
||||||
|
"@sa/axios": "workspace:*",
|
||||||
|
"@sa/color": "workspace:*",
|
||||||
|
"@sa/hooks": "workspace:*",
|
||||||
|
"@sa/materials": "workspace:*",
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"@vueuse/core": "13.0.0",
|
||||||
|
"clipboard": "2.0.11",
|
||||||
|
"dayjs": "1.11.13",
|
||||||
|
"defu": "6.1.4",
|
||||||
|
"echarts": "5.6.0",
|
||||||
|
"json5": "2.2.3",
|
||||||
|
"naive-ui": "2.41.0",
|
||||||
|
"nprogress": "0.2.0",
|
||||||
|
"pinia": "3.0.1",
|
||||||
|
"tailwind-merge": "3.0.2",
|
||||||
|
"vue": "3.5.13",
|
||||||
|
"vue-draggable-plus": "0.6.0",
|
||||||
|
"vue-i18n": "11.1.2",
|
||||||
|
"vue-router": "4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@elegant-router/vue": "0.3.8",
|
||||||
|
"@iconify/json": "2.2.318",
|
||||||
|
"@sa/scripts": "workspace:*",
|
||||||
|
"@sa/uno-preset": "workspace:*",
|
||||||
|
"@soybeanjs/eslint-config": "1.6.0",
|
||||||
|
"@types/node": "22.13.10",
|
||||||
|
"@types/nprogress": "0.2.3",
|
||||||
|
"@unocss/eslint-config": "66.0.0",
|
||||||
|
"@unocss/preset-icons": "66.0.0",
|
||||||
|
"@unocss/preset-uno": "66.0.0",
|
||||||
|
"@unocss/transformer-directives": "66.0.0",
|
||||||
|
"@unocss/transformer-variant-group": "66.0.0",
|
||||||
|
"@unocss/vite": "66.0.0",
|
||||||
|
"@vitejs/plugin-vue": "5.2.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "4.1.2",
|
||||||
|
"consola": "3.4.2",
|
||||||
|
"eslint": "9.22.0",
|
||||||
|
"eslint-plugin-vue": "10.0.0",
|
||||||
|
"kolorist": "1.8.0",
|
||||||
|
"lint-staged": "15.5.0",
|
||||||
|
"sass": "1.86.0",
|
||||||
|
"simple-git-hooks": "2.11.1",
|
||||||
|
"tsx": "4.19.3",
|
||||||
|
"typescript": "5.8.2",
|
||||||
|
"unplugin-icons": "22.1.0",
|
||||||
|
"unplugin-vue-components": "28.4.1",
|
||||||
|
"vite": "6.2.2",
|
||||||
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
|
"vite-plugin-vue-devtools": "7.7.2",
|
||||||
|
"vue-eslint-parser": "10.1.1",
|
||||||
|
"vue-tsc": "2.2.8"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"commit-msg": "pnpm sa git-commit-verify",
|
||||||
|
"pre-commit": "pnpm typecheck && pnpm lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint --fix"
|
||||||
|
},
|
||||||
|
"website": "https://admin.soybeanjs.cn"
|
||||||
|
}
|
20
packages/alova/package.json
Normal file
20
packages/alova/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/alova",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./fetch": "./src/fetch.ts",
|
||||||
|
"./client": "./src/client.ts",
|
||||||
|
"./mock": "./src/mock.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@alova/mock": "2.0.12",
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"alova": "3.2.10"
|
||||||
|
}
|
||||||
|
}
|
1
packages/alova/src/client.ts
Normal file
1
packages/alova/src/client.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from 'alova/client';
|
2
packages/alova/src/constant.ts
Normal file
2
packages/alova/src/constant.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** the backend error code key */
|
||||||
|
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
2
packages/alova/src/fetch.ts
Normal file
2
packages/alova/src/fetch.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import adapterFetch from 'alova/fetch';
|
||||||
|
export default adapterFetch;
|
77
packages/alova/src/index.ts
Normal file
77
packages/alova/src/index.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { createAlova } from 'alova';
|
||||||
|
import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova';
|
||||||
|
import VueHook from 'alova/vue';
|
||||||
|
import type { VueHookType } from 'alova/vue';
|
||||||
|
import adapterFetch from 'alova/fetch';
|
||||||
|
import { createServerTokenAuthentication } from 'alova/client';
|
||||||
|
import type { FetchRequestInit } from 'alova/fetch';
|
||||||
|
import { BACKEND_ERROR_CODE } from './constant';
|
||||||
|
import type { CustomAlovaConfig, RequestOptions } from './type';
|
||||||
|
|
||||||
|
export const createAlovaRequest = <
|
||||||
|
RequestConfig = FetchRequestInit,
|
||||||
|
ResponseType = Response,
|
||||||
|
ResponseHeader = Headers,
|
||||||
|
L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter,
|
||||||
|
L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter
|
||||||
|
>(
|
||||||
|
customConfig: CustomAlovaConfig<
|
||||||
|
AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>
|
||||||
|
>,
|
||||||
|
options: RequestOptions<AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>>
|
||||||
|
) => {
|
||||||
|
const { tokenRefresher } = options;
|
||||||
|
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||||
|
VueHookType,
|
||||||
|
AlovaRequestAdapter<RequestConfig, ResponseType, ResponseHeader>
|
||||||
|
>({
|
||||||
|
refreshTokenOnSuccess: {
|
||||||
|
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||||
|
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||||
|
},
|
||||||
|
refreshTokenOnError: {
|
||||||
|
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||||
|
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const instance = createAlova({
|
||||||
|
...customConfig,
|
||||||
|
timeout: customConfig.timeout ?? 10 * 1000,
|
||||||
|
requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(),
|
||||||
|
statesHook: VueHook,
|
||||||
|
beforeRequest: onAuthRequired(options.onRequest as any),
|
||||||
|
responded: onResponseRefreshToken({
|
||||||
|
onSuccess: async (response, method) => {
|
||||||
|
// check if http status is success
|
||||||
|
let error: any = null;
|
||||||
|
let transformedData: any = null;
|
||||||
|
try {
|
||||||
|
if (await options.isBackendSuccess(response)) {
|
||||||
|
transformedData = await options.transformBackendResponse(response);
|
||||||
|
} else {
|
||||||
|
error = new Error('the backend request error');
|
||||||
|
error.code = BACKEND_ERROR_CODE;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
await options.onError?.(error, response, method);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedData;
|
||||||
|
},
|
||||||
|
onComplete: options.onComplete,
|
||||||
|
onError: (error, method) => options.onError?.(error, null, method)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BACKEND_ERROR_CODE };
|
||||||
|
export type * from './type';
|
||||||
|
export type * from 'alova';
|
1
packages/alova/src/mock.ts
Normal file
1
packages/alova/src/mock.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@alova/mock';
|
52
packages/alova/src/type.ts
Normal file
52
packages/alova/src/type.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova';
|
||||||
|
|
||||||
|
export type CustomAlovaConfig<AG extends AlovaGenerics> = Omit<
|
||||||
|
AlovaOptions<AG>,
|
||||||
|
'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'
|
||||||
|
> & {
|
||||||
|
/** request adapter. all request of alova will be sent by it. */
|
||||||
|
requestAdapter?: AlovaRequestAdapter<AG['RequestConfig'], AG['Response'], AG['ResponseHeader']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RequestOptions<AG extends AlovaGenerics> {
|
||||||
|
/**
|
||||||
|
* The hook before request
|
||||||
|
*
|
||||||
|
* For example: You can add header token in this hook
|
||||||
|
*
|
||||||
|
* @param method alova Method Instance
|
||||||
|
*/
|
||||||
|
onRequest?: AlovaOptions<AG>['beforeRequest'];
|
||||||
|
/**
|
||||||
|
* The hook to check backend response is success or not
|
||||||
|
*
|
||||||
|
* @param response alova response
|
||||||
|
*/
|
||||||
|
isBackendSuccess: (response: AG['Response']) => Promise<boolean>;
|
||||||
|
|
||||||
|
/** The config to refresh token */
|
||||||
|
tokenRefresher?: {
|
||||||
|
/** detect the token is expired */
|
||||||
|
isExpired(response: AG['Response'], Method: Method<AG>): Promise<boolean> | boolean;
|
||||||
|
/** refresh token handler */
|
||||||
|
handler(response: AG['Response'], Method: Method<AG>): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The hook after backend request complete */
|
||||||
|
onComplete?: ResponseCompleteHandler<AG>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook to handle error
|
||||||
|
*
|
||||||
|
* For example: You can show error message in this hook
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
onError?: (error: any, response: AG['Response'] | null, methodInstance: Method<AG>) => any | Promise<any>;
|
||||||
|
/**
|
||||||
|
* transform backend response when the responseType is json
|
||||||
|
*
|
||||||
|
* @param response alova response
|
||||||
|
*/
|
||||||
|
transformBackendResponse: (response: AG['Response']) => any;
|
||||||
|
}
|
20
packages/alova/tsconfig.json
Normal file
20
packages/alova/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
21
packages/axios/package.json
Normal file
21
packages/axios/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/axios",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"axios": "1.8.3",
|
||||||
|
"axios-retry": "4.5.0",
|
||||||
|
"qs": "6.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/qs": "6.9.18"
|
||||||
|
}
|
||||||
|
}
|
5
packages/axios/src/constant.ts
Normal file
5
packages/axios/src/constant.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/** request id key */
|
||||||
|
export const REQUEST_ID_KEY = 'X-Request-Id';
|
||||||
|
|
||||||
|
/** the backend error code key */
|
||||||
|
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
183
packages/axios/src/index.ts
Normal file
183
packages/axios/src/index.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import axiosRetry from 'axios-retry';
|
||||||
|
import { nanoid } from '@sa/utils';
|
||||||
|
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
|
||||||
|
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
|
||||||
|
import type {
|
||||||
|
CustomAxiosRequestConfig,
|
||||||
|
FlatRequestInstance,
|
||||||
|
MappedType,
|
||||||
|
RequestInstance,
|
||||||
|
RequestOption,
|
||||||
|
ResponseType
|
||||||
|
} from './type';
|
||||||
|
|
||||||
|
function createCommonRequest<ResponseData = any>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData>>
|
||||||
|
) {
|
||||||
|
const opts = createDefaultOptions<ResponseData>(options);
|
||||||
|
|
||||||
|
const axiosConf = createAxiosConfig(axiosConfig);
|
||||||
|
const instance = axios.create(axiosConf);
|
||||||
|
|
||||||
|
const abortControllerMap = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
// config axios retry
|
||||||
|
const retryOptions = createRetryOptions(axiosConf);
|
||||||
|
axiosRetry(instance, retryOptions);
|
||||||
|
|
||||||
|
instance.interceptors.request.use(conf => {
|
||||||
|
const config: InternalAxiosRequestConfig = { ...conf };
|
||||||
|
|
||||||
|
// set request id
|
||||||
|
const requestId = nanoid();
|
||||||
|
config.headers.set(REQUEST_ID_KEY, requestId);
|
||||||
|
|
||||||
|
// config abort controller
|
||||||
|
if (!config.signal) {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
config.signal = abortController.signal;
|
||||||
|
abortControllerMap.set(requestId, abortController);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle config by hook
|
||||||
|
const handledConfig = opts.onRequest?.(config) || config;
|
||||||
|
|
||||||
|
return handledConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
async response => {
|
||||||
|
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
||||||
|
|
||||||
|
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
|
||||||
|
return Promise.resolve(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fail = await opts.onBackendFail(response, instance);
|
||||||
|
if (fail) {
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendError = new AxiosError<ResponseData>(
|
||||||
|
'the backend request error',
|
||||||
|
BACKEND_ERROR_CODE,
|
||||||
|
response.config,
|
||||||
|
response.request,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
await opts.onError(backendError);
|
||||||
|
|
||||||
|
return Promise.reject(backendError);
|
||||||
|
},
|
||||||
|
async (error: AxiosError<ResponseData>) => {
|
||||||
|
await opts.onError(error);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function cancelRequest(requestId: string) {
|
||||||
|
const abortController = abortControllerMap.get(requestId);
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
abortControllerMap.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelAllRequest() {
|
||||||
|
abortControllerMap.forEach(abortController => {
|
||||||
|
abortController.abort();
|
||||||
|
});
|
||||||
|
abortControllerMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
opts,
|
||||||
|
cancelRequest,
|
||||||
|
cancelAllRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a request instance
|
||||||
|
*
|
||||||
|
* @param axiosConfig axios config
|
||||||
|
* @param options request options
|
||||||
|
*/
|
||||||
|
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData>>
|
||||||
|
) {
|
||||||
|
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||||
|
|
||||||
|
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig
|
||||||
|
) {
|
||||||
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
|
if (responseType === 'json') {
|
||||||
|
return opts.transformBackendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as MappedType<R, T>;
|
||||||
|
} as RequestInstance<State>;
|
||||||
|
|
||||||
|
request.cancelRequest = cancelRequest;
|
||||||
|
request.cancelAllRequest = cancelAllRequest;
|
||||||
|
request.state = {} as State;
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a flat request instance
|
||||||
|
*
|
||||||
|
* The response data is a flat object: { data: any, error: AxiosError }
|
||||||
|
*
|
||||||
|
* @param axiosConfig axios config
|
||||||
|
* @param options request options
|
||||||
|
*/
|
||||||
|
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData>>
|
||||||
|
) {
|
||||||
|
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||||
|
|
||||||
|
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
||||||
|
T = any,
|
||||||
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
|
if (responseType === 'json') {
|
||||||
|
const data = opts.transformBackendResponse(response);
|
||||||
|
|
||||||
|
return { data, error: null, response };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: response.data as MappedType<R, T>, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||||
|
}
|
||||||
|
} as FlatRequestInstance<State, ResponseData>;
|
||||||
|
|
||||||
|
flatRequest.cancelRequest = cancelRequest;
|
||||||
|
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||||
|
flatRequest.state = {} as State;
|
||||||
|
|
||||||
|
return flatRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
|
||||||
|
export type * from './type';
|
||||||
|
export type { CreateAxiosDefaults, AxiosError };
|
48
packages/axios/src/options.ts
Normal file
48
packages/axios/src/options.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { CreateAxiosDefaults } from 'axios';
|
||||||
|
import type { IAxiosRetryConfig } from 'axios-retry';
|
||||||
|
import { stringify } from 'qs';
|
||||||
|
import { isHttpSuccess } from './shared';
|
||||||
|
import type { RequestOption } from './type';
|
||||||
|
|
||||||
|
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
||||||
|
const opts: RequestOption<ResponseData> = {
|
||||||
|
onRequest: async config => config,
|
||||||
|
isBackendSuccess: _response => true,
|
||||||
|
onBackendFail: async () => {},
|
||||||
|
transformBackendResponse: async response => response.data,
|
||||||
|
onError: async () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(opts, options);
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
|
||||||
|
const retryConfig: IAxiosRetryConfig = {
|
||||||
|
retries: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(retryConfig, config);
|
||||||
|
|
||||||
|
return retryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
|
||||||
|
const TEN_SECONDS = 10 * 1000;
|
||||||
|
|
||||||
|
const axiosConfig: CreateAxiosDefaults = {
|
||||||
|
timeout: TEN_SECONDS,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
validateStatus: isHttpSuccess,
|
||||||
|
paramsSerializer: params => {
|
||||||
|
return stringify(params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(axiosConfig, config);
|
||||||
|
|
||||||
|
return axiosConfig;
|
||||||
|
}
|
28
packages/axios/src/shared.ts
Normal file
28
packages/axios/src/shared.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
export function getContentType(config: InternalAxiosRequestConfig) {
|
||||||
|
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
|
||||||
|
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if http status is success
|
||||||
|
*
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
export function isHttpSuccess(status: number) {
|
||||||
|
const isSuccessCode = status >= 200 && status < 300;
|
||||||
|
return isSuccessCode || status === 304;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is response json
|
||||||
|
*
|
||||||
|
* @param response axios response
|
||||||
|
*/
|
||||||
|
export function isResponseJson(response: AxiosResponse) {
|
||||||
|
const { responseType } = response.config;
|
||||||
|
|
||||||
|
return responseType === 'json' || responseType === undefined;
|
||||||
|
}
|
115
packages/axios/src/type.ts
Normal file
115
packages/axios/src/type.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
export type ContentType =
|
||||||
|
| 'text/html'
|
||||||
|
| 'text/plain'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/json'
|
||||||
|
| 'application/x-www-form-urlencoded'
|
||||||
|
| 'application/octet-stream';
|
||||||
|
|
||||||
|
export interface RequestOption<ResponseData = any> {
|
||||||
|
/**
|
||||||
|
* The hook before request
|
||||||
|
*
|
||||||
|
* For example: You can add header token in this hook
|
||||||
|
*
|
||||||
|
* @param config Axios config
|
||||||
|
*/
|
||||||
|
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
||||||
|
/**
|
||||||
|
* The hook to check backend response is success or not
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
|
||||||
|
/**
|
||||||
|
* The hook after backend request fail
|
||||||
|
*
|
||||||
|
* For example: You can handle the expired token in this hook
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
* @param instance Axios instance
|
||||||
|
*/
|
||||||
|
onBackendFail: (
|
||||||
|
response: AxiosResponse<ResponseData>,
|
||||||
|
instance: AxiosInstance
|
||||||
|
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||||
|
/**
|
||||||
|
* transform backend response when the responseType is json
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
||||||
|
/**
|
||||||
|
* The hook to handle error
|
||||||
|
*
|
||||||
|
* For example: You can show error message in this hook
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseMap {
|
||||||
|
blob: Blob;
|
||||||
|
text: string;
|
||||||
|
arrayBuffer: ArrayBuffer;
|
||||||
|
stream: ReadableStream<Uint8Array>;
|
||||||
|
document: Document;
|
||||||
|
}
|
||||||
|
export type ResponseType = keyof ResponseMap | 'json';
|
||||||
|
|
||||||
|
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
|
||||||
|
? ResponseMap[R]
|
||||||
|
: JsonType;
|
||||||
|
|
||||||
|
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
|
||||||
|
responseType?: R;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RequestInstanceCommon<T> {
|
||||||
|
/**
|
||||||
|
* cancel the request by request id
|
||||||
|
*
|
||||||
|
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
*/
|
||||||
|
cancelRequest: (requestId: string) => void;
|
||||||
|
/**
|
||||||
|
* cancel all request
|
||||||
|
*
|
||||||
|
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||||
|
*/
|
||||||
|
cancelAllRequest: () => void;
|
||||||
|
/** you can set custom state in the request instance */
|
||||||
|
state: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The request instance */
|
||||||
|
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
||||||
|
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
||||||
|
data: T;
|
||||||
|
error: null;
|
||||||
|
response: AxiosResponse<ResponseData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlatResponseFailData<ResponseData = any> = {
|
||||||
|
data: null;
|
||||||
|
error: AxiosError<ResponseData>;
|
||||||
|
response: AxiosResponse<ResponseData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlatResponseData<T = any, ResponseData = any> =
|
||||||
|
| FlatResponseSuccessData<T, ResponseData>
|
||||||
|
| FlatResponseFailData<ResponseData>;
|
||||||
|
|
||||||
|
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
||||||
|
<T = any, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig<R>
|
||||||
|
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
||||||
|
}
|
20
packages/axios/tsconfig.json
Normal file
20
packages/axios/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
16
packages/color/package.json
Normal file
16
packages/color/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/color",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"colord": "2.9.3"
|
||||||
|
}
|
||||||
|
}
|
2
packages/color/src/constant/index.ts
Normal file
2
packages/color/src/constant/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './name';
|
||||||
|
export * from './palette';
|
1579
packages/color/src/constant/name.ts
Normal file
1579
packages/color/src/constant/name.ts
Normal file
File diff suppressed because it is too large
Load Diff
356
packages/color/src/constant/palette.ts
Normal file
356
packages/color/src/constant/palette.ts
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
import type { ColorPaletteFamily } from '../types';
|
||||||
|
|
||||||
|
export const colorPalettes: ColorPaletteFamily[] = [
|
||||||
|
{
|
||||||
|
name: 'Slate',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f8fafc', number: 50 },
|
||||||
|
{ hex: '#f1f5f9', number: 100 },
|
||||||
|
{ hex: '#e2e8f0', number: 200 },
|
||||||
|
{ hex: '#cbd5e1', number: 300 },
|
||||||
|
{ hex: '#94a3b8', number: 400 },
|
||||||
|
{ hex: '#64748b', number: 500 },
|
||||||
|
{ hex: '#475569', number: 600 },
|
||||||
|
{ hex: '#334155', number: 700 },
|
||||||
|
{ hex: '#1e293b', number: 800 },
|
||||||
|
{ hex: '#0f172a', number: 900 },
|
||||||
|
{ hex: '#020617', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gray',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f9fafb', number: 50 },
|
||||||
|
{ hex: '#f3f4f6', number: 100 },
|
||||||
|
{ hex: '#e5e7eb', number: 200 },
|
||||||
|
{ hex: '#d1d5db', number: 300 },
|
||||||
|
{ hex: '#9ca3af', number: 400 },
|
||||||
|
{ hex: '#6b7280', number: 500 },
|
||||||
|
{ hex: '#4b5563', number: 600 },
|
||||||
|
{ hex: '#374151', number: 700 },
|
||||||
|
{ hex: '#1f2937', number: 800 },
|
||||||
|
{ hex: '#111827', number: 900 },
|
||||||
|
{ hex: '#030712', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Zinc',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafafa', number: 50 },
|
||||||
|
{ hex: '#f4f4f5', number: 100 },
|
||||||
|
{ hex: '#e4e4e7', number: 200 },
|
||||||
|
{ hex: '#d4d4d8', number: 300 },
|
||||||
|
{ hex: '#a1a1aa', number: 400 },
|
||||||
|
{ hex: '#71717a', number: 500 },
|
||||||
|
{ hex: '#52525b', number: 600 },
|
||||||
|
{ hex: '#3f3f46', number: 700 },
|
||||||
|
{ hex: '#27272a', number: 800 },
|
||||||
|
{ hex: '#18181b', number: 900 },
|
||||||
|
{ hex: '#09090b', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Neutral',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafafa', number: 50 },
|
||||||
|
{ hex: '#f5f5f5', number: 100 },
|
||||||
|
{ hex: '#e5e5e5', number: 200 },
|
||||||
|
{ hex: '#d4d4d4', number: 300 },
|
||||||
|
{ hex: '#a3a3a3', number: 400 },
|
||||||
|
{ hex: '#737373', number: 500 },
|
||||||
|
{ hex: '#525252', number: 600 },
|
||||||
|
{ hex: '#404040', number: 700 },
|
||||||
|
{ hex: '#262626', number: 800 },
|
||||||
|
{ hex: '#171717', number: 900 },
|
||||||
|
{ hex: '#0a0a0a', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stone',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafaf9', number: 50 },
|
||||||
|
{ hex: '#f5f5f4', number: 100 },
|
||||||
|
{ hex: '#e7e5e4', number: 200 },
|
||||||
|
{ hex: '#d6d3d1', number: 300 },
|
||||||
|
{ hex: '#a8a29e', number: 400 },
|
||||||
|
{ hex: '#78716c', number: 500 },
|
||||||
|
{ hex: '#57534e', number: 600 },
|
||||||
|
{ hex: '#44403c', number: 700 },
|
||||||
|
{ hex: '#292524', number: 800 },
|
||||||
|
{ hex: '#1c1917', number: 900 },
|
||||||
|
{ hex: '#0c0a09', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Red',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fef2f2', number: 50 },
|
||||||
|
{ hex: '#fee2e2', number: 100 },
|
||||||
|
{ hex: '#fecaca', number: 200 },
|
||||||
|
{ hex: '#fca5a5', number: 300 },
|
||||||
|
{ hex: '#f87171', number: 400 },
|
||||||
|
{ hex: '#ef4444', number: 500 },
|
||||||
|
{ hex: '#dc2626', number: 600 },
|
||||||
|
{ hex: '#b91c1c', number: 700 },
|
||||||
|
{ hex: '#991b1b', number: 800 },
|
||||||
|
{ hex: '#7f1d1d', number: 900 },
|
||||||
|
{ hex: '#450a0a', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Orange',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fff7ed', number: 50 },
|
||||||
|
{ hex: '#ffedd5', number: 100 },
|
||||||
|
{ hex: '#fed7aa', number: 200 },
|
||||||
|
{ hex: '#fdba74', number: 300 },
|
||||||
|
{ hex: '#fb923c', number: 400 },
|
||||||
|
{ hex: '#f97316', number: 500 },
|
||||||
|
{ hex: '#ea580c', number: 600 },
|
||||||
|
{ hex: '#c2410c', number: 700 },
|
||||||
|
{ hex: '#9a3412', number: 800 },
|
||||||
|
{ hex: '#7c2d12', number: 900 },
|
||||||
|
{ hex: '#431407', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Amber',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fffbeb', number: 50 },
|
||||||
|
{ hex: '#fef3c7', number: 100 },
|
||||||
|
{ hex: '#fde68a', number: 200 },
|
||||||
|
{ hex: '#fcd34d', number: 300 },
|
||||||
|
{ hex: '#fbbf24', number: 400 },
|
||||||
|
{ hex: '#f59e0b', number: 500 },
|
||||||
|
{ hex: '#d97706', number: 600 },
|
||||||
|
{ hex: '#b45309', number: 700 },
|
||||||
|
{ hex: '#92400e', number: 800 },
|
||||||
|
{ hex: '#78350f', number: 900 },
|
||||||
|
{ hex: '#451a03', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yellow',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fefce8', number: 50 },
|
||||||
|
{ hex: '#fef9c3', number: 100 },
|
||||||
|
{ hex: '#fef08a', number: 200 },
|
||||||
|
{ hex: '#fde047', number: 300 },
|
||||||
|
{ hex: '#facc15', number: 400 },
|
||||||
|
{ hex: '#eab308', number: 500 },
|
||||||
|
{ hex: '#ca8a04', number: 600 },
|
||||||
|
{ hex: '#a16207', number: 700 },
|
||||||
|
{ hex: '#854d0e', number: 800 },
|
||||||
|
{ hex: '#713f12', number: 900 },
|
||||||
|
{ hex: '#422006', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lime',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f7fee7', number: 50 },
|
||||||
|
{ hex: '#ecfccb', number: 100 },
|
||||||
|
{ hex: '#d9f99d', number: 200 },
|
||||||
|
{ hex: '#bef264', number: 300 },
|
||||||
|
{ hex: '#a3e635', number: 400 },
|
||||||
|
{ hex: '#84cc16', number: 500 },
|
||||||
|
{ hex: '#65a30d', number: 600 },
|
||||||
|
{ hex: '#4d7c0f', number: 700 },
|
||||||
|
{ hex: '#3f6212', number: 800 },
|
||||||
|
{ hex: '#365314', number: 900 },
|
||||||
|
{ hex: '#1a2e05', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Green',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0fdf4', number: 50 },
|
||||||
|
{ hex: '#dcfce7', number: 100 },
|
||||||
|
{ hex: '#bbf7d0', number: 200 },
|
||||||
|
{ hex: '#86efac', number: 300 },
|
||||||
|
{ hex: '#4ade80', number: 400 },
|
||||||
|
{ hex: '#22c55e', number: 500 },
|
||||||
|
{ hex: '#16a34a', number: 600 },
|
||||||
|
{ hex: '#15803d', number: 700 },
|
||||||
|
{ hex: '#166534', number: 800 },
|
||||||
|
{ hex: '#14532d', number: 900 },
|
||||||
|
{ hex: '#052e16', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Emerald',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#ecfdf5', number: 50 },
|
||||||
|
{ hex: '#d1fae5', number: 100 },
|
||||||
|
{ hex: '#a7f3d0', number: 200 },
|
||||||
|
{ hex: '#6ee7b7', number: 300 },
|
||||||
|
{ hex: '#34d399', number: 400 },
|
||||||
|
{ hex: '#10b981', number: 500 },
|
||||||
|
{ hex: '#059669', number: 600 },
|
||||||
|
{ hex: '#047857', number: 700 },
|
||||||
|
{ hex: '#065f46', number: 800 },
|
||||||
|
{ hex: '#064e3b', number: 900 },
|
||||||
|
{ hex: '#022c22', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Teal',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0fdfa', number: 50 },
|
||||||
|
{ hex: '#ccfbf1', number: 100 },
|
||||||
|
{ hex: '#99f6e4', number: 200 },
|
||||||
|
{ hex: '#5eead4', number: 300 },
|
||||||
|
{ hex: '#2dd4bf', number: 400 },
|
||||||
|
{ hex: '#14b8a6', number: 500 },
|
||||||
|
{ hex: '#0d9488', number: 600 },
|
||||||
|
{ hex: '#0f766e', number: 700 },
|
||||||
|
{ hex: '#115e59', number: 800 },
|
||||||
|
{ hex: '#134e4a', number: 900 },
|
||||||
|
{ hex: '#042f2e', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cyan',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#ecfeff', number: 50 },
|
||||||
|
{ hex: '#cffafe', number: 100 },
|
||||||
|
{ hex: '#a5f3fc', number: 200 },
|
||||||
|
{ hex: '#67e8f9', number: 300 },
|
||||||
|
{ hex: '#22d3ee', number: 400 },
|
||||||
|
{ hex: '#06b6d4', number: 500 },
|
||||||
|
{ hex: '#0891b2', number: 600 },
|
||||||
|
{ hex: '#0e7490', number: 700 },
|
||||||
|
{ hex: '#155e75', number: 800 },
|
||||||
|
{ hex: '#164e63', number: 900 },
|
||||||
|
{ hex: '#083344', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sky',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0f9ff', number: 50 },
|
||||||
|
{ hex: '#e0f2fe', number: 100 },
|
||||||
|
{ hex: '#bae6fd', number: 200 },
|
||||||
|
{ hex: '#7dd3fc', number: 300 },
|
||||||
|
{ hex: '#38bdf8', number: 400 },
|
||||||
|
{ hex: '#0ea5e9', number: 500 },
|
||||||
|
{ hex: '#0284c7', number: 600 },
|
||||||
|
{ hex: '#0369a1', number: 700 },
|
||||||
|
{ hex: '#075985', number: 800 },
|
||||||
|
{ hex: '#0c4a6e', number: 900 },
|
||||||
|
{ hex: '#082f49', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Blue',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#eff6ff', number: 50 },
|
||||||
|
{ hex: '#dbeafe', number: 100 },
|
||||||
|
{ hex: '#bfdbfe', number: 200 },
|
||||||
|
{ hex: '#93c5fd', number: 300 },
|
||||||
|
{ hex: '#60a5fa', number: 400 },
|
||||||
|
{ hex: '#3b82f6', number: 500 },
|
||||||
|
{ hex: '#2563eb', number: 600 },
|
||||||
|
{ hex: '#1d4ed8', number: 700 },
|
||||||
|
{ hex: '#1e40af', number: 800 },
|
||||||
|
{ hex: '#1e3a8a', number: 900 },
|
||||||
|
{ hex: '#172554', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Indigo',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#eef2ff', number: 50 },
|
||||||
|
{ hex: '#e0e7ff', number: 100 },
|
||||||
|
{ hex: '#c7d2fe', number: 200 },
|
||||||
|
{ hex: '#a5b4fc', number: 300 },
|
||||||
|
{ hex: '#818cf8', number: 400 },
|
||||||
|
{ hex: '#6366f1', number: 500 },
|
||||||
|
{ hex: '#4f46e5', number: 600 },
|
||||||
|
{ hex: '#4338ca', number: 700 },
|
||||||
|
{ hex: '#3730a3', number: 800 },
|
||||||
|
{ hex: '#312e81', number: 900 },
|
||||||
|
{ hex: '#1e1b4b', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Violet',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f5f3ff', number: 50 },
|
||||||
|
{ hex: '#ede9fe', number: 100 },
|
||||||
|
{ hex: '#ddd6fe', number: 200 },
|
||||||
|
{ hex: '#c4b5fd', number: 300 },
|
||||||
|
{ hex: '#a78bfa', number: 400 },
|
||||||
|
{ hex: '#8b5cf6', number: 500 },
|
||||||
|
{ hex: '#7c3aed', number: 600 },
|
||||||
|
{ hex: '#6d28d9', number: 700 },
|
||||||
|
{ hex: '#5b21b6', number: 800 },
|
||||||
|
{ hex: '#4c1d95', number: 900 },
|
||||||
|
{ hex: '#2e1065', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Purple',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#faf5ff', number: 50 },
|
||||||
|
{ hex: '#f3e8ff', number: 100 },
|
||||||
|
{ hex: '#e9d5ff', number: 200 },
|
||||||
|
{ hex: '#d8b4fe', number: 300 },
|
||||||
|
{ hex: '#c084fc', number: 400 },
|
||||||
|
{ hex: '#a855f7', number: 500 },
|
||||||
|
{ hex: '#9333ea', number: 600 },
|
||||||
|
{ hex: '#7e22ce', number: 700 },
|
||||||
|
{ hex: '#6b21a8', number: 800 },
|
||||||
|
{ hex: '#581c87', number: 900 },
|
||||||
|
{ hex: '#3b0764', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fuchsia',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fdf4ff', number: 50 },
|
||||||
|
{ hex: '#fae8ff', number: 100 },
|
||||||
|
{ hex: '#f5d0fe', number: 200 },
|
||||||
|
{ hex: '#f0abfc', number: 300 },
|
||||||
|
{ hex: '#e879f9', number: 400 },
|
||||||
|
{ hex: '#d946ef', number: 500 },
|
||||||
|
{ hex: '#c026d3', number: 600 },
|
||||||
|
{ hex: '#a21caf', number: 700 },
|
||||||
|
{ hex: '#86198f', number: 800 },
|
||||||
|
{ hex: '#701a75', number: 900 },
|
||||||
|
{ hex: '#4a044e', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pink',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fdf2f8', number: 50 },
|
||||||
|
{ hex: '#fce7f3', number: 100 },
|
||||||
|
{ hex: '#fbcfe8', number: 200 },
|
||||||
|
{ hex: '#f9a8d4', number: 300 },
|
||||||
|
{ hex: '#f472b6', number: 400 },
|
||||||
|
{ hex: '#ec4899', number: 500 },
|
||||||
|
{ hex: '#db2777', number: 600 },
|
||||||
|
{ hex: '#be185d', number: 700 },
|
||||||
|
{ hex: '#9d174d', number: 800 },
|
||||||
|
{ hex: '#831843', number: 900 },
|
||||||
|
{ hex: '#500724', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rose',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fff1f2', number: 50 },
|
||||||
|
{ hex: '#ffe4e6', number: 100 },
|
||||||
|
{ hex: '#fecdd3', number: 200 },
|
||||||
|
{ hex: '#fda4af', number: 300 },
|
||||||
|
{ hex: '#fb7185', number: 400 },
|
||||||
|
{ hex: '#f43f5e', number: 500 },
|
||||||
|
{ hex: '#e11d48', number: 600 },
|
||||||
|
{ hex: '#be123c', number: 700 },
|
||||||
|
{ hex: '#9f1239', number: 800 },
|
||||||
|
{ hex: '#881337', number: 900 },
|
||||||
|
{ hex: '#4c0519', number: 950 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
7
packages/color/src/index.ts
Normal file
7
packages/color/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { colorPalettes } from './constant';
|
||||||
|
|
||||||
|
export * from './palette';
|
||||||
|
export * from './shared';
|
||||||
|
export { colorPalettes };
|
||||||
|
|
||||||
|
export * from './types';
|
176
packages/color/src/palette/antd.ts
Normal file
176
packages/color/src/palette/antd.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import type { AnyColor, HsvColor } from 'colord';
|
||||||
|
import { getHex, getHsv, isValidColor, mixColor } from '../shared';
|
||||||
|
import type { ColorIndex } from '../types';
|
||||||
|
|
||||||
|
/** Hue step */
|
||||||
|
const hueStep = 2;
|
||||||
|
/** Saturation step, light color part */
|
||||||
|
const saturationStep = 16;
|
||||||
|
/** Saturation step, dark color part */
|
||||||
|
const saturationStep2 = 5;
|
||||||
|
/** Brightness step, light color part */
|
||||||
|
const brightnessStep1 = 5;
|
||||||
|
/** Brightness step, dark color part */
|
||||||
|
const brightnessStep2 = 15;
|
||||||
|
/** Light color count, main color up */
|
||||||
|
const lightColorCount = 5;
|
||||||
|
/** Dark color count, main color down */
|
||||||
|
const darkColorCount = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AntD palette color by index
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param index - The color index of color palette (the main color index is 6)
|
||||||
|
* @returns Hex color
|
||||||
|
*/
|
||||||
|
export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string {
|
||||||
|
if (!isValidColor(color)) {
|
||||||
|
throw new Error('invalid input color value');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 6) {
|
||||||
|
return getHex(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = index < 6;
|
||||||
|
const hsv = getHsv(color);
|
||||||
|
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
|
||||||
|
|
||||||
|
const newHsv: HsvColor = {
|
||||||
|
h: getHue(hsv, i, isLight),
|
||||||
|
s: getSaturation(hsv, i, isLight),
|
||||||
|
v: getValue(hsv, i, isLight)
|
||||||
|
};
|
||||||
|
|
||||||
|
return getHex(newHsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map of dark color index and opacity */
|
||||||
|
const darkColorMap = [
|
||||||
|
{ index: 7, opacity: 0.15 },
|
||||||
|
{ index: 6, opacity: 0.25 },
|
||||||
|
{ index: 5, opacity: 0.3 },
|
||||||
|
{ index: 5, opacity: 0.45 },
|
||||||
|
{ index: 5, opacity: 0.65 },
|
||||||
|
{ index: 5, opacity: 0.85 },
|
||||||
|
{ index: 5, opacity: 0.9 },
|
||||||
|
{ index: 4, opacity: 0.93 },
|
||||||
|
{ index: 3, opacity: 0.95 },
|
||||||
|
{ index: 2, opacity: 0.97 },
|
||||||
|
{ index: 1, opacity: 0.98 }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AntD color palette
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param darkTheme - Dark theme
|
||||||
|
* @param darkThemeMixColor - Dark theme mix color (default: #141414)
|
||||||
|
*/
|
||||||
|
export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
|
||||||
|
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
|
||||||
|
const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index));
|
||||||
|
|
||||||
|
if (darkTheme) {
|
||||||
|
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
|
||||||
|
const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity);
|
||||||
|
|
||||||
|
return darkColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
return darkPatterns.map(item => getHex(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hue
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
let hue: number;
|
||||||
|
|
||||||
|
const hsvH = Math.round(hsv.h);
|
||||||
|
|
||||||
|
if (hsvH >= 60 && hsvH <= 240) {
|
||||||
|
hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
|
||||||
|
} else {
|
||||||
|
hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hue < 0) {
|
||||||
|
hue += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hue >= 360) {
|
||||||
|
hue -= 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get saturation
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
if (hsv.h === 0 && hsv.s === 0) {
|
||||||
|
return hsv.s;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saturation: number;
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
saturation = hsv.s - saturationStep * i;
|
||||||
|
} else if (i === darkColorCount) {
|
||||||
|
saturation = hsv.s + saturationStep;
|
||||||
|
} else {
|
||||||
|
saturation = hsv.s + saturationStep2 * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saturation > 100) {
|
||||||
|
saturation = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLight && i === lightColorCount && saturation > 10) {
|
||||||
|
saturation = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saturation < 6) {
|
||||||
|
saturation = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return saturation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of hsv
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
let value: number;
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
value = hsv.v + brightnessStep1 * i;
|
||||||
|
} else {
|
||||||
|
value = hsv.v - brightnessStep2 * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > 100) {
|
||||||
|
value = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
45
packages/color/src/palette/index.ts
Normal file
45
packages/color/src/palette/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { AnyColor } from 'colord';
|
||||||
|
import { getHex } from '../shared';
|
||||||
|
import type { ColorPaletteNumber } from '../types';
|
||||||
|
import { getRecommendedColorPalette } from './recommend';
|
||||||
|
import { getAntDColorPalette } from './antd';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette by provided color
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
|
||||||
|
*/
|
||||||
|
export function getColorPalette(color: AnyColor, recommended = false) {
|
||||||
|
const colorMap = new Map<ColorPaletteNumber, string>();
|
||||||
|
|
||||||
|
if (recommended) {
|
||||||
|
const colorPalette = getRecommendedColorPalette(getHex(color));
|
||||||
|
colorPalette.palettes.forEach(palette => {
|
||||||
|
colorMap.set(palette.number, palette.hex);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const colors = getAntDColorPalette(color);
|
||||||
|
|
||||||
|
const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
||||||
|
|
||||||
|
colorNumbers.forEach((number, index) => {
|
||||||
|
colorMap.set(number, colors[index]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette color by number
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
* @param number the color palette number
|
||||||
|
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
|
||||||
|
*/
|
||||||
|
export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) {
|
||||||
|
const colorMap = getColorPalette(color, recommended);
|
||||||
|
|
||||||
|
return colorMap.get(number as ColorPaletteNumber)!;
|
||||||
|
}
|
152
packages/color/src/palette/recommend.ts
Normal file
152
packages/color/src/palette/recommend.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared';
|
||||||
|
import { colorPalettes } from '../constant';
|
||||||
|
import type {
|
||||||
|
ColorPalette,
|
||||||
|
ColorPaletteFamily,
|
||||||
|
ColorPaletteFamilyWithNearestPalette,
|
||||||
|
ColorPaletteMatch,
|
||||||
|
ColorPaletteNumber
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get recommended color palette by provided color
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
*/
|
||||||
|
export function getRecommendedColorPalette(color: string) {
|
||||||
|
const colorPaletteFamily = getRecommendedColorPaletteFamily(color);
|
||||||
|
|
||||||
|
const colorMap = new Map<ColorPaletteNumber, ColorPalette>();
|
||||||
|
|
||||||
|
colorPaletteFamily.palettes.forEach(palette => {
|
||||||
|
colorMap.set(palette.number, palette);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainColor = colorMap.get(500)!;
|
||||||
|
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!;
|
||||||
|
|
||||||
|
const colorPalette: ColorPaletteMatch = {
|
||||||
|
...colorPaletteFamily,
|
||||||
|
colorMap,
|
||||||
|
main: mainColor,
|
||||||
|
match: matchColor
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get recommended palette color by provided color
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
* @param number the color palette number
|
||||||
|
*/
|
||||||
|
export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) {
|
||||||
|
const colorPalette = getRecommendedColorPalette(color);
|
||||||
|
|
||||||
|
const { hex } = colorPalette.colorMap.get(number)!;
|
||||||
|
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette family by provided color and color name
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
*/
|
||||||
|
export function getRecommendedColorPaletteFamily(color: string) {
|
||||||
|
if (!isValidColor(color)) {
|
||||||
|
throw new Error('Invalid color, please check color value!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let colorName = getColorName(color);
|
||||||
|
|
||||||
|
colorName = colorName.toLowerCase().replace(/\s/g, '-');
|
||||||
|
|
||||||
|
const { h: h1, s: s1 } = getHsl(color);
|
||||||
|
|
||||||
|
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes);
|
||||||
|
|
||||||
|
const { number, hex } = nearestLightnessPalette;
|
||||||
|
|
||||||
|
const { h: h2, s: s2 } = getHsl(hex);
|
||||||
|
|
||||||
|
const deltaH = h1 - h2;
|
||||||
|
|
||||||
|
const sRatio = s1 / s2;
|
||||||
|
|
||||||
|
const colorPaletteFamily: ColorPaletteFamily = {
|
||||||
|
name: colorName,
|
||||||
|
palettes: palettes.map(palette => {
|
||||||
|
let hexValue = color;
|
||||||
|
|
||||||
|
const isSame = number === palette.number;
|
||||||
|
|
||||||
|
if (!isSame) {
|
||||||
|
const { h: h3, s: s3, l } = getHsl(palette.hex);
|
||||||
|
|
||||||
|
const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH;
|
||||||
|
const newS = s3 * sRatio;
|
||||||
|
|
||||||
|
hexValue = transformHslToHex({
|
||||||
|
h: newH,
|
||||||
|
s: newS,
|
||||||
|
l
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hex: hexValue,
|
||||||
|
number: palette.number
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorPaletteFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get nearest color palette family
|
||||||
|
*
|
||||||
|
* @param color color
|
||||||
|
* @param families color palette families
|
||||||
|
*/
|
||||||
|
function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
|
||||||
|
const familyWithConfig = families.map(family => {
|
||||||
|
const palettes = family.palettes.map(palette => {
|
||||||
|
return {
|
||||||
|
...palette,
|
||||||
|
delta: getDeltaE(color, palette.hex)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...family,
|
||||||
|
palettes,
|
||||||
|
nearestPalette
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
|
||||||
|
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
|
||||||
|
);
|
||||||
|
|
||||||
|
const { l } = getHsl(color);
|
||||||
|
|
||||||
|
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
|
||||||
|
...nearestPaletteFamily,
|
||||||
|
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
|
||||||
|
const { l: prevLightness } = getHsl(prev.hex);
|
||||||
|
const { l: currLightness } = getHsl(curr.hex);
|
||||||
|
|
||||||
|
const deltaPrev = Math.abs(prevLightness - l);
|
||||||
|
const deltaCurr = Math.abs(currLightness - l);
|
||||||
|
|
||||||
|
return deltaPrev < deltaCurr ? prev : curr;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return paletteFamily;
|
||||||
|
}
|
93
packages/color/src/shared/colord.ts
Normal file
93
packages/color/src/shared/colord.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { colord, extend } from 'colord';
|
||||||
|
import namesPlugin from 'colord/plugins/names';
|
||||||
|
import mixPlugin from 'colord/plugins/mix';
|
||||||
|
import labPlugin from 'colord/plugins/lab';
|
||||||
|
import type { AnyColor, HslColor, RgbColor } from 'colord';
|
||||||
|
|
||||||
|
extend([namesPlugin, mixPlugin, labPlugin]);
|
||||||
|
|
||||||
|
export function isValidColor(color: AnyColor) {
|
||||||
|
return colord(color).isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHex(color: AnyColor) {
|
||||||
|
return colord(color).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRgb(color: AnyColor) {
|
||||||
|
return colord(color).toRgb();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHsl(color: AnyColor) {
|
||||||
|
return colord(color).toHsl();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHsv(color: AnyColor) {
|
||||||
|
return colord(color).toHsv();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeltaE(color1: AnyColor, color2: AnyColor) {
|
||||||
|
return colord(color1).delta(color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformHslToHex(color: HslColor) {
|
||||||
|
return colord(color).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add color alpha
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param alpha - Alpha (0 - 1)
|
||||||
|
*/
|
||||||
|
export function addColorAlpha(color: AnyColor, alpha: number) {
|
||||||
|
return colord(color).alpha(alpha).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix color
|
||||||
|
*
|
||||||
|
* @param firstColor - First color
|
||||||
|
* @param secondColor - Second color
|
||||||
|
* @param ratio - The ratio of the second color (0 - 1)
|
||||||
|
*/
|
||||||
|
export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) {
|
||||||
|
return colord(firstColor).mix(secondColor, ratio).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform color with opacity to similar color without opacity
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param alpha - Alpha (0 - 1)
|
||||||
|
* @param bgColor Background color (usually white or black)
|
||||||
|
*/
|
||||||
|
export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') {
|
||||||
|
const originColor = addColorAlpha(color, alpha);
|
||||||
|
const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
|
||||||
|
|
||||||
|
const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
|
||||||
|
|
||||||
|
function calRgb(or: number, bg: number, al: number) {
|
||||||
|
return bg + (or - bg) * al;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultRgb: RgbColor = {
|
||||||
|
r: calRgb(oR, bgR, alpha),
|
||||||
|
g: calRgb(oG, bgG, alpha),
|
||||||
|
b: calRgb(oB, bgB, alpha)
|
||||||
|
};
|
||||||
|
|
||||||
|
return colord(resultRgb).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is white color
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
*/
|
||||||
|
export function isWhiteColor(color: AnyColor) {
|
||||||
|
return colord(color).isEqual('#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { colord };
|
2
packages/color/src/shared/index.ts
Normal file
2
packages/color/src/shared/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './colord';
|
||||||
|
export * from './name';
|
49
packages/color/src/shared/name.ts
Normal file
49
packages/color/src/shared/name.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { colorNames } from '../constant';
|
||||||
|
import { getHex, getHsl, getRgb } from './colord';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color name
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
*/
|
||||||
|
export function getColorName(color: string) {
|
||||||
|
const hex = getHex(color);
|
||||||
|
const rgb = getRgb(color);
|
||||||
|
const hsl = getHsl(color);
|
||||||
|
|
||||||
|
let ndf = 0;
|
||||||
|
let ndf1 = 0;
|
||||||
|
let ndf2 = 0;
|
||||||
|
let cl = -1;
|
||||||
|
let df = -1;
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
colorNames.some((item, index) => {
|
||||||
|
const [hexValue, colorName] = item;
|
||||||
|
|
||||||
|
const match = hex === hexValue;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
name = colorName;
|
||||||
|
} else {
|
||||||
|
const { r, g, b } = getRgb(hexValue);
|
||||||
|
const { h, s, l } = getHsl(hexValue);
|
||||||
|
|
||||||
|
ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
|
||||||
|
ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
|
||||||
|
|
||||||
|
ndf = ndf1 + ndf2 * 2;
|
||||||
|
if (df < 0 || df > ndf) {
|
||||||
|
df = ndf;
|
||||||
|
cl = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
name = colorNames[cl][1];
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
58
packages/color/src/types/index.ts
Normal file
58
packages/color/src/types/index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* the color palette number
|
||||||
|
*
|
||||||
|
* the main color number is 500
|
||||||
|
*/
|
||||||
|
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
||||||
|
|
||||||
|
/** the color palette */
|
||||||
|
export type ColorPalette = {
|
||||||
|
/** the color hex value */
|
||||||
|
hex: string;
|
||||||
|
/**
|
||||||
|
* the color number
|
||||||
|
*
|
||||||
|
* - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
|
||||||
|
*/
|
||||||
|
number: ColorPaletteNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette family */
|
||||||
|
export type ColorPaletteFamily = {
|
||||||
|
/** the color palette family name */
|
||||||
|
name: string;
|
||||||
|
/** the color palettes */
|
||||||
|
palettes: ColorPalette[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette with delta */
|
||||||
|
export type ColorPaletteWithDelta = ColorPalette & {
|
||||||
|
delta: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette family with nearest palette */
|
||||||
|
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
|
||||||
|
nearestPalette: ColorPaletteWithDelta;
|
||||||
|
nearestLightnessPalette: ColorPaletteWithDelta;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette match */
|
||||||
|
export type ColorPaletteMatch = ColorPaletteFamily & {
|
||||||
|
/** the color map of the palette */
|
||||||
|
colorMap: Map<ColorPaletteNumber, ColorPalette>;
|
||||||
|
/**
|
||||||
|
* the main color of the palette
|
||||||
|
*
|
||||||
|
* which number is 500
|
||||||
|
*/
|
||||||
|
main: ColorPalette;
|
||||||
|
/** the match color of the palette */
|
||||||
|
match: ColorPalette;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color index of color palette
|
||||||
|
*
|
||||||
|
* From left to right, the color is from light to dark, 6 is main color
|
||||||
|
*/
|
||||||
|
export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
|
20
packages/color/tsconfig.json
Normal file
20
packages/color/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
16
packages/hooks/package.json
Normal file
16
packages/hooks/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/hooks",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/axios": "workspace:*",
|
||||||
|
"@sa/utils": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
11
packages/hooks/src/index.ts
Normal file
11
packages/hooks/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import useBoolean from './use-boolean';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
import useCountDown from './use-count-down';
|
||||||
|
import useContext from './use-context';
|
||||||
|
import useSvgIconRender from './use-svg-icon-render';
|
||||||
|
import useHookTable from './use-table';
|
||||||
|
|
||||||
|
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
||||||
|
|
||||||
|
export * from './use-signal';
|
||||||
|
export * from './use-table';
|
31
packages/hooks/src/use-boolean.ts
Normal file
31
packages/hooks/src/use-boolean.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean
|
||||||
|
*
|
||||||
|
* @param initValue Init value
|
||||||
|
*/
|
||||||
|
export default function useBoolean(initValue = false) {
|
||||||
|
const bool = ref(initValue);
|
||||||
|
|
||||||
|
function setBool(value: boolean) {
|
||||||
|
bool.value = value;
|
||||||
|
}
|
||||||
|
function setTrue() {
|
||||||
|
setBool(true);
|
||||||
|
}
|
||||||
|
function setFalse() {
|
||||||
|
setBool(false);
|
||||||
|
}
|
||||||
|
function toggle() {
|
||||||
|
setBool(!bool.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bool,
|
||||||
|
setBool,
|
||||||
|
setTrue,
|
||||||
|
setFalse,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
}
|
96
packages/hooks/src/use-context.ts
Normal file
96
packages/hooks/src/use-context.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { inject, provide } from 'vue';
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use context
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
|
||||||
|
*
|
||||||
|
* // context.ts
|
||||||
|
* import { ref } from 'vue';
|
||||||
|
* import { useContext } from '@sa/hooks';
|
||||||
|
*
|
||||||
|
* export const { setupStore, useStore } = useContext('demo', () => {
|
||||||
|
* const count = ref(0);
|
||||||
|
*
|
||||||
|
* function increment() {
|
||||||
|
* count.value++;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* function decrement() {
|
||||||
|
* count.value--;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return {
|
||||||
|
* count,
|
||||||
|
* increment,
|
||||||
|
* decrement
|
||||||
|
* };
|
||||||
|
* })
|
||||||
|
* ``` // A.vue
|
||||||
|
* ```vue
|
||||||
|
* <template>
|
||||||
|
* <div>A</div>
|
||||||
|
* </template>
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* import { setupStore } from './context';
|
||||||
|
*
|
||||||
|
* setupStore();
|
||||||
|
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||||
|
* </script>
|
||||||
|
* ``` // B.vue
|
||||||
|
* ```vue
|
||||||
|
* <template>
|
||||||
|
* <div>B</div>
|
||||||
|
* </template>
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* import { useStore } from './context';
|
||||||
|
*
|
||||||
|
* const { count, increment } = useStore();
|
||||||
|
* </script>
|
||||||
|
* ```;
|
||||||
|
*
|
||||||
|
* // C.vue is same as B.vue
|
||||||
|
*
|
||||||
|
* @param contextName Context name
|
||||||
|
* @param fn Context function
|
||||||
|
*/
|
||||||
|
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||||
|
type Context = ReturnType<T>;
|
||||||
|
|
||||||
|
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||||
|
|
||||||
|
function setupStore(...args: Parameters<T>) {
|
||||||
|
const context: Context = fn(...args);
|
||||||
|
return useProvide(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Setup store in the parent component */
|
||||||
|
setupStore,
|
||||||
|
/** Use store in the child component */
|
||||||
|
useStore
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create context */
|
||||||
|
function createContext<T>(contextName: string) {
|
||||||
|
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||||
|
|
||||||
|
function useProvide(context: T) {
|
||||||
|
provide(injectKey, context);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useInject() {
|
||||||
|
return inject(injectKey) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
useProvide,
|
||||||
|
useInject
|
||||||
|
};
|
||||||
|
}
|
49
packages/hooks/src/use-count-down.ts
Normal file
49
packages/hooks/src/use-count-down.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { computed, onScopeDispose, ref } from 'vue';
|
||||||
|
import { useRafFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* count down
|
||||||
|
*
|
||||||
|
* @param seconds - count down seconds
|
||||||
|
*/
|
||||||
|
export default function useCountDown(seconds: number) {
|
||||||
|
const FPS_PER_SECOND = 60;
|
||||||
|
|
||||||
|
const fps = ref(0);
|
||||||
|
|
||||||
|
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
|
||||||
|
|
||||||
|
const isCounting = computed(() => fps.value > 0);
|
||||||
|
|
||||||
|
const { pause, resume } = useRafFn(
|
||||||
|
() => {
|
||||||
|
if (fps.value > 0) {
|
||||||
|
fps.value -= 1;
|
||||||
|
} else {
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
function start(updateSeconds: number = seconds) {
|
||||||
|
fps.value = FPS_PER_SECOND * updateSeconds;
|
||||||
|
resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
fps.value = 0;
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
isCounting,
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
};
|
||||||
|
}
|
16
packages/hooks/src/use-loading.ts
Normal file
16
packages/hooks/src/use-loading.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import useBoolean from './use-boolean';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading
|
||||||
|
*
|
||||||
|
* @param initValue Init value
|
||||||
|
*/
|
||||||
|
export default function useLoading(initValue = false) {
|
||||||
|
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
startLoading,
|
||||||
|
endLoading
|
||||||
|
};
|
||||||
|
}
|
79
packages/hooks/src/use-request.ts
Normal file
79
packages/hooks/src/use-request.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { createFlatRequest } from '@sa/axios';
|
||||||
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
CreateAxiosDefaults,
|
||||||
|
CustomAxiosRequestConfig,
|
||||||
|
MappedType,
|
||||||
|
RequestOption,
|
||||||
|
ResponseType
|
||||||
|
} from '@sa/axios';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||||
|
data: Ref<T>;
|
||||||
|
error: Ref<null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||||
|
data: Ref<null>;
|
||||||
|
error: Ref<AxiosError<ResponseData>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||||
|
|
||||||
|
export interface HookRequestInstance<ResponseData = any> {
|
||||||
|
<T = any, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig
|
||||||
|
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||||
|
cancelRequest: (requestId: string) => void;
|
||||||
|
cancelAllRequest: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a hook request instance
|
||||||
|
*
|
||||||
|
* @param axiosConfig
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export default function createHookRequest<ResponseData = any>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData>>
|
||||||
|
) {
|
||||||
|
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||||
|
|
||||||
|
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig
|
||||||
|
) {
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
|
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||||
|
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||||
|
|
||||||
|
startLoading();
|
||||||
|
|
||||||
|
request(config).then(res => {
|
||||||
|
if (res.data) {
|
||||||
|
data.value = res.data;
|
||||||
|
} else {
|
||||||
|
error.value = res.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
endLoading();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
} as HookRequestInstance<ResponseData>;
|
||||||
|
|
||||||
|
hookRequest.cancelRequest = request.cancelRequest;
|
||||||
|
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||||
|
|
||||||
|
return hookRequest;
|
||||||
|
}
|
144
packages/hooks/src/use-signal.ts
Normal file
144
packages/hooks/src/use-signal.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
||||||
|
import type {
|
||||||
|
ComputedGetter,
|
||||||
|
DebuggerOptions,
|
||||||
|
Ref,
|
||||||
|
ShallowRef,
|
||||||
|
WritableComputedOptions,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
type Updater<T> = (value: T) => T;
|
||||||
|
type Mutator<T> = (value: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal is a reactive value that can be set, updated or mutated
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const count = useSignal(0);
|
||||||
|
*
|
||||||
|
* // `watchEffect`
|
||||||
|
* watchEffect(() => {
|
||||||
|
* console.log(count());
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // watch
|
||||||
|
* watch(count, value => {
|
||||||
|
* console.log(value);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // useComputed
|
||||||
|
* const double = useComputed(() => count() * 2);
|
||||||
|
* const writeableDouble = useComputed({
|
||||||
|
* get: () => count() * 2,
|
||||||
|
* set: value => count.set(value / 2)
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface Signal<T> {
|
||||||
|
(): Readonly<T>;
|
||||||
|
/**
|
||||||
|
* Set the value of the signal
|
||||||
|
*
|
||||||
|
* It recommend use `set` for primitive values
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
set(value: T): void;
|
||||||
|
/**
|
||||||
|
* Update the value of the signal using an updater function
|
||||||
|
*
|
||||||
|
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
|
||||||
|
*
|
||||||
|
* @param updater
|
||||||
|
*/
|
||||||
|
update(updater: Updater<T>): void;
|
||||||
|
/**
|
||||||
|
* Mutate the value of the signal using a mutator function
|
||||||
|
*
|
||||||
|
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
|
||||||
|
*
|
||||||
|
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
|
||||||
|
*
|
||||||
|
* @param mutator
|
||||||
|
*/
|
||||||
|
mutate(mutator: Mutator<T>): void;
|
||||||
|
/**
|
||||||
|
* Get the reference of the signal
|
||||||
|
*
|
||||||
|
* Sometimes it can be useful to make `v-model` work with the signal
|
||||||
|
*
|
||||||
|
* ```vue
|
||||||
|
* <template>
|
||||||
|
* <input v-model="model.count" />
|
||||||
|
* </template>;
|
||||||
|
*
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* const state = useSignal({ count: 0 }, { useRef: true });
|
||||||
|
*
|
||||||
|
* const model = state.getRef();
|
||||||
|
* </script>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getRef(): Readonly<ShallowRef<Readonly<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReadonlySignal<T> {
|
||||||
|
(): Readonly<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignalOptions {
|
||||||
|
/**
|
||||||
|
* Whether to use `ref` to store the value
|
||||||
|
*
|
||||||
|
* @default false use `sharedRef` to store the value
|
||||||
|
*/
|
||||||
|
useRef?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
|
||||||
|
const { useRef } = options || {};
|
||||||
|
|
||||||
|
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
|
||||||
|
|
||||||
|
return createSignal(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
|
||||||
|
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
|
||||||
|
export function useComputed<T>(
|
||||||
|
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
|
||||||
|
debugOptions?: DebuggerOptions
|
||||||
|
) {
|
||||||
|
const isGetter = typeof getterOrOptions === 'function';
|
||||||
|
|
||||||
|
const computedValue = computed(getterOrOptions as any, debugOptions);
|
||||||
|
|
||||||
|
if (isGetter) {
|
||||||
|
return () => computedValue.value as ReadonlySignal<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createSignal(computedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
|
||||||
|
const signal = () => state.value;
|
||||||
|
|
||||||
|
signal.set = (value: T) => {
|
||||||
|
state.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.update = (updater: Updater<T>) => {
|
||||||
|
state.value = updater(state.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.mutate = (mutator: Mutator<T>) => {
|
||||||
|
mutator(state.value);
|
||||||
|
triggerRef(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
|
||||||
|
|
||||||
|
return signal;
|
||||||
|
}
|
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { h } from 'vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svg icon render hook
|
||||||
|
*
|
||||||
|
* @param SvgIcon Svg icon component
|
||||||
|
*/
|
||||||
|
export default function useSvgIconRender(SvgIcon: Component) {
|
||||||
|
interface IconConfig {
|
||||||
|
/** Iconify icon name */
|
||||||
|
icon?: string;
|
||||||
|
/** Local icon name */
|
||||||
|
localIcon?: string;
|
||||||
|
/** Icon color */
|
||||||
|
color?: string;
|
||||||
|
/** Icon size */
|
||||||
|
fontSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svg icon VNode
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
const SvgIconVNode = (config: IconConfig) => {
|
||||||
|
const { color, fontSize, icon, localIcon } = config;
|
||||||
|
|
||||||
|
const style: IconStyle = {};
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
style.color = color;
|
||||||
|
}
|
||||||
|
if (fontSize) {
|
||||||
|
style.fontSize = `${fontSize}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!icon && !localIcon) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => h(SvgIcon, { icon, localIcon, style });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
SvgIconVNode
|
||||||
|
};
|
||||||
|
}
|
154
packages/hooks/src/use-table.ts
Normal file
154
packages/hooks/src/use-table.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import type { Ref, VNodeChild } from 'vue';
|
||||||
|
import { jsonClone } from '@sa/utils';
|
||||||
|
import useBoolean from './use-boolean';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
export type ApiFn = (args: any) => Promise<unknown>;
|
||||||
|
|
||||||
|
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||||
|
|
||||||
|
export type TableColumnCheck = {
|
||||||
|
key: string;
|
||||||
|
title: TableColumnCheckTitle;
|
||||||
|
checked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TableDataWithIndex<T> = T & { index: number };
|
||||||
|
|
||||||
|
export type TransformedData<T> = {
|
||||||
|
data: TableDataWithIndex<T>[];
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
||||||
|
|
||||||
|
export type TableConfig<A extends ApiFn, T, C> = {
|
||||||
|
/** api function to get table data */
|
||||||
|
apiFn: A;
|
||||||
|
/** api params */
|
||||||
|
apiParams?: Parameters<A>[0];
|
||||||
|
/** transform api response to table data */
|
||||||
|
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||||
|
/** columns factory */
|
||||||
|
columns: () => C[];
|
||||||
|
/**
|
||||||
|
* get column checks
|
||||||
|
*
|
||||||
|
* @param columns
|
||||||
|
*/
|
||||||
|
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
||||||
|
/**
|
||||||
|
* get columns
|
||||||
|
*
|
||||||
|
* @param columns
|
||||||
|
*/
|
||||||
|
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||||
|
/**
|
||||||
|
* callback when response fetched
|
||||||
|
*
|
||||||
|
* @param transformed transformed data
|
||||||
|
*/
|
||||||
|
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||||
|
/**
|
||||||
|
* whether to get data immediately
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
immediate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||||
|
|
||||||
|
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||||
|
|
||||||
|
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
|
||||||
|
|
||||||
|
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||||
|
|
||||||
|
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
|
||||||
|
|
||||||
|
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
||||||
|
|
||||||
|
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
||||||
|
|
||||||
|
function reloadColumns() {
|
||||||
|
allColumns.value = config.columns();
|
||||||
|
|
||||||
|
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||||
|
|
||||||
|
const defaultChecks = getColumnChecks(allColumns.value);
|
||||||
|
|
||||||
|
columnChecks.value = defaultChecks.map(col => ({
|
||||||
|
...col,
|
||||||
|
checked: checkMap.get(col.key) ?? col.checked
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
startLoading();
|
||||||
|
|
||||||
|
const formattedParams = formatSearchParams(searchParams);
|
||||||
|
|
||||||
|
const response = await apiFn(formattedParams);
|
||||||
|
|
||||||
|
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
||||||
|
|
||||||
|
data.value = transformed.data;
|
||||||
|
|
||||||
|
setEmpty(transformed.data.length === 0);
|
||||||
|
|
||||||
|
await config.onFetched?.(transformed);
|
||||||
|
|
||||||
|
endLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSearchParams(params: Record<string, unknown>) {
|
||||||
|
const formattedParams: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
formattedParams[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update search params
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||||
|
Object.assign(searchParams, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** reset search params */
|
||||||
|
function resetSearchParams() {
|
||||||
|
Object.assign(searchParams, jsonClone(apiParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
empty,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
columnChecks,
|
||||||
|
reloadColumns,
|
||||||
|
getData,
|
||||||
|
searchParams,
|
||||||
|
updateSearchParams,
|
||||||
|
resetSearchParams
|
||||||
|
};
|
||||||
|
}
|
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
19
packages/materials/package.json
Normal file
19
packages/materials/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/materials",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"simplebar-vue": "2.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typed-css-modules": "0.9.1"
|
||||||
|
}
|
||||||
|
}
|
6
packages/materials/src/index.ts
Normal file
6
packages/materials/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
|
||||||
|
import PageTab from './libs/page-tab';
|
||||||
|
import SimpleScrollbar from './libs/simple-scrollbar';
|
||||||
|
|
||||||
|
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
|
||||||
|
export * from './types';
|
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/* @type */
|
||||||
|
|
||||||
|
.layout-header,
|
||||||
|
.layout-header-placement {
|
||||||
|
height: var(--soy-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header {
|
||||||
|
z-index: var(--soy-header-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-tab {
|
||||||
|
top: var(--soy-header-height);
|
||||||
|
height: var(--soy-tab-height);
|
||||||
|
z-index: var(--soy-tab-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-tab-placement {
|
||||||
|
height: var(--soy-tab-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sider {
|
||||||
|
width: var(--soy-sider-width);
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mobile-sider {
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mobile-sider-mask {
|
||||||
|
z-index: var(--soy-mobile-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sider_collapsed {
|
||||||
|
width: var(--soy-sider-collapsed-width);
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-footer,
|
||||||
|
.layout-footer-placement {
|
||||||
|
height: var(--soy-footer-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-footer {
|
||||||
|
z-index: var(--soy-footer-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-gap {
|
||||||
|
padding-left: var(--soy-sider-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-gap_collapsed {
|
||||||
|
padding-left: var(--soy-sider-collapsed-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider-padding-top {
|
||||||
|
padding-top: var(--soy-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider-padding-bottom {
|
||||||
|
padding-bottom: var(--soy-footer-height);
|
||||||
|
}
|
18
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
18
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
declare const styles: {
|
||||||
|
readonly 'layout-header': string;
|
||||||
|
readonly 'layout-header-placement': string;
|
||||||
|
readonly 'layout-tab': string;
|
||||||
|
readonly 'layout-tab-placement': string;
|
||||||
|
readonly 'layout-sider': string;
|
||||||
|
readonly 'layout-mobile-sider': string;
|
||||||
|
readonly 'layout-mobile-sider-mask': string;
|
||||||
|
readonly 'layout-sider_collapsed': string;
|
||||||
|
readonly 'layout-footer': string;
|
||||||
|
readonly 'layout-footer-placement': string;
|
||||||
|
readonly 'left-gap': string;
|
||||||
|
readonly 'left-gap_collapsed': string;
|
||||||
|
readonly 'sider-padding-top': string;
|
||||||
|
readonly 'sider-padding-bottom': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styles;
|
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import AdminLayout from './index.vue';
|
||||||
|
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
|
||||||
|
|
||||||
|
export default AdminLayout;
|
||||||
|
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
|
237
packages/materials/src/libs/admin-layout/index.vue
Normal file
237
packages/materials/src/libs/admin-layout/index.vue
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { AdminLayoutProps } from '../../types';
|
||||||
|
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AdminLayout'
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<AdminLayoutProps>(), {
|
||||||
|
mode: 'vertical',
|
||||||
|
scrollMode: 'content',
|
||||||
|
scrollElId: LAYOUT_SCROLL_EL_ID,
|
||||||
|
commonClass: 'transition-all-300',
|
||||||
|
fixedTop: true,
|
||||||
|
maxZIndex: LAYOUT_MAX_Z_INDEX,
|
||||||
|
headerVisible: true,
|
||||||
|
headerHeight: 56,
|
||||||
|
tabVisible: true,
|
||||||
|
tabHeight: 48,
|
||||||
|
siderVisible: true,
|
||||||
|
siderCollapse: false,
|
||||||
|
siderWidth: 220,
|
||||||
|
siderCollapsedWidth: 64,
|
||||||
|
footerVisible: true,
|
||||||
|
footerHeight: 48,
|
||||||
|
rightFooter: false
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
/** Update siderCollapse */
|
||||||
|
(e: 'update:siderCollapse', collapse: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/** Main */
|
||||||
|
default?: SlotFn;
|
||||||
|
/** Header */
|
||||||
|
header?: SlotFn;
|
||||||
|
/** Tab */
|
||||||
|
tab?: SlotFn;
|
||||||
|
/** Sider */
|
||||||
|
sider?: SlotFn;
|
||||||
|
/** Footer */
|
||||||
|
footer?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const slots = defineSlots<Slots>();
|
||||||
|
|
||||||
|
const cssVars = computed(() => createLayoutCssVars(props));
|
||||||
|
|
||||||
|
// config visible
|
||||||
|
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
|
||||||
|
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
|
||||||
|
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||||
|
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||||
|
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
|
||||||
|
|
||||||
|
// scroll mode
|
||||||
|
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
|
||||||
|
const isContentScroll = computed(() => props.scrollMode === 'content');
|
||||||
|
|
||||||
|
// layout direction
|
||||||
|
const isVertical = computed(() => props.mode === 'vertical');
|
||||||
|
const isHorizontal = computed(() => props.mode === 'horizontal');
|
||||||
|
|
||||||
|
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
|
||||||
|
|
||||||
|
// css
|
||||||
|
const leftGapClass = computed(() => {
|
||||||
|
if (!props.fullContent && showSider.value) {
|
||||||
|
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
|
||||||
|
|
||||||
|
const footerLeftGapClass = computed(() => {
|
||||||
|
const condition1 = isVertical.value;
|
||||||
|
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
|
||||||
|
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
|
||||||
|
|
||||||
|
if (condition1 || condition2 || condition3) {
|
||||||
|
return leftGapClass.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const siderPaddingClass = computed(() => {
|
||||||
|
let cls = '';
|
||||||
|
|
||||||
|
if (showHeader.value && !headerLeftGapClass.value) {
|
||||||
|
cls += style['sider-padding-top'];
|
||||||
|
}
|
||||||
|
if (showFooter.value && !footerLeftGapClass.value) {
|
||||||
|
cls += ` ${style['sider-padding-bottom']}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cls;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClickMask() {
|
||||||
|
emit('update:siderCollapse', true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
|
||||||
|
<div
|
||||||
|
:id="isWrapperScroll ? scrollElId : undefined"
|
||||||
|
class="h-full flex flex-col"
|
||||||
|
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<template v-if="showHeader">
|
||||||
|
<header
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-header'],
|
||||||
|
commonClass,
|
||||||
|
headerClass,
|
||||||
|
headerLeftGapClass,
|
||||||
|
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
v-show="!fullContent && fixedHeaderAndTab"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-header-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Tab -->
|
||||||
|
<template v-if="showTab">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-tab'],
|
||||||
|
commonClass,
|
||||||
|
tabClass,
|
||||||
|
{ 'top-0!': fullContent || !showHeader },
|
||||||
|
leftGapClass,
|
||||||
|
{ 'absolute left-0 w-full': fixedHeaderAndTab }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="tab"></slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="fullContent || fixedHeaderAndTab"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-tab-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Sider -->
|
||||||
|
<template v-if="showSider">
|
||||||
|
<aside
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="absolute left-0 top-0 h-full"
|
||||||
|
:class="[
|
||||||
|
commonClass,
|
||||||
|
siderClass,
|
||||||
|
siderPaddingClass,
|
||||||
|
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="sider"></slot>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Mobile Sider -->
|
||||||
|
<template v-if="showMobileSider">
|
||||||
|
<aside
|
||||||
|
class="absolute left-0 top-0 h-full w-0 bg-white"
|
||||||
|
:class="[
|
||||||
|
commonClass,
|
||||||
|
mobileSiderClass,
|
||||||
|
style['layout-mobile-sider'],
|
||||||
|
siderCollapse ? 'overflow-hidden' : style['layout-sider']
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="sider"></slot>
|
||||||
|
</aside>
|
||||||
|
<div
|
||||||
|
v-show="!siderCollapse"
|
||||||
|
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
|
||||||
|
:class="[style['layout-mobile-sider-mask']]"
|
||||||
|
@click="handleClickMask"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main
|
||||||
|
:id="isContentScroll ? scrollElId : undefined"
|
||||||
|
class="flex flex-col flex-grow"
|
||||||
|
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-if="showFooter">
|
||||||
|
<footer
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-footer'],
|
||||||
|
commonClass,
|
||||||
|
footerClass,
|
||||||
|
footerLeftGapClass,
|
||||||
|
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</footer>
|
||||||
|
<div
|
||||||
|
v-show="!fullContent && fixedFooter"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-footer-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
68
packages/materials/src/libs/admin-layout/shared.ts
Normal file
68
packages/materials/src/libs/admin-layout/shared.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
|
||||||
|
|
||||||
|
/** The id of the scroll element of the layout */
|
||||||
|
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
|
||||||
|
|
||||||
|
/** The max z-index of the layout */
|
||||||
|
export const LAYOUT_MAX_Z_INDEX = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create layout css vars by css vars props
|
||||||
|
*
|
||||||
|
* @param props Css vars props
|
||||||
|
*/
|
||||||
|
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
|
||||||
|
const cssVars: LayoutCssVars = {
|
||||||
|
'--soy-header-height': `${props.headerHeight}px`,
|
||||||
|
'--soy-header-z-index': props.headerZIndex,
|
||||||
|
'--soy-tab-height': `${props.tabHeight}px`,
|
||||||
|
'--soy-tab-z-index': props.tabZIndex,
|
||||||
|
'--soy-sider-width': `${props.siderWidth}px`,
|
||||||
|
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
|
||||||
|
'--soy-sider-z-index': props.siderZIndex,
|
||||||
|
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
|
||||||
|
'--soy-footer-height': `${props.footerHeight}px`,
|
||||||
|
'--soy-footer-z-index': props.footerZIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
return cssVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create layout css vars
|
||||||
|
*
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function createLayoutCssVars(props: AdminLayoutProps) {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
isMobile,
|
||||||
|
maxZIndex = LAYOUT_MAX_Z_INDEX,
|
||||||
|
headerHeight,
|
||||||
|
tabHeight,
|
||||||
|
siderWidth,
|
||||||
|
siderCollapsedWidth,
|
||||||
|
footerHeight
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const headerZIndex = maxZIndex - 3;
|
||||||
|
const tabZIndex = maxZIndex - 5;
|
||||||
|
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
|
||||||
|
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
|
||||||
|
const footerZIndex = maxZIndex - 5;
|
||||||
|
|
||||||
|
const cssProps: LayoutCssVarsProps = {
|
||||||
|
headerHeight,
|
||||||
|
headerZIndex,
|
||||||
|
tabHeight,
|
||||||
|
tabZIndex,
|
||||||
|
siderWidth,
|
||||||
|
siderZIndex,
|
||||||
|
mobileSiderZIndex,
|
||||||
|
siderCollapsedWidth,
|
||||||
|
footerHeight,
|
||||||
|
footerZIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
return createLayoutCssVarsByCssVarsProps(cssProps);
|
||||||
|
}
|
53
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
53
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ButtonTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-(1px solid) rounded-4px px-12px py-4px"
|
||||||
|
:class="[
|
||||||
|
style['button-tab'],
|
||||||
|
{ [style['button-tab_dark']]: darkMode },
|
||||||
|
{ [style['button-tab_active']]: active },
|
||||||
|
{ [style['button-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChromeTabBg'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg class="size-full">
|
||||||
|
<defs>
|
||||||
|
<symbol id="geometry-left" viewBox="0 0 214 36">
|
||||||
|
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||||
|
<use xlink:href="#geometry-left" />
|
||||||
|
</symbol>
|
||||||
|
<clipPath>
|
||||||
|
<rect width="100%" height="100%" x="0" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<svg width="51%" height="100%">
|
||||||
|
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<g transform="scale(-1, 1)">
|
||||||
|
<svg width="51%" height="100%" x="-100%" y="0">
|
||||||
|
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
58
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
58
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import ChromeTabBg from './chrome-tab-bg.vue';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChromeTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
|
||||||
|
:class="[
|
||||||
|
style['chrome-tab'],
|
||||||
|
{ [style['chrome-tab_dark']]: darkMode },
|
||||||
|
{ [style['chrome-tab_active']]: active },
|
||||||
|
{ [style['chrome-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
|
||||||
|
<ChromeTabBg />
|
||||||
|
</div>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/* @type */
|
||||||
|
|
||||||
|
.button-tab {
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_dark {
|
||||||
|
border-color: #ffffff3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab:hover {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
border-color: var(--soy-primary-color-opacity3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_active {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
border-color: var(--soy-primary-color-opacity3);
|
||||||
|
background-color: var(--soy-primary-color-opacity1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_active_dark {
|
||||||
|
background-color: var(--soy-primary-color-opacity2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab .svg-close:hover {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_dark .svg-close:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover {
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active {
|
||||||
|
z-index: 10;
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab__bg {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active_dark .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover .chrome-tab__bg {
|
||||||
|
color: #dee1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active:hover .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark:hover .chrome-tab__bg {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active_dark:hover .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab .svg-close:hover {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .svg-close:hover {
|
||||||
|
background-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark .svg-close:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .chrome-tab-divider {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover .chrome-tab-divider {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark .chrome-tab-divider {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
15
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
15
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
declare const styles: {
|
||||||
|
readonly 'button-tab': string;
|
||||||
|
readonly 'button-tab_dark': string;
|
||||||
|
readonly 'button-tab_active': string;
|
||||||
|
readonly 'button-tab_active_dark': string;
|
||||||
|
readonly 'chrome-tab': string;
|
||||||
|
readonly 'chrome-tab_active': string;
|
||||||
|
readonly 'chrome-tab__bg': string;
|
||||||
|
readonly 'chrome-tab_active_dark': string;
|
||||||
|
readonly 'chrome-tab_dark': string;
|
||||||
|
readonly 'chrome-tab-divider': string;
|
||||||
|
readonly 'svg-close': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styles;
|
3
packages/materials/src/libs/page-tab/index.ts
Normal file
3
packages/materials/src/libs/page-tab/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import PageTab from './index.vue';
|
||||||
|
|
||||||
|
export default PageTab;
|
72
packages/materials/src/libs/page-tab/index.vue
Normal file
72
packages/materials/src/libs/page-tab/index.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { PageTabMode, PageTabProps } from '../../types';
|
||||||
|
import { ACTIVE_COLOR, createTabCssVars } from './shared';
|
||||||
|
import ChromeTab from './chrome-tab.vue';
|
||||||
|
import ButtonTab from './button-tab.vue';
|
||||||
|
import SvgClose from './svg-close.vue';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PageTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PageTabProps>(), {
|
||||||
|
mode: 'chrome',
|
||||||
|
commonClass: 'transition-all-300',
|
||||||
|
activeColor: ACTIVE_COLOR,
|
||||||
|
closable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'close'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const activeTabComponent = computed(() => {
|
||||||
|
const { mode, chromeClass, buttonClass } = props;
|
||||||
|
|
||||||
|
const tabComponentMap = {
|
||||||
|
chrome: {
|
||||||
|
component: ChromeTab,
|
||||||
|
class: chromeClass
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
component: ButtonTab,
|
||||||
|
class: buttonClass
|
||||||
|
}
|
||||||
|
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
|
||||||
|
|
||||||
|
return tabComponentMap[mode];
|
||||||
|
});
|
||||||
|
|
||||||
|
const cssVars = computed(() => createTabCssVars(props.activeColor));
|
||||||
|
|
||||||
|
const bindProps = computed(() => {
|
||||||
|
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
|
||||||
|
<template #prefix>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
<template #suffix>
|
||||||
|
<slot name="suffix">
|
||||||
|
<SvgClose v-if="closable" :class="[style['svg-close']]" @pointerdown.stop="handleClose" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
31
packages/materials/src/libs/page-tab/shared.ts
Normal file
31
packages/materials/src/libs/page-tab/shared.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { addColorAlpha, transformColorWithOpacity } from '@sa/color';
|
||||||
|
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
|
||||||
|
|
||||||
|
/** The active color of the tab */
|
||||||
|
export const ACTIVE_COLOR = '#1890ff';
|
||||||
|
|
||||||
|
function createCssVars(props: PageTabCssVarsProps) {
|
||||||
|
const cssVars: PageTabCssVars = {
|
||||||
|
'--soy-primary-color': props.primaryColor,
|
||||||
|
'--soy-primary-color1': props.primaryColor1,
|
||||||
|
'--soy-primary-color2': props.primaryColor2,
|
||||||
|
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
|
||||||
|
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
|
||||||
|
'--soy-primary-color-opacity3': props.primaryColorOpacity3
|
||||||
|
};
|
||||||
|
|
||||||
|
return cssVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTabCssVars(primaryColor: string) {
|
||||||
|
const cssProps: PageTabCssVarsProps = {
|
||||||
|
primaryColor,
|
||||||
|
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
|
||||||
|
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
|
||||||
|
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
|
||||||
|
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
|
||||||
|
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
|
||||||
|
};
|
||||||
|
|
||||||
|
return createCssVars(cssProps);
|
||||||
|
}
|
18
packages/materials/src/libs/page-tab/svg-close.vue
Normal file
18
packages/materials/src/libs/page-tab/svg-close.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'SvgClose'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SimpleScrollbar from './index.vue';
|
||||||
|
|
||||||
|
export default SimpleScrollbar;
|
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Simplebar from 'simplebar-vue';
|
||||||
|
import 'simplebar-vue/dist/simplebar.min.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SimpleScrollbar'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex-1-hidden">
|
||||||
|
<Simplebar class="h-full">
|
||||||
|
<slot />
|
||||||
|
</Simplebar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
294
packages/materials/src/types/index.ts
Normal file
294
packages/materials/src/types/index.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
/** Header config */
|
||||||
|
interface AdminLayoutHeaderConfig {
|
||||||
|
/**
|
||||||
|
* Whether header is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
headerVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Header class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
headerClass?: string;
|
||||||
|
/**
|
||||||
|
* Header height
|
||||||
|
*
|
||||||
|
* @default 56px
|
||||||
|
*/
|
||||||
|
headerHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tab config */
|
||||||
|
interface AdminLayoutTabConfig {
|
||||||
|
/**
|
||||||
|
* Whether tab is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
tabVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Tab class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
tabClass?: string;
|
||||||
|
/**
|
||||||
|
* Tab height
|
||||||
|
*
|
||||||
|
* @default 48px
|
||||||
|
*/
|
||||||
|
tabHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sider config */
|
||||||
|
interface AdminLayoutSiderConfig {
|
||||||
|
/**
|
||||||
|
* Whether sider is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
siderVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Sider class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
siderClass?: string;
|
||||||
|
/**
|
||||||
|
* Mobile sider class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
mobileSiderClass?: string;
|
||||||
|
/**
|
||||||
|
* Sider collapse status
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
siderCollapse?: boolean;
|
||||||
|
/**
|
||||||
|
* Sider width when collapse is false
|
||||||
|
*
|
||||||
|
* @default '220px'
|
||||||
|
*/
|
||||||
|
siderWidth?: number;
|
||||||
|
/**
|
||||||
|
* Sider width when collapse is true
|
||||||
|
*
|
||||||
|
* @default '64px'
|
||||||
|
*/
|
||||||
|
siderCollapsedWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Content config */
|
||||||
|
export interface AdminLayoutContentConfig {
|
||||||
|
/**
|
||||||
|
* Content class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
contentClass?: string;
|
||||||
|
/**
|
||||||
|
* Whether content is full the page
|
||||||
|
*
|
||||||
|
* If true, other elements will be hidden by `display: none`
|
||||||
|
*/
|
||||||
|
fullContent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Footer config */
|
||||||
|
export interface AdminLayoutFooterConfig {
|
||||||
|
/**
|
||||||
|
* Whether footer is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
footerVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether footer is fixed
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
fixedFooter?: boolean;
|
||||||
|
/**
|
||||||
|
* Footer class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
footerClass?: string;
|
||||||
|
/**
|
||||||
|
* Footer height
|
||||||
|
*
|
||||||
|
* @default 48px
|
||||||
|
*/
|
||||||
|
footerHeight?: number;
|
||||||
|
/**
|
||||||
|
* Whether footer is on the right side
|
||||||
|
*
|
||||||
|
* When the layout is vertical, the footer is on the right side
|
||||||
|
*/
|
||||||
|
rightFooter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout mode
|
||||||
|
*
|
||||||
|
* - Horizontal
|
||||||
|
* - Vertical
|
||||||
|
*/
|
||||||
|
export type LayoutMode = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scroll mode when content overflow
|
||||||
|
*
|
||||||
|
* - Wrapper: the layout component's wrapper element has a scrollbar
|
||||||
|
* - Content: the layout component's content element has a scrollbar
|
||||||
|
*
|
||||||
|
* @default 'wrapper'
|
||||||
|
*/
|
||||||
|
export type LayoutScrollMode = 'wrapper' | 'content';
|
||||||
|
|
||||||
|
/** Admin layout props */
|
||||||
|
export interface AdminLayoutProps
|
||||||
|
extends AdminLayoutHeaderConfig,
|
||||||
|
AdminLayoutTabConfig,
|
||||||
|
AdminLayoutSiderConfig,
|
||||||
|
AdminLayoutContentConfig,
|
||||||
|
AdminLayoutFooterConfig {
|
||||||
|
/**
|
||||||
|
* Layout mode
|
||||||
|
*
|
||||||
|
* - {@link LayoutMode}
|
||||||
|
*/
|
||||||
|
mode?: LayoutMode;
|
||||||
|
/** Is mobile layout */
|
||||||
|
isMobile?: boolean;
|
||||||
|
/**
|
||||||
|
* Scroll mode
|
||||||
|
*
|
||||||
|
* - {@link ScrollMode}
|
||||||
|
*/
|
||||||
|
scrollMode?: LayoutScrollMode;
|
||||||
|
/**
|
||||||
|
* The id of the scroll element of the layout
|
||||||
|
*
|
||||||
|
* It can be used to get the corresponding Dom and scroll it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* use the default id by import
|
||||||
|
* ```ts
|
||||||
|
* import { adminLayoutScrollElId } from '@sa/vue-materials';
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
scrollElId?: string;
|
||||||
|
/** The class of the scroll element */
|
||||||
|
scrollElClass?: string;
|
||||||
|
/** The class of the scroll wrapper element */
|
||||||
|
scrollWrapperClass?: string;
|
||||||
|
/**
|
||||||
|
* The common class of the layout
|
||||||
|
*
|
||||||
|
* Is can be used to configure the transition animation
|
||||||
|
*
|
||||||
|
* @default 'transition-all-300'
|
||||||
|
*/
|
||||||
|
commonClass?: string;
|
||||||
|
/**
|
||||||
|
* Whether fix the header and tab
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
fixedTop?: boolean;
|
||||||
|
/**
|
||||||
|
* The max z-index of the layout
|
||||||
|
*
|
||||||
|
* The z-index of Header,Tab,Sider and Footer will not exceed this value
|
||||||
|
*/
|
||||||
|
maxZIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
|
||||||
|
|
||||||
|
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
|
||||||
|
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
|
||||||
|
: S;
|
||||||
|
|
||||||
|
type Prefix = '--soy-';
|
||||||
|
|
||||||
|
export type LayoutCssVarsProps = Pick<
|
||||||
|
AdminLayoutProps,
|
||||||
|
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
|
||||||
|
> & {
|
||||||
|
headerZIndex?: number;
|
||||||
|
tabZIndex?: number;
|
||||||
|
siderZIndex?: number;
|
||||||
|
mobileSiderZIndex?: number;
|
||||||
|
footerZIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LayoutCssVars = {
|
||||||
|
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mode of the tab
|
||||||
|
*
|
||||||
|
* - Button: button style
|
||||||
|
* - Chrome: chrome style
|
||||||
|
*
|
||||||
|
* @default chrome
|
||||||
|
*/
|
||||||
|
export type PageTabMode = 'button' | 'chrome';
|
||||||
|
|
||||||
|
export interface PageTabProps {
|
||||||
|
/** Whether is dark mode */
|
||||||
|
darkMode?: boolean;
|
||||||
|
/**
|
||||||
|
* The mode of the tab
|
||||||
|
*
|
||||||
|
* - {@link TabMode}
|
||||||
|
*/
|
||||||
|
mode?: PageTabMode;
|
||||||
|
/**
|
||||||
|
* The common class of the layout
|
||||||
|
*
|
||||||
|
* Is can be used to configure the transition animation
|
||||||
|
*
|
||||||
|
* @default 'transition-all-300'
|
||||||
|
*/
|
||||||
|
commonClass?: string;
|
||||||
|
/** The class of the button tab */
|
||||||
|
buttonClass?: string;
|
||||||
|
/** The class of the chrome tab */
|
||||||
|
chromeClass?: string;
|
||||||
|
/** Whether the tab is active */
|
||||||
|
active?: boolean;
|
||||||
|
/** The color of the active tab */
|
||||||
|
activeColor?: string;
|
||||||
|
/**
|
||||||
|
* Whether the tab is closable
|
||||||
|
*
|
||||||
|
* Show the close icon when true
|
||||||
|
*/
|
||||||
|
closable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageTabCssVarsProps = {
|
||||||
|
primaryColor: string;
|
||||||
|
primaryColor1: string;
|
||||||
|
primaryColor2: string;
|
||||||
|
primaryColorOpacity1: string;
|
||||||
|
primaryColorOpacity2: string;
|
||||||
|
primaryColorOpacity3: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageTabCssVars = {
|
||||||
|
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||||
|
};
|
20
packages/materials/tsconfig.json
Normal file
20
packages/materials/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
15
packages/ofetch/package.json
Normal file
15
packages/ofetch/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/fetch",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ofetch": "1.4.1"
|
||||||
|
}
|
||||||
|
}
|
10
packages/ofetch/src/index.ts
Normal file
10
packages/ofetch/src/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ofetch } from 'ofetch';
|
||||||
|
import type { FetchOptions } from 'ofetch';
|
||||||
|
|
||||||
|
export function createRequest(options: FetchOptions) {
|
||||||
|
const request = ofetch.create(options);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createRequest;
|
20
packages/ofetch/tsconfig.json
Normal file
20
packages/ofetch/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
3
packages/scripts/bin.ts
Normal file
3
packages/scripts/bin.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import './src/index.ts';
|
27
packages/scripts/package.json
Normal file
27
packages/scripts/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/scripts",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"bin": {
|
||||||
|
"sa": "./bin.ts"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@soybeanjs/changelog": "0.3.24",
|
||||||
|
"bumpp": "10.1.0",
|
||||||
|
"c12": "3.0.2",
|
||||||
|
"cac": "6.7.14",
|
||||||
|
"consola": "3.4.2",
|
||||||
|
"enquirer": "2.4.1",
|
||||||
|
"execa": "9.5.2",
|
||||||
|
"kolorist": "1.8.0",
|
||||||
|
"npm-check-updates": "17.1.15",
|
||||||
|
"rimraf": "6.0.1"
|
||||||
|
}
|
||||||
|
}
|
10
packages/scripts/src/commands/changelog.ts
Normal file
10
packages/scripts/src/commands/changelog.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog';
|
||||||
|
import type { ChangelogOption } from '@soybeanjs/changelog';
|
||||||
|
|
||||||
|
export async function genChangelog(options?: Partial<ChangelogOption>, total = false) {
|
||||||
|
if (total) {
|
||||||
|
await generateTotalChangelog(options);
|
||||||
|
} else {
|
||||||
|
await generateChangelog(options);
|
||||||
|
}
|
||||||
|
}
|
5
packages/scripts/src/commands/cleanup.ts
Normal file
5
packages/scripts/src/commands/cleanup.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { rimraf } from 'rimraf';
|
||||||
|
|
||||||
|
export async function cleanup(paths: string[]) {
|
||||||
|
await rimraf(paths, { glob: true });
|
||||||
|
}
|
84
packages/scripts/src/commands/git-commit.ts
Normal file
84
packages/scripts/src/commands/git-commit.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { execCommand } from '../shared';
|
||||||
|
import { locales } from '../locales';
|
||||||
|
import type { Lang } from '../locales';
|
||||||
|
|
||||||
|
interface PromptObject {
|
||||||
|
types: string;
|
||||||
|
scopes: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git commit with Conventional Commits standard
|
||||||
|
*
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
export async function gitCommit(lang: Lang = 'en-us') {
|
||||||
|
const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
|
||||||
|
|
||||||
|
const typesChoices = gitCommitTypes.map(([value, msg]) => {
|
||||||
|
const nameWithSuffix = `${value}:`;
|
||||||
|
|
||||||
|
const message = `${nameWithSuffix.padEnd(12)}${msg}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: value,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
|
||||||
|
name: value,
|
||||||
|
message: `${value.padEnd(30)} (${msg})`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await prompt<PromptObject>([
|
||||||
|
{
|
||||||
|
name: 'types',
|
||||||
|
type: 'select',
|
||||||
|
message: gitCommitMessages.types,
|
||||||
|
choices: typesChoices
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scopes',
|
||||||
|
type: 'select',
|
||||||
|
message: gitCommitMessages.scopes,
|
||||||
|
choices: scopesChoices
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'text',
|
||||||
|
message: gitCommitMessages.description
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const breaking = result.description.startsWith('!') ? '!' : '';
|
||||||
|
|
||||||
|
const description = result.description.replace(/^!/, '').trim();
|
||||||
|
|
||||||
|
const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`;
|
||||||
|
|
||||||
|
await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Git commit message verify */
|
||||||
|
export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) {
|
||||||
|
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
|
||||||
|
|
||||||
|
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
|
||||||
|
|
||||||
|
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
|
||||||
|
|
||||||
|
if (ignores.some(regExp => regExp.test(commitMsg))) return;
|
||||||
|
|
||||||
|
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
||||||
|
|
||||||
|
if (!REG_EXP.test(commitMsg)) {
|
||||||
|
const errorMsg = locales[lang].gitCommitVerify;
|
||||||
|
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
6
packages/scripts/src/commands/index.ts
Normal file
6
packages/scripts/src/commands/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './git-commit';
|
||||||
|
export * from './cleanup';
|
||||||
|
export * from './update-pkg';
|
||||||
|
export * from './changelog';
|
||||||
|
export * from './release';
|
||||||
|
export * from './router';
|
12
packages/scripts/src/commands/release.ts
Normal file
12
packages/scripts/src/commands/release.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { versionBump } from 'bumpp';
|
||||||
|
|
||||||
|
export async function release(execute = 'pnpm sa changelog', push = true) {
|
||||||
|
await versionBump({
|
||||||
|
files: ['**/package.json', '!**/node_modules'],
|
||||||
|
execute,
|
||||||
|
all: true,
|
||||||
|
tag: true,
|
||||||
|
commit: 'chore(projects): release v%s',
|
||||||
|
push
|
||||||
|
});
|
||||||
|
}
|
90
packages/scripts/src/commands/router.ts
Normal file
90
packages/scripts/src/commands/router.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { green, red } from 'kolorist';
|
||||||
|
|
||||||
|
interface PromptObject {
|
||||||
|
routeName: string;
|
||||||
|
addRouteParams: boolean;
|
||||||
|
routeParams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** generate route */
|
||||||
|
export async function generateRoute() {
|
||||||
|
const result = await prompt<PromptObject>([
|
||||||
|
{
|
||||||
|
name: 'routeName',
|
||||||
|
type: 'text',
|
||||||
|
message: 'please enter route name',
|
||||||
|
initial: 'demo-route_child'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'addRouteParams',
|
||||||
|
type: 'confirm',
|
||||||
|
message: 'add route params?',
|
||||||
|
initial: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.addRouteParams) {
|
||||||
|
const answers = await prompt<PromptObject>({
|
||||||
|
name: 'routeParams',
|
||||||
|
type: 'text',
|
||||||
|
message: 'please enter route params',
|
||||||
|
initial: 'id'
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(result, answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/;
|
||||||
|
|
||||||
|
if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) {
|
||||||
|
throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}.
|
||||||
|
For example:
|
||||||
|
(1) one level route: ${green('demo-route')}
|
||||||
|
(2) two level route: ${green('demo-route_child')}
|
||||||
|
(3) multi level route: ${green('demo-route_child_child')}
|
||||||
|
(4) group route: ${green('_ignore_demo-route')}'
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARAM_REG = /^\w+$/g;
|
||||||
|
|
||||||
|
if (result.routeParams && !PARAM_REG.test(result.routeParams)) {
|
||||||
|
throw new Error(red('route params is invalid, it only allow letters, numbers or "_".'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const [dir, ...rest] = result.routeName.split('_') as string[];
|
||||||
|
|
||||||
|
let routeDir = path.join(cwd, 'src', 'views', dir);
|
||||||
|
|
||||||
|
if (rest.length) {
|
||||||
|
routeDir = path.join(routeDir, rest.join('_'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(routeDir)) {
|
||||||
|
mkdirSync(routeDir, { recursive: true });
|
||||||
|
} else {
|
||||||
|
throw new Error(red('route already exists'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue';
|
||||||
|
|
||||||
|
const vueTemplate = `<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>${result.routeName}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const filePath = path.join(routeDir, fileName);
|
||||||
|
|
||||||
|
await writeFile(filePath, vueTemplate);
|
||||||
|
}
|
5
packages/scripts/src/commands/update-pkg.ts
Normal file
5
packages/scripts/src/commands/update-pkg.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { execCommand } from '../shared';
|
||||||
|
|
||||||
|
export async function updatePkg(args: string[] = ['--deep', '-u']) {
|
||||||
|
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
|
||||||
|
}
|
39
packages/scripts/src/config/index.ts
Normal file
39
packages/scripts/src/config/index.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import { loadConfig } from 'c12';
|
||||||
|
import type { CliOption } from '../types';
|
||||||
|
|
||||||
|
const defaultOptions: CliOption = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
cleanupDirs: [
|
||||||
|
'**/dist',
|
||||||
|
'**/package-lock.json',
|
||||||
|
'**/yarn.lock',
|
||||||
|
'**/pnpm-lock.yaml',
|
||||||
|
'**/node_modules',
|
||||||
|
'!node_modules/**'
|
||||||
|
],
|
||||||
|
ncuCommandArgs: ['--deep', '-u'],
|
||||||
|
changelogOptions: {},
|
||||||
|
gitCommitVerifyIgnores: [
|
||||||
|
/^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
|
||||||
|
/^(Merge tag (.*?))(?:\r?\n)*$/m,
|
||||||
|
/^(R|r)evert (.*)/,
|
||||||
|
/^(amend|fixup|squash)!/,
|
||||||
|
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
|
||||||
|
/^Merge remote-tracking branch(\s*)(.*)/,
|
||||||
|
/^Automatic merge(.*)/,
|
||||||
|
/^Auto-merged (.*?) into (.*)/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {
|
||||||
|
const { config } = await loadConfig<Partial<CliOption>>({
|
||||||
|
name: 'soybean',
|
||||||
|
defaults: defaultOptions,
|
||||||
|
overrides,
|
||||||
|
cwd,
|
||||||
|
packageJson: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return config as CliOption;
|
||||||
|
}
|
109
packages/scripts/src/index.ts
Normal file
109
packages/scripts/src/index.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import cac from 'cac';
|
||||||
|
import { blue, lightGreen } from 'kolorist';
|
||||||
|
import { version } from '../package.json';
|
||||||
|
import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
|
||||||
|
import { loadCliOptions } from './config';
|
||||||
|
import type { Lang } from './locales';
|
||||||
|
|
||||||
|
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
|
||||||
|
|
||||||
|
type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
|
||||||
|
|
||||||
|
type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
|
||||||
|
|
||||||
|
interface CommandArg {
|
||||||
|
/** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */
|
||||||
|
execute?: string;
|
||||||
|
/** Indicates whether to push the git commit and tag. Defaults to true */
|
||||||
|
push?: boolean;
|
||||||
|
/** Generate changelog by total tags */
|
||||||
|
total?: boolean;
|
||||||
|
/**
|
||||||
|
* The glob pattern of dirs to clean up
|
||||||
|
*
|
||||||
|
* If not set, it will use the default value
|
||||||
|
*
|
||||||
|
* Multiple values use "," to separate them
|
||||||
|
*/
|
||||||
|
cleanupDir?: string;
|
||||||
|
/**
|
||||||
|
* display lang of cli
|
||||||
|
*
|
||||||
|
* @default 'en-us'
|
||||||
|
*/
|
||||||
|
lang?: Lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupCli() {
|
||||||
|
const cliOptions = await loadCliOptions();
|
||||||
|
|
||||||
|
const cli = cac(blue('soybean-admin'));
|
||||||
|
|
||||||
|
cli
|
||||||
|
.version(lightGreen(version))
|
||||||
|
.option(
|
||||||
|
'-e, --execute [command]',
|
||||||
|
"Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'"
|
||||||
|
)
|
||||||
|
.option('-p, --push', 'Indicates whether to push the git commit and tag')
|
||||||
|
.option('-t, --total', 'Generate changelog by total tags')
|
||||||
|
.option(
|
||||||
|
'-c, --cleanupDir <dir>',
|
||||||
|
'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
|
||||||
|
)
|
||||||
|
.option('-l, --lang <lang>', 'display lang of cli', { default: 'en-us', type: [String] })
|
||||||
|
.help();
|
||||||
|
|
||||||
|
const commands: CommandWithAction<CommandArg> = {
|
||||||
|
cleanup: {
|
||||||
|
desc: 'delete dirs: node_modules, dist, etc.',
|
||||||
|
action: async () => {
|
||||||
|
await cleanup(cliOptions.cleanupDirs);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'update-pkg': {
|
||||||
|
desc: 'update package.json dependencies versions',
|
||||||
|
action: async () => {
|
||||||
|
await updatePkg(cliOptions.ncuCommandArgs);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'git-commit': {
|
||||||
|
desc: 'git commit, generate commit message which match Conventional Commits standard',
|
||||||
|
action: async args => {
|
||||||
|
await gitCommit(args?.lang);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'git-commit-verify': {
|
||||||
|
desc: 'verify git commit message, make sure it match Conventional Commits standard',
|
||||||
|
action: async args => {
|
||||||
|
await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changelog: {
|
||||||
|
desc: 'generate changelog',
|
||||||
|
action: async args => {
|
||||||
|
await genChangelog(cliOptions.changelogOptions, args?.total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
release: {
|
||||||
|
desc: 'release: update version, generate changelog, commit code',
|
||||||
|
action: async args => {
|
||||||
|
await release(args?.execute, args?.push);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'gen-route': {
|
||||||
|
desc: 'generate route',
|
||||||
|
action: async () => {
|
||||||
|
await generateRoute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [command, { desc, action }] of Object.entries(commands)) {
|
||||||
|
cli.command(command, lightGreen(desc)).action(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCli();
|
82
packages/scripts/src/locales/index.ts
Normal file
82
packages/scripts/src/locales/index.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { bgRed, green, red, yellow } from 'kolorist';
|
||||||
|
|
||||||
|
export type Lang = 'zh-cn' | 'en-us';
|
||||||
|
|
||||||
|
export const locales = {
|
||||||
|
'zh-cn': {
|
||||||
|
gitCommitMessages: {
|
||||||
|
types: '请选择提交类型',
|
||||||
|
scopes: '请选择提交范围',
|
||||||
|
description: `请输入描述信息(${yellow('!')}开头表示破坏性改动`
|
||||||
|
},
|
||||||
|
gitCommitTypes: [
|
||||||
|
['feat', '新功能'],
|
||||||
|
['feat-wip', '开发中的功能,比如某功能的部分代码'],
|
||||||
|
['fix', '修复Bug'],
|
||||||
|
['docs', '只涉及文档更新'],
|
||||||
|
['typo', '代码或文档勘误,比如错误拼写'],
|
||||||
|
['style', '修改代码风格,不影响代码含义的变更'],
|
||||||
|
['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
|
||||||
|
['perf', '可提高性能的代码更改'],
|
||||||
|
['optimize', '优化代码质量的代码更改'],
|
||||||
|
['test', '添加缺失的测试或更正现有测试'],
|
||||||
|
['build', '影响构建系统或外部依赖项的更改'],
|
||||||
|
['ci', '对 CI 配置文件和脚本的更改'],
|
||||||
|
['chore', '没有修改src或测试文件的其他变更'],
|
||||||
|
['revert', '还原先前的提交']
|
||||||
|
] as [string, string][],
|
||||||
|
gitCommitScopes: [
|
||||||
|
['projects', '项目'],
|
||||||
|
['packages', '包'],
|
||||||
|
['components', '组件'],
|
||||||
|
['hooks', '钩子函数'],
|
||||||
|
['utils', '工具函数'],
|
||||||
|
['types', 'TS类型声明'],
|
||||||
|
['styles', '代码风格'],
|
||||||
|
['deps', '项目依赖'],
|
||||||
|
['release', '发布项目新版本'],
|
||||||
|
['other', '其他的变更']
|
||||||
|
] as [string, string][],
|
||||||
|
gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green(
|
||||||
|
'推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org'
|
||||||
|
)}`
|
||||||
|
},
|
||||||
|
'en-us': {
|
||||||
|
gitCommitMessages: {
|
||||||
|
types: 'Please select a type',
|
||||||
|
scopes: 'Please select a scope',
|
||||||
|
description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
|
||||||
|
},
|
||||||
|
gitCommitTypes: [
|
||||||
|
['feat', 'A new feature'],
|
||||||
|
['feat-wip', 'Features in development, such as partial code for a certain feature'],
|
||||||
|
['fix', 'A bug fix'],
|
||||||
|
['docs', 'Documentation only changes'],
|
||||||
|
['typo', 'Code or document corrections, such as spelling errors'],
|
||||||
|
['style', 'Changes that do not affect the meaning of the code'],
|
||||||
|
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
|
||||||
|
['perf', 'A code change that improves performance'],
|
||||||
|
['optimize', 'A code change that optimizes code quality'],
|
||||||
|
['test', 'Adding missing tests or correcting existing tests'],
|
||||||
|
['build', 'Changes that affect the build system or external dependencies'],
|
||||||
|
['ci', 'Changes to our CI configuration files and scripts'],
|
||||||
|
['chore', "Other changes that don't modify src or test files"],
|
||||||
|
['revert', 'Reverts a previous commit']
|
||||||
|
] as [string, string][],
|
||||||
|
gitCommitScopes: [
|
||||||
|
['projects', 'project'],
|
||||||
|
['packages', 'packages'],
|
||||||
|
['components', 'components'],
|
||||||
|
['hooks', 'hook functions'],
|
||||||
|
['utils', 'utils functions'],
|
||||||
|
['types', 'TS declaration'],
|
||||||
|
['styles', 'style'],
|
||||||
|
['deps', 'project dependencies'],
|
||||||
|
['release', 'release project'],
|
||||||
|
['other', 'other changes']
|
||||||
|
] as [string, string][],
|
||||||
|
gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
|
||||||
|
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
} satisfies Record<Lang, Record<string, unknown>>;
|
7
packages/scripts/src/shared/index.ts
Normal file
7
packages/scripts/src/shared/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { Options } from 'execa';
|
||||||
|
|
||||||
|
export async function execCommand(cmd: string, args: string[], options?: Options) {
|
||||||
|
const { execa } = await import('execa');
|
||||||
|
const res = await execa(cmd, args, options);
|
||||||
|
return (res?.stdout as string)?.trim() || '';
|
||||||
|
}
|
31
packages/scripts/src/types/index.ts
Normal file
31
packages/scripts/src/types/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { ChangelogOption } from '@soybeanjs/changelog';
|
||||||
|
|
||||||
|
export interface CliOption {
|
||||||
|
/** The project root directory */
|
||||||
|
cwd: string;
|
||||||
|
/**
|
||||||
|
* Cleanup dirs
|
||||||
|
*
|
||||||
|
* Glob pattern syntax {@link https://github.com/isaacs/minimatch}
|
||||||
|
*
|
||||||
|
* @default
|
||||||
|
* ```json
|
||||||
|
* ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
cleanupDirs: string[];
|
||||||
|
/**
|
||||||
|
* Npm-check-updates command args
|
||||||
|
*
|
||||||
|
* @default ['--deep', '-u']
|
||||||
|
*/
|
||||||
|
ncuCommandArgs: string[];
|
||||||
|
/**
|
||||||
|
* Options of generate changelog
|
||||||
|
*
|
||||||
|
* @link https://github.com/soybeanjs/changelog
|
||||||
|
*/
|
||||||
|
changelogOptions: Partial<ChangelogOption>;
|
||||||
|
/** The ignore pattern list of git commit verify */
|
||||||
|
gitCommitVerifyIgnores: RegExp[];
|
||||||
|
}
|
20
packages/scripts/tsconfig.json
Normal file
20
packages/scripts/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "typings/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
12
packages/uno-preset/package.json
Normal file
12
packages/uno-preset/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@sa/uno-preset",
|
||||||
|
"version": "1.3.13",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user