2
0

初始化项目

This commit is contained in:
caiyuchao
2024-11-14 11:06:38 +08:00
parent 988b9e6799
commit 4ffac789e1
320 changed files with 34244 additions and 0 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

44
.env Normal file
View File

@@ -0,0 +1,44 @@
VITE_BASE_URL=/
VITE_APP_TITLE=Vue-AntD-Web
VITE_APP_DESC=Vue-AntD-Web
# the prefix of the icon name
VITE_ICON_PREFIX=icon
# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
# format {VITE_ICON_PREFIX}-{local icon name}
VITE_ICON_LOCAL_PREFIX=icon-local
# auth route mode: static dynamic
VITE_AUTH_ROUTE_MODE=dynamic
# static auth route home
VITE_ROUTE_HOME=manage_user
# default menu icon
VITE_MENU_ICON=mdi:menu
# whether to enable http proxy when is dev mode
VITE_HTTP_PROXY=Y
# vue-router mode: hash | history | memory
VITE_ROUTER_HISTORY_MODE=history
# success code of backend service, when the code is received, the request is successful
VITE_SERVICE_SUCCESS_CODE=200
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
VITE_SERVICE_LOGOUT_CODES=401
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
VITE_SERVICE_MODAL_LOGOUT_CODES=401
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
# when the route mode is static, the defined super role
VITE_STATIC_SUPER_ROLE=R_SUPER

7
.env.prod Normal file
View File

@@ -0,0 +1,7 @@
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9529"
}`

7
.env.test Normal file
View File

@@ -0,0 +1,7 @@
# backend service base url, test environment
VITE_SERVICE_BASE_URL=http://localhost:8080
# other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"
}`

13
.gitattributes vendored Normal file
View File

@@ -0,0 +1,13 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.mjs" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.scss" eol=lf
"*.md" eol=lf
"*.yaml" eol=lf
"*.yml" eol=lf

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
yarn.lock
.VSCodeCounter
**/.vitepress/cache

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
registry=https://registry.npmmirror.com/
shamefully-hoist=true
ignore-workspace-root-check=true

22
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
"recommendations": [
"afzalsayed96.icones",
"antfu.iconify",
"antfu.unocss",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"formulahendry.auto-close-tag",
"formulahendry.auto-complete-tag",
"formulahendry.auto-rename-tag",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",
"naumovs.color-highlight",
"pkief.material-icon-theme",
"sdras.vue-vscode-snippets",
"vue.volar",
"whtouche.vscode-js-console-utils",
"zhuangtongfa.material-theme"
]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Vue Debugger",
"url": "http://localhost:9527",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "TS Debugger",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"program": "${file}"
}
]
}

27
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,27 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.experimental.useFlatConfig": true,
"editor.formatOnSave": false,
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
"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/langs"],
"prettier.enable": false,
"unocss.root": ["./"],
"cSpell.words": [
"fullscreen",
"hexcode",
"Localforage",
"nacos",
"ryadmin",
"Simplebar",
"treeselect",
"userinfo"
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 SoybeanJS
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 +1,32 @@
# Front-End at CRM
## Wfc-Cloud-Vue-AntD-Web(SoybeanAdmin AntDesign)
## 使用
**环境准备**
确保你的环境满足以下要求:
- **git**: 你需要git来克隆和管理项目版本。
- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
**安装依赖**
```bash
pnpm i
```
> 由于本项目采用了 pnpm monorepo 的管理方式,因此请不要使用 npm 或 yarn 来安装依赖。
**启动项目**
```bash
pnpm dev
```
**构建项目**
```bash
pnpm build
```

1
build/config/index.ts Normal file
View File

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

36
build/config/proxy.ts Normal file
View File

@@ -0,0 +1,36 @@
import type { ProxyOptions } from 'vite';
import { createServiceConfig } from '../../src/utils/service';
/**
* Set http proxy
*
* @param env - The current env
* @param isDev - Is development environment
*/
export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
if (!isEnableHttpProxy) return undefined;
const { baseURL, proxyPattern, other } = createServiceConfig(env);
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern });
other.forEach(item => {
Object.assign(proxy, createProxyItem(item));
});
return proxy;
}
function createProxyItem(item: App.Service.ServiceConfigItem) {
const proxy: Record<string, ProxyOptions> = {};
proxy[item.proxyPattern] = {
target: item.baseURL,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
};
return proxy;
}

24
build/plugins/index.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import progress from 'vite-plugin-progress';
import { setupElegantRouter } from './router';
import { setupUnocss } from './unocss';
import { setupUnplugin } from './unplugin';
export function setupVitePlugins(viteEnv: Env.ImportMeta) {
const plugins: PluginOption = [
vue({
script: {
defineModel: true
}
}),
vueJsx(),
setupElegantRouter(),
setupUnocss(viteEnv),
...setupUnplugin(viteEnv),
progress()
];
return plugins;
}

44
build/plugins/router.ts Normal file
View File

@@ -0,0 +1,44 @@
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'
},
customRoutes: {
names: ['exception_403', 'exception_404', 'exception_500']
},
routePathTransformer(routeName, routePath) {
const key = routeName as RouteKey;
if (key === 'login') {
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
const moduleReg = modules.join('|');
return `/login/:module(${moduleReg})?`;
}
return routePath;
},
onRouteMetaGen(routeName) {
const key = routeName as RouteKey;
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
const meta: Partial<RouteMeta> = {
title: key,
i18nKey: `route.${key}` as App.I18n.I18nKey
};
if (constantRoutes.includes(key)) {
meta.constant = true;
}
return meta;
}
});
}

