feat(projects): 1.0 beta

This commit is contained in:
Soybean 2023-11-17 08:45:00 +08:00
parent 1ea4817f6a
commit e918a2c0f5
499 changed files with 15918 additions and 24708 deletions

View File

@ -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
View File

@ -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

View File

@ -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'
};
}

View File

@ -1,2 +1,2 @@
VITE_HTTP_PROXY=Y
VITE_SOYBEAN_ROUTE_PLUGIN=Y

View File

@ -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

View File

@ -1,3 +0,0 @@
!.env-config.ts
router-page.d.ts

6
.eslintrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "sa/vue",
"settings": {
"import/core-modules": ["uno.css", "~icons/*", "virtual:svg-icons-register"]
}
}

View File

@ -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
View File

@ -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

View File

@ -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: 补充说明(可选)

View File

@ -1,11 +0,0 @@
## Pull Request 详情
请根据实际使用情况修改以下信息。
## 版本信息
## 解决了哪些问题
## 是否关闭了某个 Issue
Closes #

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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": {}
}
}

File diff suppressed because it is too large Load Diff

21
LICENSE
View File

@ -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.

View File

@ -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

View File

@ -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
};

View File

@ -1,2 +1 @@
export * from './define';
export * from './proxy';

View File

@ -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;
}

View File

@ -1,3 +0,0 @@
export * from './plugins';
export * from './config';
export * from './utils';

View File

@ -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 });
};

View File

@ -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;
}

View File

@ -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();
`
});
};

View File

@ -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
View 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
View 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
})
]
});
}

View File

@ -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;
}

View File

@ -1,7 +0,0 @@
import { visualizer } from 'rollup-plugin-visualizer';
export default visualizer({
gzipSize: true,
brotliSize: true,
open: true
});

View File

@ -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}`;
}

View File

@ -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

View File

@ -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

View File

@ -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
View 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}`;
}

View File

@ -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>

View File

@ -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;

View File

@ -1,5 +0,0 @@
import auth from './auth';
import route from './route';
import management from './management';
export default [...auth, ...route, ...management];

View File

@ -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;

View File

@ -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;

View File

@ -1,6 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
import api from './api';
export function setupMockServer() {
createProdMockServer(api);
}

View File

@ -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'
}
];

View File

@ -1,2 +0,0 @@
export * from './auth';
export * from './route';

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -0,0 +1,17 @@
{
"name": "@sa/color-palette",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
}
},
"dependencies": {
"colord": "2.9.3"
}
}

View 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();
}

View 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 };

File diff suppressed because it is too large Load Diff

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

View 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;
}

View 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;
}

View 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;
};

View 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: {}
}
});

View 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>
`;

View File

@ -0,0 +1,4 @@
import Theme from 'vitepress/theme';
import './style.css';
export default Theme;

View 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;
}

View 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"
}
}

View 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: 完善的前后端权限管理方案
---

View File

@ -0,0 +1,6 @@
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
};

View 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'
}
};

View 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]
}
};

View 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'
}
};

View 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'
}
};

View File

@ -0,0 +1,6 @@
const baseConfig = require('./configs/base');
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = baseConfig;

View 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"
}
}

File diff suppressed because it is too large Load Diff

View 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
};

View File

@ -0,0 +1,14 @@
{
"name": "@sa/hooks",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
}
}
}

View 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 };

View File

@ -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);

View 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
};
}

View File

@ -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);

View 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
};
}

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

View File

@ -0,0 +1,6 @@
{
"extends": "sa/vue",
"rules": {
"vue/multi-word-component-names": "off"
}
}

View 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"
}
}

View 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';

View 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);
}

View 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;

View 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 };

View 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>

View 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);
}

View File

@ -0,0 +1,3 @@
import ColorPicker from './index.vue';
export default ColorPicker;

View 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>

View 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>

View 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>

View 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>

View 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>

View 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);
}

View 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;

View File

@ -0,0 +1,3 @@
import PageTab from './index.vue';
export default PageTab;

View 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>

View 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);
}

View File

@ -0,0 +1,3 @@
import SimpleScrollbar from './index.vue';
export default SimpleScrollbar;

View 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>

View 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;
};

View 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