mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-17 01:06:38 +08:00
feat(projects): 1.0 beta
This commit is contained in:
parent
1ea4817f6a
commit
e918a2c0f5
@ -1,11 +0,0 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
27
.env
27
.env
@ -1,20 +1,21 @@
|
||||
VITE_BASE_URL=/
|
||||
|
||||
VITE_APP_NAME=SoybeanAdmin
|
||||
VITE_APP_TITLE=SoybeanAdmin
|
||||
|
||||
VITE_APP_TITLE=Soybean管理系统
|
||||
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
|
||||
|
||||
VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
||||
|
||||
# 权限路由模式: static | dynamic
|
||||
VITE_AUTH_ROUTE_MODE=static
|
||||
|
||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||
|
||||
# iconify图标作为组件的前缀
|
||||
# the prefix of the icon name
|
||||
VITE_ICON_PREFIX=icon
|
||||
|
||||
# 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFIX
|
||||
# 格式 {VITE_ICON_PREFIX}-{本地图标集合名称}
|
||||
# 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=static
|
||||
|
||||
# static auth route home
|
||||
VITE_ROUTE_HOME=home
|
||||
|
||||
# default menu icon
|
||||
VITE_MENU_ICON=mdi:menu
|
@ -1,30 +0,0 @@
|
||||
/** 请求服务的环境配置 */
|
||||
type ServiceEnv = Record<ServiceEnvType, ServiceEnvConfig>;
|
||||
|
||||
/** 不同请求服务的环境配置 */
|
||||
const serviceEnv: ServiceEnv = {
|
||||
dev: {
|
||||
url: 'http://localhost:8080'
|
||||
},
|
||||
test: {
|
||||
url: 'http://localhost:8080'
|
||||
},
|
||||
prod: {
|
||||
url: 'http://localhost:8080'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境模式下的请求服务的配置
|
||||
* @param env 环境
|
||||
*/
|
||||
export function getServiceEnvConfig(env: ImportMetaEnv): ServiceEnvConfigWithProxyPattern {
|
||||
const { VITE_SERVICE_ENV = 'dev' } = env;
|
||||
|
||||
const config = serviceEnv[VITE_SERVICE_ENV];
|
||||
|
||||
return {
|
||||
...config,
|
||||
proxyPattern: '/proxy-pattern'
|
||||
};
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
VITE_HTTP_PROXY=Y
|
||||
VITE_SOYBEAN_ROUTE_PLUGIN=Y
|
||||
|
||||
|
@ -1,10 +1,2 @@
|
||||
VITE_VISUALIZER=N
|
||||
|
||||
VITE_COMPRESS=N
|
||||
|
||||
# gzip | brotliCompress | deflate | deflateRaw
|
||||
VITE_COMPRESS_TYPE=gzip
|
||||
|
||||
VITE_PWA=N
|
||||
|
||||
VITE_PROD_MOCK=Y
|
||||
VITE_ROUTER_HISTORY_MODE=history
|
||||
VITE_SOURCE_MAP=N
|
@ -1,3 +0,0 @@
|
||||
!.env-config.ts
|
||||
router-page.d.ts
|
||||
|
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "sa/vue",
|
||||
"settings": {
|
||||
"import/core-modules": ["uno.css", "~icons/*", "virtual:svg-icons-register"]
|
||||
}
|
||||
}
|
133
.eslintrc.js
133
.eslintrc.js
@ -1,133 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['soybeanjs/vue'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['./scripts/*.ts'],
|
||||
rules: {
|
||||
'no-unused-expressions': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off', // use tsc to check the ts code of the vue
|
||||
'vue/no-setup-props-destructure': 'off' // wait to fix this rule
|
||||
}
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
'import/core-modules': ['uno.css', '~icons/*', 'virtual:svg-icons-register']
|
||||
},
|
||||
rules: {
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'never',
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: 'vue',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'vue-router',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'pinia',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'naive-ui',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/constants',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/config',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/settings',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/plugins',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/layouts',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/views',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/components',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/router',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/service',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/store',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/context',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/composables',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/hooks',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/utils',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/assets',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/**',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
}
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'pinia', 'naive-ui']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
17
.gitattributes
vendored
17
.gitattributes
vendored
@ -1,17 +0,0 @@
|
||||
"*.vue" eol=lf
|
||||
"*.js" eol=lf
|
||||
"*.ts" eol=lf
|
||||
"*.jsx" eol=lf
|
||||
"*.tsx" eol=lf
|
||||
"*.cjs" eol=lf
|
||||
"*.cts" eol=lf
|
||||
"*.mjs" eol=lf
|
||||
"*.mts" eol=lf
|
||||
"*.json" eol=lf
|
||||
"*.html" eol=lf
|
||||
"*.css" eol=lf
|
||||
"*.less" eol=lf
|
||||
"*.scss" eol=lf
|
||||
"*.sass" eol=lf
|
||||
"*.styl" eol=lf
|
||||
"*.md" eol=lf
|
90
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
90
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@ -1,90 +0,0 @@
|
||||
name: Bug提交
|
||||
description: 在使用软件或功能的过程中遇到了错误
|
||||
title: '[Bug]: '
|
||||
labels: [ "bug?" ]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 请按照以下要求进行提交
|
||||
### 1. 提交后需要指定标签和截止时间。
|
||||
---
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 环境信息
|
||||
请根据实际使用环境修改以下信息。
|
||||
|
||||
- type: input
|
||||
id: env-program-ver
|
||||
attributes:
|
||||
label: 软件版本
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-ver
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: 选择运行软件的系统版本
|
||||
options:
|
||||
- Windows (64)
|
||||
- Windows (32/x84)
|
||||
- MacOS
|
||||
- Linux
|
||||
- Ubuntu
|
||||
- CentOS
|
||||
- ArchLinux
|
||||
- UNIX (Android)
|
||||
- 其它(请在下方说明)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-arch
|
||||
attributes:
|
||||
label: 运行架构
|
||||
description: (可选) 选择运行软件的系统架构
|
||||
options:
|
||||
- AMD64
|
||||
- x86
|
||||
- ARM [32] (别名:AArch32 / ARMv7)
|
||||
- ARM [64] (别名:AArch64 / ARMv8)
|
||||
- 其它
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: |
|
||||
我们需要执行哪些操作才能让 bug 出现?
|
||||
简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望的结果是什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际的结果是什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logging
|
||||
attributes:
|
||||
label: 日志记录(可选)
|
||||
render: golang
|
||||
|
||||
- type: textarea
|
||||
id: extra-desc
|
||||
attributes:
|
||||
label: 补充说明(可选)
|
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,11 +0,0 @@
|
||||
## Pull Request 详情
|
||||
|
||||
请根据实际使用情况修改以下信息。
|
||||
|
||||
## 版本信息
|
||||
|
||||
## 解决了哪些问题
|
||||
|
||||
## 是否关闭了某个 Issue
|
||||
|
||||
Closes #
|
30
.github/workflows/linter.yml
vendored
30
.github/workflows/linter.yml
vendored
@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Lint Code
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint All Code
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Lint Code Base
|
||||
uses: github/super-linter@v4
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
# To change branch master or main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FILTER_REGEX_EXCLUDE: (docs|.github)
|
||||
VALIDATE_MARKDOWN: false
|
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@ -1,25 +0,0 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- run: npx githublogen
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -11,10 +11,8 @@ node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dist.zip
|
||||
coverage
|
||||
*.local
|
||||
stats.html
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@ -22,8 +20,8 @@ stats.html
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/launch.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
@ -31,7 +29,7 @@ stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/src/typings/components.d.ts
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
.VSCodeCounter
|
||||
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -8,11 +8,9 @@
|
||||
"formulahendry.auto-close-tag",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"lokalise.i18n-ally",
|
||||
"mariusalchimavicius.json-to-ts",
|
||||
"mhutchie.git-graph",
|
||||
"sdras.vue-vscode-snippets",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -5,7 +5,7 @@
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Vue debugger",
|
||||
"url": "http://localhost:3200",
|
||||
"url": "http://localhost:9527",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
|
81
.vscode/settings.json
vendored
81
.vscode/settings.json
vendored
@ -1,51 +1,46 @@
|
||||
{
|
||||
"cSpell.ignorePaths": [
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"node_modules",
|
||||
"vscode-extension",
|
||||
".git/objects",
|
||||
".vscode",
|
||||
".vscode-insiders",
|
||||
"CHANGELOG.md",
|
||||
"dist",
|
||||
"public",
|
||||
"styles"
|
||||
],
|
||||
"cSpell.words": [
|
||||
"AMAP",
|
||||
"antdesign",
|
||||
"antv",
|
||||
"apacheecharts",
|
||||
"areaspline",
|
||||
"bmapgl",
|
||||
"apifox",
|
||||
"clickoutside",
|
||||
"clsx",
|
||||
"colord",
|
||||
"echarts",
|
||||
"consola",
|
||||
"Destructurable",
|
||||
"EDITMSG",
|
||||
"espree",
|
||||
"execa",
|
||||
"gitee",
|
||||
"gridicons",
|
||||
"heroicons",
|
||||
"HEXA",
|
||||
"hexcode",
|
||||
"iconify",
|
||||
"jsapi",
|
||||
"naiveui",
|
||||
"Popconfirm",
|
||||
"Posva",
|
||||
"Shenzhen",
|
||||
"Sider",
|
||||
"INDEXEDDB",
|
||||
"jiti",
|
||||
"kolorist",
|
||||
"Laba",
|
||||
"localforage",
|
||||
"LOCALSTORAGE",
|
||||
"majesticons",
|
||||
"MEDZ",
|
||||
"nocheck",
|
||||
"nprogress",
|
||||
"ofetch",
|
||||
"pickr",
|
||||
"preflights",
|
||||
"sider",
|
||||
"simonwep",
|
||||
"simplebar",
|
||||
"tada",
|
||||
"tauri",
|
||||
"Uncapitalize",
|
||||
"unocss",
|
||||
"unplugin",
|
||||
"vditor",
|
||||
"VERCEL",
|
||||
"Vite",
|
||||
"vitejs",
|
||||
"vuedraggable",
|
||||
"VITE",
|
||||
"vitepress",
|
||||
"vueuse",
|
||||
"wangeditor",
|
||||
"wechat",
|
||||
"xgplayer",
|
||||
"yanbowe",
|
||||
"ភាសាខ្មែរ"
|
||||
"WEBSQL",
|
||||
"wechat"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
@ -56,22 +51,20 @@
|
||||
"strings": true
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"eslint.validate": ["json"],
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv",
|
||||
"*.svg": "html"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"i18n-ally.displayLanguage": "zh-CN",
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.editor.preferEditor": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["src/locales/lang"],
|
||||
"material-icon-theme.activeIconPack": "vue",
|
||||
"[html][css][less][scss][sass][markdown][yaml][yml][jsonc]": {
|
||||
"unocss.root": ["./"],
|
||||
"[html][css][less][scss][sass][markdown][yaml][yml][json][jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"prettier": {}
|
||||
}
|
||||
}
|
||||
|
1275
CHANGELOG.md
1275
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
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.
|
16
Makefile
16
Makefile
@ -1,16 +0,0 @@
|
||||
ImageTag ?=v0.9.6
|
||||
SoybeanAdminImg ?= soybeanjs/soybean-admin:$(ImageTag)
|
||||
|
||||
VERSION=$(shell git rev-parse --short HEAD)
|
||||
|
||||
soybean-admin: soybean-admin-build soybean-admin-push
|
||||
|
||||
soybean-admin-build:
|
||||
docker build --build-arg version=$(VERSION) -t ${SoybeanAdminImg} -f docker/Dockerfile .
|
||||
|
||||
soybean-admin-push:
|
||||
docker push ${SoybeanAdminImg}
|
||||
|
||||
# run tauri app:
|
||||
run:
|
||||
pnpm tauri dev
|
@ -1,8 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/** 项目构建时间 */
|
||||
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
export const viteDefine = {
|
||||
PROJECT_BUILD_TIME
|
||||
};
|
@ -1,2 +1 @@
|
||||
export * from './define';
|
||||
export * from './proxy';
|
||||
|
@ -1,20 +1,38 @@
|
||||
import type { ProxyOptions } from 'vite';
|
||||
import { createServiceConfig, createProxyPattern } from '../../env.config';
|
||||
|
||||
/**
|
||||
* 设置网络代理
|
||||
* @param isOpenProxy - 是否开启代理
|
||||
* @param envConfig - env环境配置
|
||||
* set http proxy
|
||||
* @param env - the current env
|
||||
*/
|
||||
export function createViteProxy(isOpenProxy: boolean, envConfig: ServiceEnvConfigWithProxyPattern) {
|
||||
if (!isOpenProxy) return undefined;
|
||||
export function createViteProxy(env: Env.ImportMeta) {
|
||||
const isEnableHttpProxy = env.VITE_HTTP_PROXY === 'Y';
|
||||
|
||||
const proxy: Record<string, string | ProxyOptions> = {
|
||||
[envConfig.proxyPattern]: {
|
||||
target: envConfig.url,
|
||||
if (!isEnableHttpProxy) return undefined;
|
||||
|
||||
const { baseURL, otherBaseURL } = createServiceConfig(env);
|
||||
|
||||
const defaultProxyPattern = createProxyPattern();
|
||||
|
||||
const proxy: Record<string, ProxyOptions> = {
|
||||
[defaultProxyPattern]: {
|
||||
target: baseURL,
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(new RegExp(`^${envConfig.proxyPattern}`), '')
|
||||
rewrite: path => path.replace(new RegExp(`^${defaultProxyPattern}`), '')
|
||||
}
|
||||
};
|
||||
|
||||
const otherURLEntries = Object.entries(otherBaseURL);
|
||||
|
||||
for (const [key, url] of otherURLEntries) {
|
||||
const proxyPattern = createProxyPattern(key);
|
||||
|
||||
proxy[proxyPattern] = {
|
||||
target: url,
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(new RegExp(`^${proxyPattern}`), '')
|
||||
};
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
export * from './plugins';
|
||||
export * from './config';
|
||||
export * from './utils';
|
@ -1,6 +0,0 @@
|
||||
import ViteCompression from 'vite-plugin-compression';
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv;
|
||||
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE });
|
||||
};
|
@ -1,22 +1,14 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import unocss from '@unocss/vite';
|
||||
import progress from 'vite-plugin-progress';
|
||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
||||
import unplugin from './unplugin';
|
||||
import mock from './mock';
|
||||
import visualizer from './visualizer';
|
||||
import compress from './compress';
|
||||
import pwa from './pwa';
|
||||
import progress from 'vite-plugin-progress';
|
||||
import { setupElegantRouter } from './router';
|
||||
import { setupUnocss } from './unocss';
|
||||
import { setupUnplugin } from './unplugin';
|
||||
|
||||
/**
|
||||
* vite插件
|
||||
* @param viteEnv - 环境变量配置
|
||||
*/
|
||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [
|
||||
export function setupVitePlugins(viteEnv: Env.ImportMeta) {
|
||||
const plugins: PluginOption = [
|
||||
vue({
|
||||
script: {
|
||||
defineModel: true
|
||||
@ -24,24 +16,11 @@ export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugin
|
||||
}),
|
||||
vueJsx(),
|
||||
VueDevtools(),
|
||||
...unplugin(viteEnv),
|
||||
unocss(),
|
||||
mock(viteEnv),
|
||||
setupElegantRouter(),
|
||||
setupUnocss(viteEnv),
|
||||
...setupUnplugin(viteEnv),
|
||||
progress()
|
||||
];
|
||||
|
||||
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||
plugins.push(visualizer as PluginOption);
|
||||
}
|
||||
if (viteEnv.VITE_COMPRESS === 'Y') {
|
||||
plugins.push(compress(viteEnv));
|
||||
}
|
||||
if (viteEnv.VITE_PWA === 'Y' || viteEnv.VITE_VERCEL === 'Y') {
|
||||
plugins.push(pwa());
|
||||
}
|
||||
if (viteEnv.VITE_SOYBEAN_ROUTE_PLUGIN === 'Y') {
|
||||
plugins.push(pageRoute());
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { viteMockServe } from 'vite-plugin-mock';
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
const prodMock = viteEnv.VITE_PROD_MOCK === 'Y';
|
||||
|
||||
return viteMockServe({
|
||||
mockPath: 'mock',
|
||||
prodEnabled: prodMock,
|
||||
injectCode: `
|
||||
import { setupMockServer } from '../mock';
|
||||
setupMockServer();
|
||||
`
|
||||
});
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default function setupVitePwa() {
|
||||
return VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.ico'],
|
||||
manifest: {
|
||||
name: 'SoybeanAdmin',
|
||||
short_name: 'SoybeanAdmin',
|
||||
theme_color: '#fff',
|
||||
icons: [
|
||||
{
|
||||
src: '/logo.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/logo.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/logo.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
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;
|
||||
}
|
||||
});
|
||||
}
|
33
build/plugins/unocss.ts
Normal file
33
build/plugins/unocss.ts
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
import path from 'node:path';
|
||||
import type { PluginOption } from 'vite';
|
||||
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 { AntDesignVueResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import { getSrcPath } from '../utils';
|
||||
|
||||
export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||
|
||||
const srcPath = getSrcPath();
|
||||
const localIconPath = `${srcPath}/assets/svg-icon`;
|
||||
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||
|
||||
/** 本地svg图标集合名称 */
|
||||
/**
|
||||
* the name of the local icon collection
|
||||
*/
|
||||
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||
|
||||
return [
|
||||
const plugins: PluginOption[] = [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
@ -30,6 +32,9 @@ export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||
dts: 'src/typings/components.d.ts',
|
||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: false
|
||||
}),
|
||||
NaiveUiResolver(),
|
||||
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||
]
|
||||
@ -41,4 +46,6 @@ export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||
customDomId: '__SVG_ICON_LOCAL__'
|
||||
})
|
||||
];
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default visualizer({
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
open: true
|
||||
});
|
@ -1,20 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 获取项目根路径
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目src路径
|
||||
* @param srcName - src目录名称(默认: "src")
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
const rootPath = getRootPath();
|
||||
|
||||
return `${rootPath}/${srcName}`;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
.npmrc
|
||||
.cache
|
||||
|
||||
tests/server/static
|
||||
tests/server/static/upload
|
||||
|
||||
.local
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.eslintcache
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
# .vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
/vite-profile.cpuprofile
|
@ -1,24 +0,0 @@
|
||||
FROM node:16.17.0 as builder
|
||||
|
||||
ENV WORKDIR=/soybean-admin
|
||||
|
||||
WORKDIR $WORKDIR
|
||||
|
||||
COPY ./ $WORKDIR/
|
||||
|
||||
ARG version
|
||||
ENV COMMITID=$version
|
||||
|
||||
RUN npm i -g pnpm
|
||||
|
||||
RUN pnpm install
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:alpine as prod
|
||||
|
||||
RUN mkdir /soybean
|
||||
|
||||
COPY --from=builder /soybean-admin/dist /soybean-admin
|
||||
COPY --from=builder /soybean-admin/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
@ -1,54 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
# 不缓存html,防止程序更新后缓存继续生效
|
||||
if ($request_filename ~* .*\.(?:htm|html)$) {
|
||||
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||
access_log on;
|
||||
}
|
||||
root /soybean-admin/;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# location /soybean/soybean-webserver/v1 {
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
|
||||
# # 后台接口地址
|
||||
# proxy_pass http://192.168.1.99:30597/v1;
|
||||
# proxy_redirect default;
|
||||
# add_header Access-Control-Allow-Origin *;
|
||||
# add_header Access-Control-Allow-Headers X-Requested-With;
|
||||
# add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
|
||||
# }
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
44
env.config.ts
Normal file
44
env.config.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* create service config by current env
|
||||
* @param env the current env
|
||||
*/
|
||||
export function createServiceConfig(env: Env.ImportMeta) {
|
||||
const mockURL = 'https://mock.apifox.com/m1/3109515-0-default';
|
||||
|
||||
const serviceConfigMap = {
|
||||
dev: {
|
||||
baseURL: mockURL,
|
||||
otherBaseURL: {
|
||||
demo: 'http://localhost:9528'
|
||||
}
|
||||
},
|
||||
test: {
|
||||
baseURL: mockURL,
|
||||
otherBaseURL: {
|
||||
demo: 'http://localhost:9529'
|
||||
}
|
||||
},
|
||||
prod: {
|
||||
baseURL: mockURL,
|
||||
otherBaseURL: {
|
||||
demo: 'http://localhost:9530'
|
||||
}
|
||||
}
|
||||
} satisfies App.Service.ServiceConfigMap;
|
||||
|
||||
const { VITE_SERVICE_ENV = 'dev' } = env;
|
||||
|
||||
return serviceConfigMap[VITE_SERVICE_ENV];
|
||||
}
|
||||
|
||||
/**
|
||||
* get proxy pattern of service url
|
||||
* @param key if not set, will use the default key
|
||||
*/
|
||||
export function createProxyPattern(key?: string) {
|
||||
if (!key) {
|
||||
return '/proxy';
|
||||
}
|
||||
|
||||
return `/proxy-${key}`;
|
||||
}
|
27
index.html
27
index.html
@ -1,16 +1,15 @@
|
||||
<!-- prettier-ignore -->
|
||||
<!DOCTYPE html>
|
||||
<!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" />
|
||||
<title>%VITE_APP_NAME%</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="appLoading"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="appLoading"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
128
mock/api/auth.ts
128
mock/api/auth.ts
@ -1,128 +0,0 @@
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
import { userModel } from '../model';
|
||||
|
||||
/** 参数错误的状态码 */
|
||||
const ERROR_PARAM_CODE = 10000;
|
||||
|
||||
const ERROR_PARAM_MSG = '参数校验失败!';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
// 获取验证码
|
||||
{
|
||||
url: '/mock/getSmsCode',
|
||||
method: 'post',
|
||||
response: (): Service.MockServiceResult<boolean> => {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: true
|
||||
};
|
||||
}
|
||||
},
|
||||
// 用户+密码 登录
|
||||
{
|
||||
url: '/mock/login',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
|
||||
const { userName = undefined, password = undefined } = options.body;
|
||||
|
||||
if (!userName || !password) {
|
||||
return {
|
||||
code: ERROR_PARAM_CODE,
|
||||
message: ERROR_PARAM_MSG,
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
const findItem = userModel.find(item => item.userName === userName && item.password === password);
|
||||
|
||||
if (findItem) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
token: findItem.token,
|
||||
refreshToken: findItem.refreshToken
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 1000,
|
||||
message: '用户名或密码错误!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
// 获取用户信息(请求头携带token, 根据token获取用户信息)
|
||||
{
|
||||
url: '/mock/getUserInfo',
|
||||
method: 'get',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.UserInfo | null> => {
|
||||
// 这里的mock插件得到的字段是authorization, 前端传递的是Authorization字段
|
||||
const { authorization = '' } = options.headers;
|
||||
const REFRESH_TOKEN_CODE = 66666;
|
||||
|
||||
if (!authorization) {
|
||||
return {
|
||||
code: REFRESH_TOKEN_CODE,
|
||||
message: '用户已失效或不存在!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
const userInfo: Auth.UserInfo = {
|
||||
userId: '',
|
||||
userName: '',
|
||||
userRole: 'user'
|
||||
};
|
||||
const isInUser = userModel.some(item => {
|
||||
const flag = item.token === authorization;
|
||||
if (flag) {
|
||||
const { userId: itemUserId, userName, userRole } = item;
|
||||
Object.assign(userInfo, { userId: itemUserId, userName, userRole });
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
|
||||
if (isInUser) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: userInfo
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: REFRESH_TOKEN_CODE,
|
||||
message: '用户信息异常!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/updateToken',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
|
||||
const { refreshToken = '' } = options.body;
|
||||
|
||||
const findItem = userModel.find(item => item.refreshToken === refreshToken);
|
||||
|
||||
if (findItem) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
token: findItem.token,
|
||||
refreshToken: findItem.refreshToken
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 3000,
|
||||
message: '用户已失效或不存在!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@ -1,5 +0,0 @@
|
||||
import auth from './auth';
|
||||
import route from './route';
|
||||
import management from './management';
|
||||
|
||||
export default [...auth, ...route, ...management];
|
@ -1,33 +0,0 @@
|
||||
import { mock } from 'mockjs';
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
{
|
||||
url: '/mock/getAllUserList',
|
||||
method: 'post',
|
||||
response: (): Service.MockServiceResult<ApiUserManagement.User[]> => {
|
||||
const data = mock({
|
||||
'list|1000': [
|
||||
{
|
||||
id: '@id',
|
||||
userName: '@cname',
|
||||
'age|18-56': 56,
|
||||
'gender|1': ['0', '1', null],
|
||||
phone:
|
||||
/^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/,
|
||||
'email|1': ['@email("qq.com")', null],
|
||||
'userStatus|1': ['1', '2', '3', '4', null]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: data.list
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@ -1,29 +0,0 @@
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
import { routeModel, userModel } from '../model';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
{
|
||||
url: '/mock/getUserRoutes',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult => {
|
||||
const { userId = undefined } = options.body;
|
||||
|
||||
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis';
|
||||
|
||||
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
||||
|
||||
const filterRoutes = routeModel[role];
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
routes: filterRoutes,
|
||||
home: routeHomeName
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@ -1,6 +0,0 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
|
||||
import api from './api';
|
||||
|
||||
export function setupMockServer() {
|
||||
createProdMockServer(api);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
interface UserModel extends Auth.UserInfo {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const userModel: UserModel[] = [
|
||||
{
|
||||
token: '__TOKEN_SOYBEAN__',
|
||||
refreshToken: '__REFRESH_TOKEN_SOYBEAN__',
|
||||
userId: '0',
|
||||
userName: 'Soybean',
|
||||
userRole: 'super',
|
||||
password: 'soybean123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_SUPER__',
|
||||
refreshToken: '__REFRESH_TOKEN_SUPER__',
|
||||
userId: '1',
|
||||
userName: 'Super',
|
||||
userRole: 'super',
|
||||
password: 'super123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_ADMIN__',
|
||||
refreshToken: '__REFRESH_TOKEN_ADMIN__',
|
||||
userId: '2',
|
||||
userName: 'Admin',
|
||||
userRole: 'admin',
|
||||
password: 'admin123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_USER01__',
|
||||
refreshToken: '__REFRESH_TOKEN_USER01__',
|
||||
userId: '3',
|
||||
userName: 'User01',
|
||||
userRole: 'user',
|
||||
password: 'user01123'
|
||||
}
|
||||
];
|
@ -1,2 +0,0 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
1263
mock/model/route.ts
1263
mock/model/route.ts
File diff suppressed because it is too large
Load Diff
148
package.json
148
package.json
@ -1,134 +1,76 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"version": "0.10.4",
|
||||
"version": "1.0.0",
|
||||
"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/honghuangdc/soybean-admin",
|
||||
"repository": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"Vue",
|
||||
"Vue3",
|
||||
"admin",
|
||||
"admin-template",
|
||||
"vue-admin",
|
||||
"vue-admin-template",
|
||||
"Vite3",
|
||||
"Vite",
|
||||
"vite-admin",
|
||||
"TypeScript",
|
||||
"TS",
|
||||
"NaiveUI",
|
||||
"naive-ui",
|
||||
"naive-admin",
|
||||
"NaiveUI-Admin",
|
||||
"naive-ui-admin",
|
||||
"UnoCSS"
|
||||
],
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_SERVICE_ENV=dev vite",
|
||||
"dev:test": "cross-env VITE_SERVICE_ENV=test vite",
|
||||
"dev:prod": "cross-env VITE_SERVICE_ENV=prod vite",
|
||||
"build": "npm run typecheck && cross-env VITE_SERVICE_ENV=prod vite build",
|
||||
"build:dev": "npm run typecheck && cross-env VITE_SERVICE_ENV=dev vite build",
|
||||
"build:test": "npm run typecheck && cross-env VITE_SERVICE_ENV=test vite build",
|
||||
"build:vercel": "cross-env VITE_HASH_ROUTE=Y VITE_VERCEL=Y vite build",
|
||||
"dev": "vite",
|
||||
"build": "run-s typecheck build-only",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "soy prettier-write",
|
||||
"commit": "soy git-commit",
|
||||
"cleanup": "soy cleanup",
|
||||
"update-pkg": "soy ncu",
|
||||
"release": "soy release",
|
||||
"tsx": "tsx",
|
||||
"logo": "tsx ./scripts/logo.ts",
|
||||
"prepare": "soy init-simple-git-hooks"
|
||||
"format": "sa prettier-write",
|
||||
"commit": "sa git-commit",
|
||||
"cleanup": "sa cleanup",
|
||||
"update-pkg": "sa update-pkg",
|
||||
"prepare": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "0.11.8",
|
||||
"@antv/g2": "4.2.10",
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@soybeanjs/vue-materials": "0.2.0",
|
||||
"@vueuse/core": "10.5.0",
|
||||
"axios": "1.5.1",
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@sa/color-palette": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/request": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "10.6.1",
|
||||
"clipboard": "2.0.11",
|
||||
"colord": "2.9.3",
|
||||
"crypto-js": "4.1.1",
|
||||
"dayjs": "1.11.10",
|
||||
"echarts": "5.4.3",
|
||||
"form-data": "4.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash-es": "4.17.21",
|
||||
"naive-ui": "2.35.0",
|
||||
"pinia": "2.1.6",
|
||||
"print-js": "1.6.0",
|
||||
"qs": "6.11.2",
|
||||
"socket.io-client": "4.7.2",
|
||||
"swiper": "10.3.1",
|
||||
"ua-parser-js": "1.0.36",
|
||||
"vditor": "3.9.6",
|
||||
"vue": "3.3.4",
|
||||
"vue-i18n": "9.5.0",
|
||||
"vue-router": "4.2.5",
|
||||
"vuedraggable": "4.1.0",
|
||||
"wangeditor": "4.7.15",
|
||||
"xgplayer": "3.0.9"
|
||||
"naive-ui": "^2.35.0",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.1.7",
|
||||
"vue": "3.3.8",
|
||||
"vue-i18n": "9.6.5",
|
||||
"vue-router": "4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@amap/amap-jsapi-types": "0.0.13",
|
||||
"@iconify/json": "2.2.128",
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@soybeanjs/cli": "0.7.4",
|
||||
"@soybeanjs/vite-plugin-vue-page-route": "0.0.10",
|
||||
"@types/bmapgl": "0.0.7",
|
||||
"@types/crypto-js": "4.1.2",
|
||||
"@types/node": "20.8.4",
|
||||
"@types/qs": "6.9.8",
|
||||
"@types/ua-parser-js": "0.7.37",
|
||||
"@unocss/preset-uno": "0.56.5",
|
||||
"@unocss/transformer-directives": "0.56.5",
|
||||
"@unocss/vite": "0.56.5",
|
||||
"@vitejs/plugin-vue": "4.4.0",
|
||||
"@elegant-router/vue": "0.3.0",
|
||||
"@iconify/json": "2.2.142",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@types/lodash-es": "4.17.11",
|
||||
"@types/node": "20.9.0",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/preset-icons": "0.57.4",
|
||||
"@unocss/preset-uno": "0.57.4",
|
||||
"@unocss/transformer-directives": "0.57.4",
|
||||
"@unocss/transformer-variant-group": "0.57.4",
|
||||
"@unocss/vite": "0.57.4",
|
||||
"@vitejs/plugin-vue": "4.4.1",
|
||||
"@vitejs/plugin-vue-jsx": "3.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-config-soybeanjs": "0.5.7",
|
||||
"mockjs": "1.1.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"sass": "1.69.3",
|
||||
"eslint-config-sa": "workspace:*",
|
||||
"npm-run-all": "4.1.5",
|
||||
"sass": "^1.69.5",
|
||||
"simple-git-hooks": "2.9.0",
|
||||
"tsx": "3.13.0",
|
||||
"typescript": "5.2.2",
|
||||
"unplugin-icons": "0.17.0",
|
||||
"unplugin-icons": "0.17.4",
|
||||
"unplugin-vue-components": "0.25.2",
|
||||
"vite": "4.4.11",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-mock": "2.9.8",
|
||||
"vite": "4.5.0",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-pwa": "0.16.5",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "1.0.0-rc.5",
|
||||
"vue-tsc": "1.8.19"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
||||
}
|
||||
"vue-tsc": "1.8.22"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm soy git-commit-verify",
|
||||
"pre-commit": "pnpm typecheck && pnpm soy lint-staged"
|
||||
},
|
||||
"soybean": {
|
||||
"useSoybeanToken": true
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
"pre-commit": "pnpm typecheck && pnpm sa lint-staged"
|
||||
}
|
||||
}
|
||||
|
17
packages/color-palette/package.json
Normal file
17
packages/color-palette/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@sa/color-palette",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"colord": "2.9.3"
|
||||
}
|
||||
}
|
29
packages/color-palette/src/color.ts
Normal file
29
packages/color-palette/src/color.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { colord, extend } from 'colord';
|
||||
import type { HslColor } from 'colord';
|
||||
import labPlugin from 'colord/plugins/lab';
|
||||
|
||||
extend([labPlugin]);
|
||||
|
||||
export function isValidColor(color: string) {
|
||||
return colord(color).isValid();
|
||||
}
|
||||
|
||||
export function getHex(color: string) {
|
||||
return colord(color).toHex();
|
||||
}
|
||||
|
||||
export function getRgb(color: string) {
|
||||
return colord(color).toRgb();
|
||||
}
|
||||
|
||||
export function getHsl(color: string) {
|
||||
return colord(color).toHsl();
|
||||
}
|
||||
|
||||
export function getDeltaE(color1: string, color2: string) {
|
||||
return colord(color1).delta(color2);
|
||||
}
|
||||
|
||||
export function transformHslToHex(color: HslColor) {
|
||||
return colord(color).toHex();
|
||||
}
|
56
packages/color-palette/src/index.ts
Normal file
56
packages/color-palette/src/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { getColorPaletteFamily } from './palette';
|
||||
import { getColorName } from './name';
|
||||
import type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily } from './type';
|
||||
import defaultPalettes from './json/palette.json';
|
||||
|
||||
/**
|
||||
* get color palette by provided color and color name
|
||||
* @param color the provided color
|
||||
* @param colorName color name
|
||||
*/
|
||||
export function getColorPalette(color: string, colorName: string) {
|
||||
const colorPaletteFamily = getColorPaletteFamily(color, colorName);
|
||||
|
||||
const colorMap = new Map<ColorPaletteNumber, ColorPaletteItem>();
|
||||
|
||||
colorPaletteFamily.palettes.forEach(palette => {
|
||||
colorMap.set(palette.number, palette);
|
||||
});
|
||||
|
||||
const mainColor = colorMap.get(500) as ColorPaletteItem;
|
||||
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hexcode === color) as ColorPaletteItem;
|
||||
|
||||
const colorPalette: ColorPalette = {
|
||||
...colorPaletteFamily,
|
||||
colorMap,
|
||||
main: mainColor,
|
||||
match: matchColor
|
||||
};
|
||||
|
||||
return colorPalette;
|
||||
}
|
||||
|
||||
/**
|
||||
* get color by color palette number
|
||||
* @param color color
|
||||
* @param num color palette number
|
||||
* @return color hexcode
|
||||
*/
|
||||
export function getColorByColorPaletteNumber(color: string, num: ColorPaletteNumber) {
|
||||
const colorPalette = getColorPalette(color, color);
|
||||
|
||||
const colorItem = colorPalette.colorMap.get(num) as ColorPaletteItem;
|
||||
|
||||
return colorItem.hexcode;
|
||||
}
|
||||
|
||||
export default getColorPalette;
|
||||
|
||||
/**
|
||||
* the builtin color palettes
|
||||
*/
|
||||
const colorPalettes = defaultPalettes as ColorPaletteFamily[];
|
||||
|
||||
export { getColorName, colorPalettes };
|
||||
|
||||
export type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily };
|
1568
packages/color-palette/src/json/color-name.json
Normal file
1568
packages/color-palette/src/json/color-name.json
Normal file
File diff suppressed because it is too large
Load Diff
274
packages/color-palette/src/json/palette.json
Normal file
274
packages/color-palette/src/json/palette.json
Normal file
@ -0,0 +1,274 @@
|
||||
[
|
||||
{
|
||||
"key": "red",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fef2f2", "number": 50, "name": "Bridesmaid" },
|
||||
{ "hexcode": "#fee2e2", "number": 100, "name": "Pippin" },
|
||||
{ "hexcode": "#fecaca", "number": 200, "name": "Your Pink" },
|
||||
{ "hexcode": "#fca5a5", "number": 300, "name": "Cornflower Lilac" },
|
||||
{ "hexcode": "#f87171", "number": 400, "name": "Bittersweet" },
|
||||
{ "hexcode": "#ef4444", "number": 500, "name": "Cinnabar" },
|
||||
{ "hexcode": "#dc2626", "number": 600, "name": "Persian Red" },
|
||||
{ "hexcode": "#b91c1c", "number": 700, "name": "Thunderbird" },
|
||||
{ "hexcode": "#991b1b", "number": 800, "name": "Old Brick" },
|
||||
{ "hexcode": "#7f1d1d", "number": 900, "name": "Falu Red" },
|
||||
{ "hexcode": "#450a0a", "number": 950, "name": "Mahogany" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "orange",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff7ed", "number": 50, "name": "Serenade" },
|
||||
{ "hexcode": "#ffedd5", "number": 100, "name": "Derby" },
|
||||
{ "hexcode": "#fed7aa", "number": 200, "name": "Caramel" },
|
||||
{ "hexcode": "#fdba74", "number": 300, "name": "Macaroni and Cheese" },
|
||||
{ "hexcode": "#fb923c", "number": 400, "name": "Neon Carrot" },
|
||||
{ "hexcode": "#f97316", "number": 500, "name": "Ecstasy" },
|
||||
{ "hexcode": "#ea580c", "number": 600, "name": "Trinidad" },
|
||||
{ "hexcode": "#c2410c", "number": 700, "name": "Tia Maria" },
|
||||
{ "hexcode": "#9a3412", "number": 800, "name": "Tabasco" },
|
||||
{ "hexcode": "#7c2d12", "number": 900, "name": "Pueblo" },
|
||||
{ "hexcode": "#431407", "number": 950, "name": "Rebel" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "amber",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fffbeb", "number": 50, "name": "Island Spice" },
|
||||
{ "hexcode": "#fef3c7", "number": 100, "name": "Beeswax" },
|
||||
{ "hexcode": "#fde68a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fcd34d", "number": 300, "name": "Mustard" },
|
||||
{ "hexcode": "#fbbf24", "number": 400, "name": "Lightning Yellow" },
|
||||
{ "hexcode": "#f59e0b", "number": 500, "name": "California" },
|
||||
{ "hexcode": "#d97706", "number": 600, "name": "Christine" },
|
||||
{ "hexcode": "#b45309", "number": 700, "name": "Vesuvius" },
|
||||
{ "hexcode": "#92400e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#78350f", "number": 900, "name": "Copper Canyon" },
|
||||
{ "hexcode": "#451a03", "number": 950, "name": "Brown Pod" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "yellow",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fefce8", "number": 50, "name": "Orange White" },
|
||||
{ "hexcode": "#fef9c3", "number": 100, "name": "Lemon Chiffon" },
|
||||
{ "hexcode": "#fef08a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fde047", "number": 300, "name": "Bright Sun" },
|
||||
{ "hexcode": "#facc15", "number": 400, "name": "Candlelight" },
|
||||
{ "hexcode": "#eab308", "number": 500, "name": "Corn" },
|
||||
{ "hexcode": "#ca8a04", "number": 600, "name": "Pirate Gold" },
|
||||
{ "hexcode": "#a16207", "number": 700, "name": "Mai Tai" },
|
||||
{ "hexcode": "#854d0e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#713f12", "number": 900, "name": "Sepia" },
|
||||
{ "hexcode": "#422006", "number": 950, "name": "Dark Ebony" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lime",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f7fee7", "number": 50, "name": "Spring Sun" },
|
||||
{ "hexcode": "#ecfccb", "number": 100, "name": "Chiffon" },
|
||||
{ "hexcode": "#d9f99d", "number": 200, "name": "Gossip" },
|
||||
{ "hexcode": "#bef264", "number": 300, "name": "Sulu" },
|
||||
{ "hexcode": "#a3e635", "number": 400, "name": "Conifer" },
|
||||
{ "hexcode": "#84cc16", "number": 500, "name": "Lima" },
|
||||
{ "hexcode": "#65a30d", "number": 600, "name": "Christi" },
|
||||
{ "hexcode": "#4d7c0f", "number": 700, "name": "Green Leaf" },
|
||||
{ "hexcode": "#3f6212", "number": 800, "name": "Dell" },
|
||||
{ "hexcode": "#365314", "number": 900, "name": "Clover" },
|
||||
{ "hexcode": "#1a2e05", "number": 950, "name": "Deep Forest Green" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "green",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdf4", "number": 50, "name": "Ottoman" },
|
||||
{ "hexcode": "#dcfce7", "number": 100, "name": "Blue Romance" },
|
||||
{ "hexcode": "#bbf7d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#86efac", "number": 300, "name": "Algae Green" },
|
||||
{ "hexcode": "#4ade80", "number": 400, "name": "Emerald" },
|
||||
{ "hexcode": "#22c55e", "number": 500, "name": "Malachite" },
|
||||
{ "hexcode": "#16a34a", "number": 600, "name": "Salem" },
|
||||
{ "hexcode": "#15803d", "number": 700, "name": "Jewel" },
|
||||
{ "hexcode": "#166534", "number": 800, "name": "Jewel" },
|
||||
{ "hexcode": "#14532d", "number": 900, "name": "Green Pea" },
|
||||
{ "hexcode": "#052e16", "number": 950, "name": "English Holly" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "emerald",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfdf5", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#d1fae5", "number": 100, "name": "Granny Apple" },
|
||||
{ "hexcode": "#a7f3d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#6ee7b7", "number": 300, "name": "Bermuda" },
|
||||
{ "hexcode": "#34d399", "number": 400, "name": "Shamrock" },
|
||||
{ "hexcode": "#10b981", "number": 500, "name": "Mountain Meadow" },
|
||||
{ "hexcode": "#059669", "number": 600, "name": "Green Haze" },
|
||||
{ "hexcode": "#047857", "number": 700, "name": "Watercourse" },
|
||||
{ "hexcode": "#065f46", "number": 800, "name": "Watercourse" },
|
||||
{ "hexcode": "#064e3b", "number": 900, "name": "Evening Sea" },
|
||||
{ "hexcode": "#022c22", "number": 950, "name": "Burnham" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "teal",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdfa", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#ccfbf1", "number": 100, "name": "Scandal" },
|
||||
{ "hexcode": "#99f6e4", "number": 200, "name": "Ice Cold" },
|
||||
{ "hexcode": "#5eead4", "number": 300, "name": "Turquoise Blue" },
|
||||
{ "hexcode": "#2dd4bf", "number": 400, "name": "Turquoise" },
|
||||
{ "hexcode": "#14b8a6", "number": 500, "name": "Java" },
|
||||
{ "hexcode": "#0d9488", "number": 600, "name": "Blue Chill" },
|
||||
{ "hexcode": "#0f766e", "number": 700, "name": "Genoa" },
|
||||
{ "hexcode": "#115e59", "number": 800, "name": "Eden" },
|
||||
{ "hexcode": "#134e4a", "number": 900, "name": "Eden" },
|
||||
{ "hexcode": "#042f2e", "number": 950, "name": "Tiber" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "cyan",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfeff", "number": 50, "name": "Bubbles" },
|
||||
{ "hexcode": "#cffafe", "number": 100, "name": "Oyster Bay" },
|
||||
{ "hexcode": "#a5f3fc", "number": 200, "name": "Anakiwa" },
|
||||
{ "hexcode": "#67e8f9", "number": 300, "name": "Spray" },
|
||||
{ "hexcode": "#22d3ee", "number": 400, "name": "Bright Turquoise" },
|
||||
{ "hexcode": "#06b6d4", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0891b2", "number": 600, "name": "Bondi Blue" },
|
||||
{ "hexcode": "#0e7490", "number": 700, "name": "Blue Chill" },
|
||||
{ "hexcode": "#155e75", "number": 800, "name": "Blumine" },
|
||||
{ "hexcode": "#164e63", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#083344", "number": 950, "name": "Tarawera" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sky",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0f9ff", "number": 50, "name": "Alice Blue" },
|
||||
{ "hexcode": "#e0f2fe", "number": 100, "name": "Pattens Blue" },
|
||||
{ "hexcode": "#bae6fd", "number": 200, "name": "French Pass" },
|
||||
{ "hexcode": "#7dd3fc", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#38bdf8", "number": 400, "name": "Picton Blue" },
|
||||
{ "hexcode": "#0ea5e9", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0284c7", "number": 600, "name": "Lochmara" },
|
||||
{ "hexcode": "#0369a1", "number": 700, "name": "Bahama Blue" },
|
||||
{ "hexcode": "#075985", "number": 800, "name": "Venice Blue" },
|
||||
{ "hexcode": "#0c4a6e", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#082f49", "number": 950, "name": "Blue Whale" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "blue",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eff6ff", "number": 50, "name": "Zumthor" },
|
||||
{ "hexcode": "#dbeafe", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#bfdbfe", "number": 200, "name": "Tropical Blue" },
|
||||
{ "hexcode": "#93c5fd", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#60a5fa", "number": 400, "name": "Cornflower Blue" },
|
||||
{ "hexcode": "#3b82f6", "number": 500, "name": "Dodger Blue" },
|
||||
{ "hexcode": "#2563eb", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#1d4ed8", "number": 700, "name": "Cerulean Blue" },
|
||||
{ "hexcode": "#1e40af", "number": 800, "name": "Persian Blue" },
|
||||
{ "hexcode": "#1e3a8a", "number": 900, "name": "Bay of Many" },
|
||||
{ "hexcode": "#172554", "number": 950, "name": "Bunting" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "indigo",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eef2ff", "number": 50, "name": "Zircon" },
|
||||
{ "hexcode": "#e0e7ff", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#c7d2fe", "number": 200, "name": "Periwinkle" },
|
||||
{ "hexcode": "#a5b4fc", "number": 300, "name": "Perano" },
|
||||
{ "hexcode": "#818cf8", "number": 400, "name": "Portage" },
|
||||
{ "hexcode": "#6366f1", "number": 500, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4f46e5", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4338ca", "number": 700, "name": "Governor Bay" },
|
||||
{ "hexcode": "#3730a3", "number": 800, "name": "Governor Bay" },
|
||||
{ "hexcode": "#312e81", "number": 900, "name": "Minsk" },
|
||||
{ "hexcode": "#1e1b4b", "number": 950, "name": "Port Gore" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "violet",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f5f3ff", "number": 50, "name": "Titan White" },
|
||||
{ "hexcode": "#ede9fe", "number": 100, "name": "Titan White" },
|
||||
{ "hexcode": "#ddd6fe", "number": 200, "name": "Fog" },
|
||||
{ "hexcode": "#c4b5fd", "number": 300, "name": "Melrose" },
|
||||
{ "hexcode": "#a78bfa", "number": 400, "name": "Dull Lavender" },
|
||||
{ "hexcode": "#8b5cf6", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#7c3aed", "number": 600, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6d28d9", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#5b21b6", "number": 800, "name": "Purple Heart" },
|
||||
{ "hexcode": "#4c1d95", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#2e1065", "number": 950, "name": "Violent Violet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "purple",
|
||||
"palettes": [
|
||||
{ "hexcode": "#faf5ff", "number": 50, "name": "Magnolia" },
|
||||
{ "hexcode": "#f3e8ff", "number": 100, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#e9d5ff", "number": 200, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#d8b4fe", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#c084fc", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#a855f7", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#9333ea", "number": 600, "name": "Electric Violet" },
|
||||
{ "hexcode": "#7e22ce", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6b21a8", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#581c87", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#3b0764", "number": 950, "name": "Christalle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "fuchsia",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf4ff", "number": 50, "name": "White Pointer" },
|
||||
{ "hexcode": "#fae8ff", "number": 100, "name": "White Pointer" },
|
||||
{ "hexcode": "#f5d0fe", "number": 200, "name": "Mauve" },
|
||||
{ "hexcode": "#f0abfc", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#e879f9", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#d946ef", "number": 500, "name": "Heliotrope" },
|
||||
{ "hexcode": "#c026d3", "number": 600, "name": "Fuchsia Pink" },
|
||||
{ "hexcode": "#a21caf", "number": 700, "name": "Violet Eggplant" },
|
||||
{ "hexcode": "#86198f", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#701a75", "number": 900, "name": "Seance" },
|
||||
{ "hexcode": "#4a044e", "number": 950, "name": "Clairvoyant" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pink",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf2f8", "number": 50, "name": "Wisp Pink" },
|
||||
{ "hexcode": "#fce7f3", "number": 100, "name": "Carousel Pink" },
|
||||
{ "hexcode": "#fbcfe8", "number": 200, "name": "Classic Rose" },
|
||||
{ "hexcode": "#f9a8d4", "number": 300, "name": "Lavender Pink" },
|
||||
{ "hexcode": "#f472b6", "number": 400, "name": "Persian Pink" },
|
||||
{ "hexcode": "#ec4899", "number": 500, "name": "Brilliant Rose" },
|
||||
{ "hexcode": "#db2777", "number": 600, "name": "Cerise" },
|
||||
{ "hexcode": "#be185d", "number": 700, "name": "Maroon Flush" },
|
||||
{ "hexcode": "#9d174d", "number": 800, "name": "Disco" },
|
||||
{ "hexcode": "#831843", "number": 900, "name": "Disco" },
|
||||
{ "hexcode": "#500724", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "rose",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff1f2", "number": 50, "name": "Lavender blush" },
|
||||
{ "hexcode": "#ffe4e6", "number": 100, "name": "Cosmos" },
|
||||
{ "hexcode": "#fecdd3", "number": 200, "name": "Pastel Pink" },
|
||||
{ "hexcode": "#fda4af", "number": 300, "name": "Sweet Pink" },
|
||||
{ "hexcode": "#fb7185", "number": 400, "name": "Froly" },
|
||||
{ "hexcode": "#f43f5e", "number": 500, "name": "Radical Red" },
|
||||
{ "hexcode": "#e11d48", "number": 600, "name": "Amaranth" },
|
||||
{ "hexcode": "#be123c", "number": 700, "name": "Cardinal" },
|
||||
{ "hexcode": "#9f1239", "number": 800, "name": "Shiraz" },
|
||||
{ "hexcode": "#881337", "number": 900, "name": "Claret" },
|
||||
{ "hexcode": "#4c0519", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
}
|
||||
]
|
46
packages/color-palette/src/name.ts
Normal file
46
packages/color-palette/src/name.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { getHex, getRgb, getHsl } from './color';
|
||||
import colorNames from './json/color-name.json';
|
||||
|
||||
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 hexcode = `#${hexValue}`;
|
||||
|
||||
const match = hex === hexcode;
|
||||
|
||||
if (match) {
|
||||
name = colorName;
|
||||
} else {
|
||||
const { r, g, b } = getRgb(hexcode);
|
||||
const { h, s, l } = getHsl(hexcode);
|
||||
|
||||
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 = cl < 0 ? 'Invalid Color' : colorNames[cl][1];
|
||||
|
||||
return name;
|
||||
}
|
95
packages/color-palette/src/palette.ts
Normal file
95
packages/color-palette/src/palette.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { isValidColor, getHsl, getDeltaE, transformHslToHex } from './color';
|
||||
import { getColorName } from './name';
|
||||
import type { ColorPaletteFamily, ColorPaletteFamilyWithNearestPalette } from './type';
|
||||
import defaultPalettes from './json/palette.json';
|
||||
|
||||
export function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
|
||||
const familyWithConfig = families.map(family => {
|
||||
const palettes = family.palettes.map(palette => {
|
||||
return {
|
||||
...palette,
|
||||
delta: getDeltaE(color, palette.hexcode)
|
||||
};
|
||||
});
|
||||
|
||||
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.hexcode);
|
||||
const { l: currLightness } = getHsl(curr.hexcode);
|
||||
|
||||
const deltaPrev = Math.abs(prevLightness - l);
|
||||
const deltaCurr = Math.abs(currLightness - l);
|
||||
|
||||
return deltaPrev < deltaCurr ? prev : curr;
|
||||
})
|
||||
};
|
||||
|
||||
return paletteFamily;
|
||||
}
|
||||
|
||||
export function getColorPaletteFamily(color: string, colorName: string) {
|
||||
if (!isValidColor(color)) {
|
||||
throw new Error('Invalid color, please check color value!');
|
||||
}
|
||||
|
||||
const { h: h1, s: s1 } = getHsl(color);
|
||||
|
||||
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(
|
||||
color,
|
||||
defaultPalettes as ColorPaletteFamily[]
|
||||
);
|
||||
|
||||
const { number, hexcode } = nearestLightnessPalette;
|
||||
|
||||
const { h: h2, s: s2 } = getHsl(hexcode);
|
||||
|
||||
const deltaH = h1 - h2 || h2;
|
||||
|
||||
const sRatio = s1 / s2;
|
||||
|
||||
const colorPaletteFamily: ColorPaletteFamily = {
|
||||
key: colorName,
|
||||
palettes: palettes.map(palette => {
|
||||
let hexValue = color;
|
||||
|
||||
const isSame = number === palette.number;
|
||||
|
||||
if (!isSame) {
|
||||
const { h: h3, s: s3, l } = getHsl(palette.hexcode);
|
||||
|
||||
const newH = deltaH < 0 ? h3 + deltaH : deltaH;
|
||||
const newS = s3 * sRatio;
|
||||
|
||||
hexValue = transformHslToHex({
|
||||
h: newH,
|
||||
s: newS,
|
||||
l
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hexcode: hexValue,
|
||||
number: palette.number,
|
||||
name: getColorName(hexValue)
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return colorPaletteFamily;
|
||||
}
|
63
packages/color-palette/src/type.ts
Normal file
63
packages/color-palette/src/type.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* the color palette number
|
||||
*/
|
||||
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
||||
|
||||
/**
|
||||
* the color palette item
|
||||
*/
|
||||
export type ColorPaletteItem = {
|
||||
/**
|
||||
* the color hexcode
|
||||
*/
|
||||
hexcode: string;
|
||||
/**
|
||||
* the color number
|
||||
* @link {@link ColorPaletteNumber}
|
||||
*/
|
||||
number: ColorPaletteNumber;
|
||||
/**
|
||||
* the color name
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ColorPaletteFamily = {
|
||||
/**
|
||||
* the color palette family key
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* the color palette family's palettes
|
||||
*/
|
||||
palettes: ColorPaletteItem[];
|
||||
};
|
||||
|
||||
export type ColorPaletteWithDelta = ColorPaletteItem & {
|
||||
delta: number;
|
||||
};
|
||||
|
||||
export type ColorPaletteItemWithName = ColorPaletteItem & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
|
||||
nearestPalette: ColorPaletteWithDelta;
|
||||
nearestLightnessPalette: ColorPaletteWithDelta;
|
||||
};
|
||||
|
||||
export type ColorPalette = ColorPaletteFamily & {
|
||||
/**
|
||||
* the color map of the palette
|
||||
*/
|
||||
colorMap: Map<ColorPaletteNumber, ColorPaletteItem>;
|
||||
/**
|
||||
* the main color of the palette
|
||||
* @description which number is 500
|
||||
*/
|
||||
main: ColorPaletteItemWithName;
|
||||
/**
|
||||
* the match color of the palette
|
||||
*/
|
||||
match: ColorPaletteItemWithName;
|
||||
};
|
38
packages/docs/.vitepress/config.ts
Normal file
38
packages/docs/.vitepress/config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
export default defineConfig({
|
||||
title: 'Soybean Admin',
|
||||
description: '一个优雅、清新、漂亮的中后台模版',
|
||||
head: [
|
||||
['meta', { name: 'author', content: 'Soybean' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
name: 'keywords',
|
||||
content: 'soybean, soybean-admin, vite, vue, vue3, soybean-admin docs'
|
||||
}
|
||||
],
|
||||
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no'
|
||||
}
|
||||
],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
srcDir: path.join(process.cwd(), 'packages/docs/src'),
|
||||
themeConfig: {
|
||||
logo: '/logo.svg',
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/honghuangdc/soybean-admin' }],
|
||||
algolia: {
|
||||
appId: '98WN1RY04S',
|
||||
apiKey: '13e9f5767b774422a5880723d9c23265',
|
||||
indexName: 'soybean'
|
||||
},
|
||||
nav: [],
|
||||
sidebar: {}
|
||||
}
|
||||
});
|
32
packages/docs/.vitepress/icon.ts
Normal file
32
packages/docs/.vitepress/icon.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export const qqSvg = `
|
||||
<svg height="2500" viewBox="-1.94 0 124.879 145.085" width="2101" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m60.503 142.237c-12.533 0-24.038-4.195-31.445-10.46-3.762 1.124-8.574 2.932-11.61 5.175-2.6 1.918-2.275 3.874-1.807 4.663 2.056 3.47 35.273 2.216 44.862 1.136zm0 0c12.535 0 24.039-4.195 31.447-10.46 3.76 1.124 8.573 2.932 11.61 5.175 2.598 1.918 2.274 3.874 1.805 4.663-2.056 3.47-35.272 2.216-44.862 1.136zm0 0"
|
||||
fill="#faab07"
|
||||
/>
|
||||
<path
|
||||
d="m60.576 67.119c20.698-.14 37.286-4.147 42.907-5.683 1.34-.367 2.056-1.024 2.056-1.024.005-.189.085-3.37.085-5.01 0-27.634-13.044-55.401-45.124-55.402-32.08.001-45.125 27.769-45.125 55.401 0 1.642.08 4.822.086 5.01 0 0 .583.615 1.65.913 5.19 1.444 22.09 5.65 43.312 5.795zm56.245 23.02c-1.283-4.129-3.034-8.944-4.808-13.568 0 0-1.02-.126-1.537.023-15.913 4.623-35.202 7.57-49.9 7.392h-.153c-14.616.175-33.774-2.737-49.634-7.315-.606-.175-1.802-.1-1.802-.1-1.774 4.624-3.525 9.44-4.808 13.568-6.119 19.69-4.136 27.838-2.627 28.02 3.239.392 12.606-14.821 12.606-14.821 0 15.459 13.957 39.195 45.918 39.413h.848c31.96-.218 45.917-23.954 45.917-39.413 0 0 9.368 15.213 12.607 14.822 1.508-.183 3.491-8.332-2.627-28.021"
|
||||
/>
|
||||
<path
|
||||
d="m49.085 40.824c-4.352.197-8.07-4.76-8.304-11.063-.236-6.305 3.098-11.576 7.45-11.773 4.347-.195 8.064 4.76 8.3 11.065.238 6.306-3.097 11.577-7.446 11.771m31.133-11.063c-.233 6.302-3.951 11.26-8.303 11.063-4.35-.195-7.684-5.465-7.446-11.77.236-6.305 3.952-11.26 8.3-11.066 4.352.197 7.686 5.468 7.449 11.773"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m87.952 49.725c-1.162-2.575-12.875-5.445-27.374-5.445h-.156c-14.5 0-26.212 2.87-27.375 5.446a.863.863 0 0 0 -.085.367c0 .186.063.352.16.496.98 1.427 13.985 8.487 27.3 8.487h.156c13.314 0 26.319-7.058 27.299-8.487a.873.873 0 0 0 .16-.498.856.856 0 0 0 -.085-.365"
|
||||
fill="#faab07"
|
||||
/>
|
||||
<path
|
||||
d="m54.434 29.854c.199 2.49-1.167 4.702-3.046 4.943-1.883.242-3.568-1.58-3.768-4.07-.197-2.492 1.167-4.704 3.043-4.944 1.886-.244 3.574 1.58 3.771 4.07m11.956.833c.385-.689 3.004-4.312 8.427-2.993 1.425.347 2.084.857 2.223 1.057.205.296.262.718.053 1.286-.412 1.126-1.263 1.095-1.734.875-.305-.142-4.082-2.66-7.562 1.097-.24.257-.668.346-1.073.04-.407-.308-.574-.93-.334-1.362"
|
||||
/>
|
||||
<path
|
||||
d="m60.576 83.08h-.153c-9.996.12-22.116-1.204-33.854-3.518-1.004 5.818-1.61 13.132-1.09 21.853 1.316 22.043 14.407 35.9 34.614 36.1h.82c20.208-.2 33.298-14.057 34.616-36.1.52-8.723-.087-16.035-1.092-21.854-11.739 2.315-23.862 3.64-33.86 3.518"
|
||||
fill="#fff"
|
||||
/>
|
||||
<g fill="#eb1923">
|
||||
<path d="m32.102 81.235v21.693s9.937 2.004 19.893.616v-20.009c-6.307-.357-13.109-1.152-19.893-2.3" />
|
||||
<path
|
||||
d="m105.539 60.412s-19.33 6.102-44.963 6.275h-.153c-25.591-.172-44.896-6.255-44.962-6.275l-6.474 16.158c16.193 4.882 36.261 8.028 51.436 7.845h.153c15.175.183 35.242-2.963 51.437-7.845zm0 0"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
4
packages/docs/.vitepress/theme/index.ts
Normal file
4
packages/docs/.vitepress/theme/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import Theme from 'vitepress/theme';
|
||||
import './style.css';
|
||||
|
||||
export default Theme;
|
90
packages/docs/.vitepress/theme/style.css
Normal file
90
packages/docs/.vitepress/theme/style.css
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: #646cff;
|
||||
--vp-c-brand-light: #747bff;
|
||||
--vp-c-brand-lighter: #9499ff;
|
||||
--vp-c-brand-lightest: #bcc0ff;
|
||||
--vp-c-brand-dark: #535bf2;
|
||||
--vp-c-brand-darker: #454ce1;
|
||||
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
var(--vp-c-brand-lightest) 30%,
|
||||
var(--vp-c-brand-darker)
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
var(--vp-c-brand-lightest) 30%,
|
||||
var(--vp-c-brand) 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(40px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(72px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||
}
|
12
packages/docs/package.json
Normal file
12
packages/docs/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sa/docs",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"serve": "vitepress serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "1.0.0-rc.25"
|
||||
}
|
||||
}
|
44
packages/docs/src/index.md
Normal file
44
packages/docs/src/index.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
title: Soybean Admin
|
||||
titleTemplate: 一个清新优雅的中后台模版
|
||||
|
||||
hero:
|
||||
name: Soybean Admin
|
||||
text: 清新优雅的中后台模版
|
||||
tagline: 基于 Vue3 + Vite3 + TS + NaiveUI + UnoCSS
|
||||
image:
|
||||
src: /logo.svg
|
||||
alt: Soybean Admin
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 开始
|
||||
link: /guide/
|
||||
- theme: alt
|
||||
text: 介绍
|
||||
link: /guide/introduction
|
||||
- theme: alt
|
||||
text: 在 GitHub 上查看
|
||||
link: https://github.com/honghuangdc/soybean-admin
|
||||
|
||||
features:
|
||||
- icon: 🆕
|
||||
title: 最新流行技术栈
|
||||
details: 基于Vue3、Vite3、TS、NaiveUI和UnoCSS等最新技术栈开发
|
||||
- icon: 🦋
|
||||
title: 极高水准的代码规范
|
||||
details: 代码规范完善,代码结构清晰
|
||||
- icon: 🛠️
|
||||
title: 丰富的插件
|
||||
details: 常见的Web端插件示例实现
|
||||
- icon: 🔩
|
||||
title: 主题配置
|
||||
details: 丰富的主题配置及暗黑主题适配
|
||||
- icon: 🔗
|
||||
title: 基于文件的路由系统
|
||||
details: 自动生成路由声明、路由导入和路由模块
|
||||
- icon: 🔑
|
||||
title: 权限管理
|
||||
details: 完善的前后端权限管理方案
|
||||
---
|
6
packages/eslint-config/configs/base.js
Normal file
6
packages/eslint-config/configs/base.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
|
||||
};
|
44
packages/eslint-config/configs/js.js
Normal file
44
packages/eslint-config/configs/js.js
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es2024: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2024,
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
sourceType: 'module'
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'*.min.*',
|
||||
'CHANGELOG.md',
|
||||
'dist',
|
||||
'LICENSE*',
|
||||
'output',
|
||||
'coverage',
|
||||
'public',
|
||||
'temp',
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'__snapshots__',
|
||||
'!.github',
|
||||
'!.vitepress',
|
||||
'!.vscode'
|
||||
],
|
||||
plugins: ['n', 'promise'],
|
||||
extends: [require.resolve('../rules/all.js'), 'plugin:import/recommended'],
|
||||
rules: {
|
||||
// import
|
||||
'import/no-mutable-exports': 'error',
|
||||
'import/no-named-as-default': 'off'
|
||||
}
|
||||
};
|
11
packages/eslint-config/configs/prettier.js
Normal file
11
packages/eslint-config/configs/prettier.js
Normal file
@ -0,0 +1,11 @@
|
||||
const prettierRules = require('../rules/prettier');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', prettierRules]
|
||||
}
|
||||
};
|
61
packages/eslint-config/configs/ts.js
Normal file
61
packages/eslint-config/configs/ts.js
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [require.resolve('./js.js'), 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['tsconfig.json', 'packages/*/tsconfig.json', 'examples/*/tsconfig.json', 'docs/*/tsconfig.json']
|
||||
}
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.mts', '*.cts'],
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.mjs', '*.cjs', '*.cts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off'
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
// TS
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: false }],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true
|
||||
}
|
||||
],
|
||||
|
||||
// Override JS
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'all',
|
||||
ignoreRestSiblings: false,
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
|
||||
// off
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
}
|
||||
};
|
30
packages/eslint-config/configs/vue.js
Normal file
30
packages/eslint-config/configs/vue.js
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['plugin:vue/vue3-recommended', require.resolve('./base.js')],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: {
|
||||
js: 'espree',
|
||||
jsx: 'espree',
|
||||
ts: '@typescript-eslint/parser',
|
||||
tsx: '@typescript-eslint/parser'
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off' // TS will check un declared variables, if the script code is is in a .vue file, this rule should not disabled
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
};
|
6
packages/eslint-config/index.js
Normal file
6
packages/eslint-config/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
const baseConfig = require('./configs/base');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = baseConfig;
|
23
packages/eslint-config/package.json
Normal file
23
packages/eslint-config/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "eslint-config-sa",
|
||||
"version": "1.0.0",
|
||||
"description": "SoybeanAdmin's eslint config resets",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./vue": "./configs/vue.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "8.44.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-n": "16.3.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"prettier": "3.1.0"
|
||||
}
|
||||
}
|
1681
packages/eslint-config/rules/all.js
Normal file
1681
packages/eslint-config/rules/all.js
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/eslint-config/rules/prettier.js
Normal file
26
packages/eslint-config/rules/prettier.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @type {import('prettier').Options}
|
||||
*/
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
rangeStart: 0,
|
||||
rangeEnd: Number.POSITIVE_INFINITY,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
vueIndentScriptAndStyle: false,
|
||||
endOfLine: 'lf',
|
||||
embeddedLanguageFormatting: 'auto',
|
||||
singleAttributePerLine: false
|
||||
};
|
14
packages/hooks/package.json
Normal file
14
packages/hooks/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
6
packages/hooks/src/index.ts
Normal file
6
packages/hooks/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
import useContext from './use-context';
|
||||
import useSvgIconRender from './use-svg-icon-render';
|
||||
|
||||
export { useBoolean, useLoading, useContext, useSvgIconRender };
|
@ -1,8 +1,8 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* boolean组合式函数
|
||||
* @param initValue 初始值
|
||||
* boolean
|
||||
* @param initValue init value
|
||||
*/
|
||||
export default function useBoolean(initValue = false) {
|
||||
const bool = ref(initValue);
|
103
packages/hooks/src/use-context.ts
Normal file
103
packages/hooks/src/use-context.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* use context
|
||||
* @param contextName context name
|
||||
* @param fn context function
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
};
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
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);
|
||||
|
56
packages/hooks/src/use-svg-icon-render.ts
Normal file
56
packages/hooks/src/use-svg-icon-render.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
||||
};
|
||||
}
|
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
6
packages/materials/.eslintrc
Normal file
6
packages/materials/.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "sa/vue",
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off"
|
||||
}
|
||||
}
|
22
packages/materials/package.json
Normal file
22
packages/materials/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"@simonwep/pickr": "1.9.0",
|
||||
"simplebar-vue": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.8.1"
|
||||
}
|
||||
}
|
7
packages/materials/src/index.ts
Normal file
7
packages/materials/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AdminLayout, { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX } from './libs/admin-layout';
|
||||
import PageTab from './libs/page-tab';
|
||||
import SimpleScrollbar from './libs/simple-scrollbar';
|
||||
import ColorPicker from './libs/color-picker';
|
||||
|
||||
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar, ColorPicker };
|
||||
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);
|
||||
}
|
17
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
17
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
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 = 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_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX } from './shared';
|
||||
|
||||
export default AdminLayout;
|
||||
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
|
238
packages/materials/src/libs/admin-layout/index.vue
Normal file
238
packages/materials/src/libs/admin-layout/index.vue
Normal file
@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div :class="['relative h-full', commonClass]" :style="cssVars">
|
||||
<div
|
||||
:id="isWrapperScroll ? scrollElId : undefined"
|
||||
:class="['flex flex-col h-full', commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<template v-if="showHeader">
|
||||
<header
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
style['layout-header'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
headerClass,
|
||||
headerLeftGapClass,
|
||||
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<div
|
||||
v-show="!fullContent && fixedHeaderAndTab"
|
||||
:class="[style['layout-header-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Tab -->
|
||||
<template v-if="showTab">
|
||||
<div
|
||||
:class="[
|
||||
style['layout-tab'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
tabClass,
|
||||
{ 'top-0!': fullContent || !showHeader },
|
||||
leftGapClass,
|
||||
{ 'absolute left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="tab"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-show="fullContent || fixedHeaderAndTab"
|
||||
:class="[style['layout-tab-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Sider -->
|
||||
<template v-if="showSider">
|
||||
<aside
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
'absolute left-0 top-0 h-full',
|
||||
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 w-0 h-full bg-white',
|
||||
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 w-full h-full bg-[rgba(0,0,0,0.2)]', style['layout-mobile-sider-mask']]"
|
||||
@click="handleClickMask"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
:id="isContentScroll ? scrollElId : undefined"
|
||||
:class="[
|
||||
'flex flex-col flex-grow',
|
||||
commonClass,
|
||||
contentClass,
|
||||
leftGapClass,
|
||||
{ 'overflow-y-auto': isContentScroll }
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<template v-if="showFooter">
|
||||
<footer
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
style['layout-footer'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
footerClass,
|
||||
footerLeftGapClass,
|
||||
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
|
||||
]"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
<div
|
||||
v-show="!fullContent && fixedFooter"
|
||||
:class="[style['layout-footer-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { AdminLayoutProps } from '../../types';
|
||||
import { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, 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>
|
||||
|
||||
<style scoped></style>
|
70
packages/materials/src/libs/admin-layout/shared.ts
Normal file
70
packages/materials/src/libs/admin-layout/shared.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { AdminLayoutProps, LayoutCssVarsProps, LayoutCssVars } 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);
|
||||
}
|
3
packages/materials/src/libs/color-picker/index.ts
Normal file
3
packages/materials/src/libs/color-picker/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import ColorPicker from './index.vue';
|
||||
|
||||
export default ColorPicker;
|
116
packages/materials/src/libs/color-picker/index.vue
Normal file
116
packages/materials/src/libs/color-picker/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import ColorPicker from '@simonwep/pickr';
|
||||
import '@simonwep/pickr/dist/themes/nano.min.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'ColorPicker'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
palettes?: string[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
palettes: () => [
|
||||
'#3b82f6',
|
||||
'#6366f1',
|
||||
'#8b5cf6',
|
||||
'#a855f7',
|
||||
'#0ea5e9',
|
||||
'#06b6d4',
|
||||
'#f43f5e',
|
||||
'#ef4444',
|
||||
'#ec4899',
|
||||
'#d946ef',
|
||||
'#f97316',
|
||||
'#f59e0b',
|
||||
'#eab308',
|
||||
'#84cc16',
|
||||
'#22c55e',
|
||||
'#10b981',
|
||||
'#14b8a6'
|
||||
]
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:color', value: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const instance = ref<ColorPicker | null>(null);
|
||||
|
||||
function handleColorChange(hsva: ColorPicker.HSVaColor) {
|
||||
const color = hsva.toHEXA().toString();
|
||||
emit('update:color', color);
|
||||
}
|
||||
|
||||
function initColorPicker() {
|
||||
if (!domRef.value) return;
|
||||
|
||||
instance.value = ColorPicker.create({
|
||||
el: domRef.value,
|
||||
theme: 'nano',
|
||||
swatches: props.palettes,
|
||||
lockOpacity: true,
|
||||
default: props.color,
|
||||
disabled: props.disabled,
|
||||
components: {
|
||||
preview: true,
|
||||
opacity: false,
|
||||
hue: true,
|
||||
interaction: {
|
||||
hex: true,
|
||||
rgba: true,
|
||||
input: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
instance.value.on('change', handleColorChange);
|
||||
}
|
||||
|
||||
function updateColor(color: string) {
|
||||
if (!instance.value) return;
|
||||
|
||||
instance.value.setColor(color);
|
||||
}
|
||||
|
||||
function updateDisabled(disabled: boolean) {
|
||||
if (!instance.value) return;
|
||||
|
||||
if (disabled) {
|
||||
instance.value.disable();
|
||||
} else {
|
||||
instance.value.enable();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.color,
|
||||
value => {
|
||||
updateColor(value);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
value => {
|
||||
updateDisabled(value);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initColorPicker();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
49
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
49
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
':soy: relative inline-flex justify-center items-center gap-12px px-12px py-4px border-1px border-solid rounded-4px cursor-pointer whitespace-nowrap',
|
||||
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps } from '../../types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ButtonTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<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 @@
|
||||
<template>
|
||||
<svg style="width: 100%; height: 100%">
|
||||
<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"></path>
|
||||
</symbol>
|
||||
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||
<use xlink:href="#geometry-left"></use>
|
||||
</symbol>
|
||||
<clipPath>
|
||||
<rect width="100%" height="100%" x="0"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="51%" height="100%">
|
||||
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor"></use>
|
||||
</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"></use>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ChromeTabBg'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
55
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
55
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
':soy: relative inline-flex justify-center items-center gap-16px -mr-18px px-24px py-6px cursor-pointer whitespace-nowrap',
|
||||
style['chrome-tab'],
|
||||
{ [style['chrome-tab_dark']]: darkMode },
|
||||
{ [style['chrome-tab_active']]: active },
|
||||
{ [style['chrome-tab_active_dark']]: active && darkMode }
|
||||
]"
|
||||
>
|
||||
<div :class="[':soy: absolute left-0 top-0 -z-1 w-full h-full pointer-events-none', style['chrome-tab__bg']]">
|
||||
<ChromeTabBg />
|
||||
</div>
|
||||
<slot name="prefix"></slot>
|
||||
<slot></slot>
|
||||
<slot name="suffix"></slot>
|
||||
<div :class="[':soy: absolute right-7px w-1px h-16px bg-#1f2225', style['chrome-tab-divider']]"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChromeTabBg from './chrome-tab-bg.vue';
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps } from '../../types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChromeTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
packages/materials/src/libs/page-tab/icon-close.vue
Normal file
31
packages/materials/src/libs/page-tab/icon-close.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div
|
||||
class=":soy: relative inline-flex justify-center items-center w-16px h-16px text-14px rd-50%"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'IconClose'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'click'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function handleClick() {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 .icon_close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.button-tab_dark .icon_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 .icon_close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
.chrome-tab_active .icon_close:hover {
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.chrome-tab_dark .icon_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);
|
||||
}
|
14
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
14
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
declare const styles: {
|
||||
readonly 'button-tab': string;
|
||||
readonly 'button-tab_dark': string;
|
||||
readonly 'button-tab_active': string;
|
||||
readonly 'button-tab_active_dark': string;
|
||||
readonly icon_close: 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;
|
||||
};
|
||||
export = 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;
|
94
packages/materials/src/libs/page-tab/index.vue
Normal file
94
packages/materials/src/libs/page-tab/index.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<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">
|
||||
<SvgIconClose v-if="closable" :class="[style['icon_close']]" @click="handleClose" />
|
||||
</slot>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { createTabCssVars, ACTIVE_COLOR } from './shared';
|
||||
import ChromeTab from './chrome-tab.vue';
|
||||
import ButtonTab from './button-tab.vue';
|
||||
import SvgIconClose from './icon-close.vue';
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps, PageTabMode } from '../../types';
|
||||
|
||||
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>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
|
||||
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>
|
||||
|
||||
<style scoped></style>
|
33
packages/materials/src/libs/page-tab/shared.ts
Normal file
33
packages/materials/src/libs/page-tab/shared.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { addColorAlpha, transformColorWithOpacity } from '@sa/utils';
|
||||
import type { PageTabCssVarsProps, PageTabCssVars } 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);
|
||||
}
|
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="flex-1-hidden h-full">
|
||||
<Simplebar class="h-full">
|
||||
<slot />
|
||||
</Simplebar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
282
packages/materials/src/types/index.ts
Normal file
282
packages/materials/src/types/index.ts
Normal file
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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
|
||||
* @description 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
|
||||
* @description 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
|
||||
* @description it can be used to get the corresponding Dom and scroll it
|
||||
* @default
|
||||
* ```ts
|
||||
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
|
||||
* ```
|
||||
* @example use the default id by import
|
||||
* ```ts
|
||||
* import { adminLayoutScrollElId } from '@sa/vue-materials';
|
||||
* ```
|
||||
*/
|
||||
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
|
||||
* @description 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
|
||||
* @description 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
|
||||
* @description 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
|
||||
* @description 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": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user