32
build/plugins/unocss.ts Normal file
View File

@@ -0,0 +1,32 @@
import process from 'node:process';
import path from 'node:path';
import unocss from '@unocss/vite';
import presetIcons from '@unocss/preset-icons';
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
export function setupUnocss(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
return unocss({
presets: [
presetIcons({
prefix: `${VITE_ICON_PREFIX}-`,
scale: 1,
extraProperties: {
display: 'inline-block'
},
collections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
warn: true
})
]
});
}

69
build/plugins/unplugin.ts Normal file
View File

@@ -0,0 +1,69 @@
import process from 'node:process';
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 { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import AutoImport from 'unplugin-auto-import/vite';
export function setupUnplugin(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
const plugins: PluginOption[] = [
Icons({
compiler: 'vue3',
customCollections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dts: 'src/typings/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [
AntDesignVueResolver({
importStyle: false
}),
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
]
}),
createSvgIconsPlugin({
iconDirs: [localIconPath],
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
inject: 'body-last',
customDomId: '__SVG_ICON_LOCAL__'
}),
AutoImport({
imports: [
'vue',
'vue-router',
'vitest',
'@vueuse/core',
'pinia',
{ 'lodash-es': ['cloneDeep', 'assign', 'omit', 'pick'] },
{
'ant-design-vue': [
['message', '$message'],
['notification', '$notification'],
['Modal', '$modal']
]
}
],
dts: 'src/typings/auto-imports.d.ts',
dirs: ['src/utils/**', 'src/store/modules/**', 'src/hooks/common/**', 'src/service/api/**']
})
];
return plugins;
}

46
env.config.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* 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: 'http://localhost:8080',
otherBaseURL: {
demo: 'http://localhost:9528'
}
},
test: {
baseURL: mockURL,
otherBaseURL: {
demo: 'http://localhost:9529'
}
},
prod: {
baseURL: mockURL,
otherBaseURL: {
demo: 'http://localhost:9530'
}
}
};
const { VITE_SERVICE_ENV = 'dev' } = env;
return serviceConfigMap[VITE_SERVICE_ENV as keyof typeof serviceConfigMap];
}
/**
* Get proxy pattern of service url
*
* @param key If not set, will use the default key
*/
export function createProxyPattern(key?: App.Service.OtherBaseURLKey) {
if (!key) {
return '/proxy';
}
return `/proxy-${key}`;
}

25
eslint.config.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from '@soybeanjs/eslint-config';
export default defineConfig(
{ vue: true, unocss: true },
{
rules: {
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index', 'App', '[id]']
}
],
'vue/component-name-in-template-casing': [
'warn',
'PascalCase',
{
registeredComponentsOnly: false,
ignores: ['/^icon-/']
}
],
'order-attributify': 'off',
'no-warning-comments': 'off'
}
}
);

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!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_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

73
package.json Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "vue-antd",
"type": "module",
"version": "1.0.0",
"scripts": {
"build": "vite build --mode prod",
"build:test": "vite build --mode test",
"dev": "vite --mode test",
"dev:prod": "vite --mode prod",
"gen-route": "sa gen-route",
"lint": "eslint . --fix",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@sa/axios": "workspace:*",
"@sa/color-palette": "workspace:*",
"@sa/fetch": "workspace:*",
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "10.10.0",
"ant-design-vue": "4.2.2",
"clipboard": "2.0.11",
"dayjs": "1.11.11",
"echarts": "5.5.0",
"lodash-es": "4.17.21",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"vue": "3.4.27",
"vue-draggable-plus": "0.5.0",
"vue-i18n": "9.13.1",
"vue-router": "4.3.2"
},
"devDependencies": {
"@elegant-router/vue": "0.3.7",
"@iconify/json": "2.2.217",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.3.6",
"@types/lodash-es": "4.17.12",
"@types/node": "20.14.2",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.60.4",
"@unocss/preset-attributify": "^0.60.4",
"@unocss/preset-icons": "0.60.4",
"@unocss/preset-tagify": "^0.60.4",
"@unocss/preset-uno": "0.60.4",
"@unocss/transformer-directives": "0.60.4",
"@unocss/transformer-variant-group": "0.60.4",
"@unocss/vite": "0.60.4",
"@vitejs/plugin-vue": "5.0.5",
"@vitejs/plugin-vue-jsx": "4.0.0",
"eslint": "9.4.0",
"eslint-plugin-vue": "9.26.0",
"lint-staged": "15.2.5",
"sass": "1.77.4",
"simple-git-hooks": "2.11.1",
"tsx": "4.14.1",
"typescript": "5.4.5",
"unplugin-auto-import": "^0.17.6",
"unplugin-icons": "0.19.0",
"unplugin-vue-components": "0.27.0",
"vite": "5.2.13",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vitest": "^1.6.0",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.21"
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "@sa/axios",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.6.8",
"axios-retry": "4.1.0",
"qs": "6.12.1"
},
"devDependencies": {
"@types/qs": "6.9.15"
}
}

View File

@@ -0,0 +1,5 @@
/** request id key */
export const REQUEST_ID_KEY = 'X-Request-Id';
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

179
packages/axios/src/index.ts Normal file
View File

@@ -0,0 +1,179 @@
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
FlatRequestInstance,
MappedType,
RequestInstance,
RequestOption,
ResponseType
} from './type';
function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
axiosRetry(instance, retryOptions);
instance.interceptors.request.use(conf => {
const config: InternalAxiosRequestConfig = { ...conf };
// set request id
const requestId = nanoid();
config.headers.set(REQUEST_ID_KEY, requestId);
// config cancel token
const cancelTokenSource = axios.CancelToken.source();
config.cancelToken = cancelTokenSource.token;
cancelTokenSourceMap.set(requestId, cancelTokenSource);
// handle config by hook
const handledConfig = opts.onRequest?.(config) || config;
return handledConfig;
});
instance.interceptors.response.use(
async response => {
if (opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}
const fail = await opts.onBackendFail(response, instance);
if (fail) {
return fail;
}
const backendError = new AxiosError<ResponseData>(
'the backend request error',
BACKEND_ERROR_CODE,
response.config,
response.request,
response
);
await opts.onError(backendError);
return Promise.reject(backendError);
},
async (error: AxiosError<ResponseData>) => {
await opts.onError(error);
return Promise.reject(error);
}
);
function cancelRequest(requestId: string) {
const cancelTokenSource = cancelTokenSourceMap.get(requestId);
if (cancelTokenSource) {
cancelTokenSource.cancel();
cancelTokenSourceMap.delete(requestId);
}
}
function cancelAllRequest() {
cancelTokenSourceMap.forEach(cancelTokenSource => {
cancelTokenSource.cancel();
});
cancelTokenSourceMap.clear();
}
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}
/**
* create a request instance
*
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
return opts.transformBackendResponse(response);
}
return response.data as MappedType<R, T>;
} as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;
request.state = {} as State;
return request;
}
/**
* create a flat request instance
*
* The response data is a flat object: { data: any, error: AxiosError }
*
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
T = any,
R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
try {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
const data = opts.transformBackendResponse(response);
return { data, error: null };
}
return { data: response.data as MappedType<R, T>, error: null };
} catch (error) {
return { data: null, error };
}
} as FlatRequestInstance<State, ResponseData>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State;
return flatRequest;
}
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
export type * from './type';
export type { CreateAxiosDefaults, AxiosError };

View File

@@ -0,0 +1,48 @@
import type { CreateAxiosDefaults } from 'axios';
import type { IAxiosRetryConfig } from 'axios-retry';
import { stringify } from 'qs';
import { isHttpSuccess } from './shared';
import type { RequestOption } from './type';
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
const opts: RequestOption<ResponseData> = {
onRequest: async config => config,
isBackendSuccess: _response => true,
onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {}
};
Object.assign(opts, options);
return opts;
}
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
const retryConfig: IAxiosRetryConfig = {
retries: 3
};
Object.assign(retryConfig, config);
return retryConfig;
}
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
const TEN_SECONDS = 10 * 1000;
const axiosConfig: CreateAxiosDefaults = {
timeout: TEN_SECONDS,
headers: {
'Content-Type': 'application/json'
},
validateStatus: isHttpSuccess,
paramsSerializer: params => {
return stringify(params);
}
};
Object.assign(axiosConfig, config);
return axiosConfig;
}

View File

@@ -0,0 +1,28 @@
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export function getContentType(config: InternalAxiosRequestConfig) {
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
return contentType;
}
/**
* check if http status is success
*
* @param status
*/
export function isHttpSuccess(status: number) {
const isSuccessCode = status >= 200 && status < 300;
return isSuccessCode || status === 304;
}
/**
* is response json
*
* @param response axios response
*/
export function isResponseJson(response: AxiosResponse) {
const { responseType } = response.config;
return responseType === 'json' || responseType === undefined;
}

101
packages/axios/src/type.ts Normal file
View File

@@ -0,0 +1,101 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export type ContentType =
| 'text/html'
| 'text/plain'
| 'multipart/form-data'
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param config Axios config
*/
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* The hook to check backend response is success or not
*
* @param response Axios response
*/
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
/**
* The hook after backend request fail
*
* For example: You can handle the expired token in this hook
*
* @param response Axios response
* @param instance Axios instance
*/
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}
interface ResponseMap {
blob: Blob;
text: string;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
? ResponseMap[R]
: JsonType;
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
responseType?: R;
};
export interface RequestInstanceCommon<T> {
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;
}
/** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<T = any> = {
data: T;
error: null;
};
export type FlatResponseFailData<ResponseData = any> = {
data: null;
error: AxiosError<ResponseData>;
};
export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<T>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,15 @@
{
"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,41 @@
import { getColorPaletteFamily } from './palette';
import { getColorName } from './name';
import type { ColorPalette, ColorPaletteFamily, ColorPaletteItem, ColorPaletteNumber } 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;
}
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, getHsl, getRgb } 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 { getDeltaE, getHsl, isValidColor, 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,49 @@
/** 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
*
* Which number is 500
*/
main: ColorPaletteItemWithName;
/** The match color of the palette */
match: ColorPaletteItemWithName;
};

View File

@@ -0,0 +1,39 @@
import process from 'node:process';
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,86 @@
/**
* 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": {
"build": "vitepress build",
"dev": "vitepress dev",
"serve": "vitepress serve"
},
"devDependencies": {
"vitepress": "1.0.0-rc.36"
}
}

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,4 @@
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
};

View File

@@ -0,0 +1,42 @@
/** @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,9 @@
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,59 @@
/** @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,28 @@
/** @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,4 @@
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.56.1",
"@typescript-eslint/eslint-plugin": "6.18.0",
"@typescript-eslint/parser": "6.18.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.6.1",
"eslint-plugin-prettier": "5.1.2",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.19.2",
"prettier": "3.1.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
/** @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,15 @@
{
"name": "@sa/hooks",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*"
}
}

View File

@@ -0,0 +1,10 @@
import useBoolean from './use-boolean';
import useLoading from './use-loading';
import useCountDown from './use-count-down';
import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export * from './use-table';

View File

@@ -0,0 +1,31 @@
import { ref } from 'vue';
/**
* Boolean
*
* @param initValue Init value
*/
export default function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle
};
}

View File

@@ -0,0 +1,96 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/**
* Use context
*
* @example
* ```ts
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
*
* // context.ts
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
* count.value++;
* }
*
* function decrement() {
* count.value--;
* }
*
* return {
* count,
* increment,
* decrement
* };
* })
* ``` // A.vue
* ```vue
* <template>
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
*
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
* <template>
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
*
* const { count, increment } = useStore();
* </script>
* ```;
*
* // C.vue is same as B.vue
*
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
return {
/** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
};
}
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvide(context: T) {
provide(injectKey, context);
return context;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
};
}

View File

@@ -0,0 +1,49 @@
import { computed, onScopeDispose, ref } from 'vue';
import { useRafFn } from '@vueuse/core';
/**
* count down
*
* @param seconds - count down seconds
*/
export default function useCountDown(seconds: number) {
const FPS_PER_SECOND = 60;
const fps = ref(0);
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
const isCounting = computed(() => fps.value > 0);
const { pause, resume } = useRafFn(
() => {
if (fps.value > 0) {
fps.value -= 1;
} else {
pause();
}
},
{ immediate: false }
);
function start(updateSeconds: number = seconds) {
fps.value = FPS_PER_SECOND * updateSeconds;
resume();
}
function stop() {
fps.value = 0;
pause();
}
onScopeDispose(() => {
pause();
});
return {
count,
isCounting,
start,
stop
};
}

View File

@@ -0,0 +1,16 @@
import useBoolean from './use-boolean';
/**
* Loading
*
* @param initValue Init value
*/
export default function useLoading(initValue = false) {
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
return {
loading,
startLoading,
endLoading
};
}

View File

@@ -0,0 +1,79 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { createFlatRequest } from '@sa/axios';
import type {
AxiosError,
CreateAxiosDefaults,
CustomAxiosRequestConfig,
MappedType,
RequestOption,
ResponseType
} from '@sa/axios';
import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<T>;
error: Ref<null>;
};
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
data: Ref<null>;
error: Ref<AxiosError<ResponseData>>;
};
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData = any> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}
/**
* create a hook request instance
*
* @param axiosConfig
* @param options
*/
export default function createHookRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const request = createFlatRequest<ResponseData>(axiosConfig, options);
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const { loading, startLoading, endLoading } = useLoading();
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
startLoading();
request(config).then(res => {
if (res.data) {
data.value = res.data;
} else {
error.value = res.error;
}
endLoading();
});
return {
loading,
data,
error
};
} as HookRequestInstance<ResponseData>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest;
return hookRequest;
}

View File

@@ -0,0 +1,50 @@
import { h } from 'vue';
import type { Component } from 'vue';
/**
* Svg icon render hook
*
* @param SvgIcon Svg icon component
*/
export default function useSvgIconRender(SvgIcon: Component) {
interface IconConfig {
/** Iconify icon name */
icon?: string;
/** Local icon name */
localIcon?: string;
/** Icon color */
color?: string;
/** Icon size */
fontSize?: number;
}
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
/**
* Svg icon VNode
*
* @param config
*/
const SvgIconVNode = (config: IconConfig) => {
const { color, fontSize, icon, localIcon } = config;
const style: IconStyle = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
if (!icon && !localIcon) {
return undefined;
}
return () => h(SvgIcon, { icon, localIcon, style });
};
return {
SvgIconVNode
};
}

View File

@@ -0,0 +1,152 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheck = {
key: string;
title: string;
checked: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
rows: TableDataWithIndex<T>[];
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** row key */
rowKey?: string;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* callback when response fetched
*
* @param transformed transformed data
*/
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
...col,
checked: checkMap.get(col.key) ?? col.checked
}));
}
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.rows;
setEmpty(transformed.rows.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
getData();
}
if (immediate) {
getData();
}
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

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

View File

@@ -0,0 +1,20 @@
{
"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.9.1"
}
}

View File

@@ -0,0 +1,7 @@
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
import PageTab from './libs/page-tab';
import SimpleScrollbar from './libs/simple-scrollbar';
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 default styles;

View File

@@ -0,0 +1,5 @@
import AdminLayout from './index.vue';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
export default AdminLayout;
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };

View File

@@ -0,0 +1,237 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { AdminLayoutProps } from '../../types';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
import style from './index.module.css';
defineOptions({
name: 'AdminLayout'
});
const props = withDefaults(defineProps<AdminLayoutProps>(), {
mode: 'vertical',
scrollMode: 'content',
scrollElId: LAYOUT_SCROLL_EL_ID,
commonClass: 'transition-all-300',
fixedTop: true,
maxZIndex: LAYOUT_MAX_Z_INDEX,
headerVisible: true,
headerHeight: 56,
tabVisible: true,
tabHeight: 48,
siderVisible: true,
siderCollapse: false,
siderWidth: 220,
siderCollapsedWidth: 64,
footerVisible: true,
footerHeight: 48,
rightFooter: false
});
interface Emits {
/** Update siderCollapse */
(e: 'update:siderCollapse', collapse: boolean): void;
}
const emit = defineEmits<Emits>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/** Main */
default?: SlotFn;
/** Header */
header?: SlotFn;
/** Tab */
tab?: SlotFn;
/** Sider */
sider?: SlotFn;
/** Footer */
footer?: SlotFn;
};
const slots = defineSlots<Slots>();
const cssVars = computed(() => createLayoutCssVars(props));
// config visible
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
// scroll mode
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
const isContentScroll = computed(() => props.scrollMode === 'content');
// layout direction
const isVertical = computed(() => props.mode === 'vertical');
const isHorizontal = computed(() => props.mode === 'horizontal');
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
// css
const leftGapClass = computed(() => {
if (!props.fullContent && showSider.value) {
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
}
return '';
});
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
const footerLeftGapClass = computed(() => {
const condition1 = isVertical.value;
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
if (condition1 || condition2 || condition3) {
return leftGapClass.value;
}
return '';
});
const siderPaddingClass = computed(() => {
let cls = '';
if (showHeader.value && !headerLeftGapClass.value) {
cls += style['sider-padding-top'];
}
if (showFooter.value && !footerLeftGapClass.value) {
cls += ` ${style['sider-padding-bottom']}`;
}
return cls;
});
function handleClickMask() {
emit('update:siderCollapse', true);
}
</script>
<template>
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
class="h-full flex flex-col"
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-header'],
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="header"></slot>
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-header-placement']]"
></div>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
class="flex-shrink-0"
:class="[
style['layout-tab'],
commonClass,
tabClass,
{ 'top-0!': fullContent || !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="tab"></slot>
</div>
<div
v-show="fullContent || fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-tab-placement']]"
></div>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
class="absolute left-0 top-0 h-full"
:class="[
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
class="absolute left-0 top-0 h-full w-0 bg-white"
:class="[
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
<div
v-show="!siderCollapse"
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
:class="[style['layout-mobile-sider-mask']]"
@click="handleClickMask"
></div>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
class="flex flex-col flex-grow"
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
>
<slot></slot>
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-footer'],
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
]"
>
<slot name="footer"></slot>
</footer>
<div
v-show="!fullContent && fixedFooter"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-footer-placement']]"
></div>
</template>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,68 @@
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
/** The id of the scroll element of the layout */
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
/** The max z-index of the layout */
export const LAYOUT_MAX_Z_INDEX = 100;
/**
* Create layout css vars by css vars props
*
* @param props Css vars props
*/
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
const cssVars: LayoutCssVars = {
'--soy-header-height': `${props.headerHeight}px`,
'--soy-header-z-index': props.headerZIndex,
'--soy-tab-height': `${props.tabHeight}px`,
'--soy-tab-z-index': props.tabZIndex,
'--soy-sider-width': `${props.siderWidth}px`,
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
'--soy-sider-z-index': props.siderZIndex,
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
'--soy-footer-height': `${props.footerHeight}px`,
'--soy-footer-z-index': props.footerZIndex
};
return cssVars;
}
/**
* Create layout css vars
*
* @param props
*/
export function createLayoutCssVars(props: AdminLayoutProps) {
const {
mode,
isMobile,
maxZIndex = LAYOUT_MAX_Z_INDEX,
headerHeight,
tabHeight,
siderWidth,
siderCollapsedWidth,
footerHeight
} = props;
const headerZIndex = maxZIndex - 3;
const tabZIndex = maxZIndex - 5;
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
const footerZIndex = maxZIndex - 5;
const cssProps: LayoutCssVarsProps = {
headerHeight,
headerZIndex,
tabHeight,
tabZIndex,
siderWidth,
siderZIndex,
mobileSiderZIndex,
siderCollapsedWidth,
footerHeight,
footerZIndex
};
return createLayoutCssVarsByCssVarsProps(cssProps);
}

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 { onMounted, ref, watch } 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,53 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import style from './index.module.css';
defineOptions({
name: 'ButtonTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-1px rounded-4px border-solid px-12px py-4px"
:class="[
style['button-tab'],
{ [style['button-tab_dark']]: darkMode },
{ [style['button-tab_active']]: active },
{ [style['button-tab_active_dark']]: active && darkMode }
]"
>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'ChromeTabBg'
});
</script>
<template>
<svg class="size-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
</svg>
</g>
</svg>
</template>
<style scoped></style>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import ChromeTabBg from './chrome-tab-bg.vue';
import style from './index.module.css';
defineOptions({
name: 'ChromeTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
:class="[
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode }
]"
>
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'IconClose'
});
const emit = defineEmits<Emits>();
interface Emits {
(e: 'click'): void;
}
function handleClick() {
emit('click');
}
</script>
<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>
<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 .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: var(--soy-primary-color);
}
.button-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab:hover {
z-index: 9;
}
.chrome-tab_active {
z-index: 10;
color: var(--soy-primary-color);
}
.chrome-tab__bg {
color: transparent;
}
.chrome-tab_active .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_active_dark .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab:hover .chrome-tab__bg {
color: #dee1e6;
}
.chrome-tab_active:hover .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_dark:hover .chrome-tab__bg {
color: #333333;
}
.chrome-tab_active_dark:hover .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: #9ca3af;
}
.chrome-tab_active .svg-close:hover {
background-color: var(--soy-primary-color);
}
.chrome-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab_active .chrome-tab-divider {
opacity: 0;
}
.chrome-tab:hover .chrome-tab-divider {
opacity: 0;
}
.chrome-tab_dark .chrome-tab-divider {
background-color: rgba(255, 255, 255, 0.9);
}

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 'chrome-tab': string;
readonly 'chrome-tab_active': string;
readonly 'chrome-tab__bg': string;
readonly 'chrome-tab_active_dark': string;
readonly 'chrome-tab_dark': string;
readonly 'chrome-tab-divider': string;
readonly 'svg-close': string;
};
export default styles;

View File

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

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import type { PageTabMode, PageTabProps } from '../../types';
import { ACTIVE_COLOR, createTabCssVars } from './shared';
import ChromeTab from './chrome-tab.vue';
import ButtonTab from './button-tab.vue';
import SvgClose from './svg-close.vue';
import style from './index.module.css';
defineOptions({
name: 'PageTab'
});
const props = withDefaults(defineProps<PageTabProps>(), {
mode: 'chrome',
commonClass: 'transition-all-300',
activeColor: ACTIVE_COLOR,
closable: true
});
interface Emits {
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass } = props;
const tabComponentMap = {
chrome: {
component: ChromeTab,
class: chromeClass
},
button: {
component: ButtonTab,
class: buttonClass
}
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
return tabComponentMap[mode];
});
const cssVars = computed(() => createTabCssVars(props.activeColor));
const bindProps = computed(() => {
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
return rest;
});
function handleClose() {
emit('close');
}
</script>
<template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
<template #prefix>
<slot name="prefix"></slot>
</template>
<slot></slot>
<template #suffix>
<slot name="suffix">
<SvgClose v-if="closable" :class="[style['svg-close']]" @click="handleClose" />
</slot>
</template>
</component>
</template>
<style scoped></style>

View File

@@ -0,0 +1,31 @@
import { addColorAlpha, transformColorWithOpacity } from '@sa/utils';
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
/** The active color of the tab */
export const ACTIVE_COLOR = '#1890ff';
function createCssVars(props: PageTabCssVarsProps) {
const cssVars: PageTabCssVars = {
'--soy-primary-color': props.primaryColor,
'--soy-primary-color1': props.primaryColor1,
'--soy-primary-color2': props.primaryColor2,
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
'--soy-primary-color-opacity3': props.primaryColorOpacity3
};
return cssVars;
}
export function createTabCssVars(primaryColor: string) {
const cssProps: PageTabCssVarsProps = {
primaryColor,
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
};
return createCssVars(cssProps);
}

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'SvgClose'
});
const emit = defineEmits<Emits>();
interface Emits {
(e: 'click'): void;
}
function handleClick() {
emit('click');
}
</script>
<template>
<div
class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px"
@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"
/>
</svg>
</div>
</template>
<style scoped></style>

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="h-full flex-1-hidden">
<Simplebar data-simplebar-auto-hide="false" class="h-full">
<slot />
</Simplebar>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,294 @@
/** Header config */
interface AdminLayoutHeaderConfig {
/**
* Whether header is visible
*
* @default true
*/
headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/**
* Header height
*
* @default 56px
*/
headerHeight?: number;
}
/** Tab config */
interface AdminLayoutTabConfig {
/**
* Whether tab is visible
*
* @default true
*/
tabVisible?: boolean;
/**
* Tab class
*
* @default ''
*/
tabClass?: string;
/**
* Tab height
*
* @default 48px
*/
tabHeight?: number;
}
/** Sider config */
interface AdminLayoutSiderConfig {
/**
* Whether sider is visible
*
* @default true
*/
siderVisible?: boolean;
/**
* Sider class
*
* @default ''
*/
siderClass?: string;
/**
* Mobile sider class
*
* @default ''
*/
mobileSiderClass?: string;
/**
* Sider collapse status
*
* @default false
*/
siderCollapse?: boolean;
/**
* Sider width when collapse is false
*
* @default '220px'
*/
siderWidth?: number;
/**
* Sider width when collapse is true
*
* @default '64px'
*/
siderCollapsedWidth?: number;
}
/** Content config */
export interface AdminLayoutContentConfig {
/**
* Content class
*
* @default ''
*/
contentClass?: string;
/**
* Whether content is full the page
*
* If true, other elements will be hidden by `display: none`
*/
fullContent?: boolean;
}
/** Footer config */
export interface AdminLayoutFooterConfig {
/**
* Whether footer is visible
*
* @default true
*/
footerVisible?: boolean;
/**
* Whether footer is fixed
*
* @default true
*/
fixedFooter?: boolean;
/**
* Footer class
*
* @default ''
*/
footerClass?: string;
/**
* Footer height
*
* @default 48px
*/
footerHeight?: number;
/**
* Whether footer is on the right side
*
* When the layout is vertical, the footer is on the right side
*/
rightFooter?: boolean;
}
/**
* Layout mode
*
* - Horizontal
* - Vertical
*/
export type LayoutMode = 'horizontal' | 'vertical';
/**
* The scroll mode when content overflow
*
* - Wrapper: the layout component's wrapper element has a scrollbar
* - Content: the layout component's content element has a scrollbar
*
* @default 'wrapper'
*/
export type LayoutScrollMode = 'wrapper' | 'content';
/** Admin layout props */
export interface AdminLayoutProps
extends AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
AdminLayoutSiderConfig,
AdminLayoutContentConfig,
AdminLayoutFooterConfig {
/**
* Layout mode
*
* - {@link LayoutMode}
*/
mode?: LayoutMode;
/** Is mobile layout */
isMobile?: boolean;
/**
* Scroll mode
*
* - {@link ScrollMode}
*/
scrollMode?: LayoutScrollMode;
/**
* The id of the scroll element of the layout
*
* It can be used to get the corresponding Dom and scroll it
*
* @example
* use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*
* @default
* ```ts
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
* ```
*/
scrollElId?: string;
/** The class of the scroll element */
scrollElClass?: string;
/** The class of the scroll wrapper element */
scrollWrapperClass?: string;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/**
* Whether fix the header and tab
*
* @default true
*/
fixedTop?: boolean;
/**
* The max z-index of the layout
*
* The z-index of Header,Tab,Sider and Footer will not exceed this value
*/
maxZIndex?: number;
}
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
: S;
type Prefix = '--soy-';
export type LayoutCssVarsProps = Pick<
AdminLayoutProps,
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
> & {
headerZIndex?: number;
tabZIndex?: number;
siderZIndex?: number;
mobileSiderZIndex?: number;
footerZIndex?: number;
};
export type LayoutCssVars = {
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};
/**
* The mode of the tab
*
* - Button: button style
* - Chrome: chrome style
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome';
export interface PageTabProps {
/** Whether is dark mode */
darkMode?: boolean;
/**
* The mode of the tab
*
* - {@link TabMode}
*/
mode?: PageTabMode;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/** The class of the button tab */
buttonClass?: string;
/** The class of the chrome tab */
chromeClass?: string;
/** Whether the tab is active */
active?: boolean;
/** The color of the active tab */
activeColor?: string;
/**
* Whether the tab is closable
*
* Show the close icon when true
*/
closable?: boolean;
}
export type PageTabCssVarsProps = {
primaryColor: string;
primaryColor1: string;
primaryColor2: string;
primaryColorOpacity1: string;
primaryColorOpacity2: string;
primaryColorOpacity3: string;
};
export type PageTabCssVars = {
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,15 @@
{
"name": "@sa/fetch",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.3.4"
}
}

View File

@@ -0,0 +1,10 @@
import { ofetch } from 'ofetch';
import type { FetchOptions } from 'ofetch';
export function createRequest(options: FetchOptions) {
const request = ofetch.create(options);
return request;
}
export default createRequest;

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

3
packages/scripts/bin.ts Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env tsx
import './src/index.ts';

View File

@@ -0,0 +1,27 @@
{
"name": "@sa/scripts",
"version": "1.0.0",
"bin": {
"sa": "./bin.ts"
},
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.22",
"bumpp": "9.4.0",
"c12": "1.10.0",
"cac": "6.7.14",
"consola": "3.2.3",
"enquirer": "^2.4.1",
"execa": "8.0.1",
"kolorist": "1.8.0",
"npm-check-updates": "16.14.18",
"rimraf": "5.0.5"
}
}

View File

@@ -0,0 +1,10 @@
import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog';
import type { ChangelogOption } from '@soybeanjs/changelog';
export async function genChangelog(options?: Partial<ChangelogOption>, total = false) {
if (total) {
await generateTotalChangelog(options);
} else {
await generateChangelog(options);
}
}

View File

@@ -0,0 +1,5 @@
import { rimraf } from 'rimraf';
export async function cleanup(paths: string[]) {
await rimraf(paths, { glob: true });
}

View File

@@ -0,0 +1,86 @@
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { prompt } from 'enquirer';
import { bgRed, green, red, yellow } from 'kolorist';
import { execCommand } from '../shared';
import type { CliOption } from '../types';
interface PromptObject {
types: string;
scopes: string;
description: string;
}
/**
* Git commit with Conventional Commits standard
*
* @param gitCommitTypes
* @param gitCommitScopes
*/
export async function gitCommit(
gitCommitTypes: CliOption['gitCommitTypes'],
gitCommitScopes: CliOption['gitCommitScopes']
) {
const typesChoices = gitCommitTypes.map(([value, msg]) => {
const nameWithSuffix = `${value}:`;
const message = `${nameWithSuffix.padEnd(12)}${msg}`;
return {
name: value,
message
};
});
const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
name: value,
message: `${value.padEnd(30)} (${msg})`
}));
const result = await prompt<PromptObject>([
{
name: 'types',
type: 'select',
message: 'Please select a type',
choices: typesChoices
},
{
name: 'scopes',
type: 'select',
message: 'Please select a scope',
choices: scopesChoices
},
{
name: 'description',
type: 'text',
message: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
}
]);
const breaking = result.description.startsWith('!') ? '!' : '';
const description = result.description.replace(/^!/, '').trim();
const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`;
await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
}
/** Git commit message verify */
export async function gitCommitVerify() {
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
if (!REG_EXP.test(commitMsg)) {
throw new Error(
`${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
);
}
}

View File

@@ -0,0 +1,6 @@
export * from './git-commit';
export * from './cleanup';
export * from './update-pkg';
export * from './changelog';
export * from './release';
export * from './router';

View File

@@ -0,0 +1,12 @@
import { versionBump } from 'bumpp';
export async function release(execute = 'pnpm sa changelog', push = true) {
await versionBump({
files: ['**/package.json', '!**/node_modules'],
execute,
all: true,
tag: true,
commit: 'chore(projects): release v%s',
push
});
}

View File

@@ -0,0 +1,90 @@
import process from 'node:process';
import path from 'node:path';
import { writeFile } from 'node:fs/promises';
import { existsSync, mkdirSync } from 'node:fs';
import { prompt } from 'enquirer';
import { green, red } from 'kolorist';
interface PromptObject {
routeName: string;
addRouteParams: boolean;
routeParams: string;
}
/** generate route */
export async function generateRoute() {
const result = await prompt<PromptObject>([
{
name: 'routeName',
type: 'text',
message: 'please enter route name',
initial: 'demo-route_child'
},
{
name: 'addRouteParams',
type: 'confirm',
message: 'add route params?',
initial: false
}
]);
if (result.addRouteParams) {
const answers = await prompt<PromptObject>({
name: 'routeParams',
type: 'text',
message: 'please enter route params',
initial: 'id'
});
Object.assign(result, answers);
}
const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/;
if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) {
throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}.
For example:
(1) one level route: ${green('demo-route')}
(2) two level route: ${green('demo-route_child')}
(3) multi level route: ${green('demo-route_child_child')}
(4) group route: ${green('_ignore_demo-route')}'
`);
}
const PARAM_REG = /^\w+$/g;
if (result.routeParams && !PARAM_REG.test(result.routeParams)) {
throw new Error(red('route params is invalid, it only allow letters, numbers or "_".'));
}
const cwd = process.cwd();
const [dir, ...rest] = result.routeName.split('_') as string[];
let routeDir = path.join(cwd, 'src', 'views', dir);
if (rest.length) {
routeDir = path.join(routeDir, rest.join('_'));
}
if (!existsSync(routeDir)) {
mkdirSync(routeDir, { recursive: true });
} else {
throw new Error(red('route already exists'));
}
const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue';
const vueTemplate = `<script setup lang="ts"></script>
<template>
<div>${result.routeName}</div>
</template>
<style scoped></style>
`;
const filePath = path.join(routeDir, fileName);
await writeFile(filePath, vueTemplate);
}

View File

@@ -0,0 +1,5 @@
import { execCommand } from '../shared';
export async function updatePkg(args: string[] = ['--deep', '-u']) {
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
}

View File

@@ -0,0 +1,53 @@
import process from 'node:process';
import { loadConfig } from 'c12';
import type { CliOption } from '../types';
const defaultOptions: CliOption = {
cwd: process.cwd(),
cleanupDirs: [
'**/dist',
'**/package-lock.json',
'**/yarn.lock',
'**/pnpm-lock.yaml',
'**/node_modules',
'!node_modules/**'
],
gitCommitTypes: [
['feat', 'A new feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
],
gitCommitScopes: [
['projects', 'project'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
],
ncuCommandArgs: ['--deep', '-u'],
changelogOptions: {}
};
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {
const { config } = await loadConfig<Partial<CliOption>>({
name: 'soybean',
defaults: defaultOptions,
overrides,
cwd,
packageJson: true
});
return config as CliOption;
}

View File

@@ -0,0 +1,101 @@
import cac from 'cac';
import { blue, lightGreen } from 'kolorist';
import { version } from '../package.json';
import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
import { loadCliOptions } from './config';
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
interface CommandArg {
/** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */
execute?: string;
/** Indicates whether to push the git commit and tag. Defaults to true */
push?: boolean;
/** Generate changelog by total tags */
total?: boolean;
/**
* The glob pattern of dirs to cleanup
*
* If not set, it will use the default value
*
* Multiple values use "," to separate them
*/
cleanupDir?: string;
}
export async function setupCli() {
const cliOptions = await loadCliOptions();
const cli = cac(blue('soybean-admin'));
cli
.version(lightGreen(version))
.option(
'-e, --execute [command]',
"Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'"
)
.option('-p, --push', 'Indicates whether to push the git commit and tag')
.option('-t, --total', 'Generate changelog by total tags')
.option(
'-c, --cleanupDir <dir>',
'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
)
.help();
const commands: CommandWithAction<CommandArg> = {
cleanup: {
desc: 'delete dirs: node_modules, dist, etc.',
action: async () => {
await cleanup(cliOptions.cleanupDirs);
}
},
'update-pkg': {
desc: 'update package.json dependencies versions',
action: async () => {
await updatePkg(cliOptions.ncuCommandArgs);
}
},
'git-commit': {
desc: 'git commit, generate commit message which match Conventional Commits standard',
action: async () => {
await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
}
},
'git-commit-verify': {
desc: 'verify git commit message, make sure it match Conventional Commits standard',
action: async () => {
await gitCommitVerify();
}
},
changelog: {
desc: 'generate changelog',
action: async args => {
await genChangelog(cliOptions.changelogOptions, args?.total);
}
},
release: {
desc: 'release: update version, generate changelog, commit code',
action: async args => {
await release(args?.execute, args?.push);
}
},
'gen-route': {
desc: 'generate route',
action: async () => {
await generateRoute();
}
}
};
for (const [command, { desc, action }] of Object.entries(commands)) {
cli.command(command, lightGreen(desc)).action(action);
}
cli.parse();
}
setupCli();

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