mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-12 04:43:42 +08:00
Compare commits
453 Commits
v0.9.7
...
tauri-v1.0
Author | SHA1 | Date | |
---|---|---|---|
|
ebdef724b0 | ||
|
4af884c08b | ||
|
41e470ed6e | ||
|
2f15a2ac6a | ||
|
1c72dc76ee | ||
|
a1b484a8eb | ||
|
d9410e416e | ||
|
769d84a7f4 | ||
|
0fbb00cee1 | ||
|
cc539974b2 | ||
|
1ec5ea0ff1 | ||
|
9125cc9b60 | ||
|
76011af8a9 | ||
|
4babbe19a3 | ||
|
ae508bc572 | ||
|
c8260221dd | ||
|
82b53d7e98 | ||
|
f69e1523c0 | ||
|
c0009203c6 | ||
|
f23e1f7ea4 | ||
|
f89e6c0361 | ||
|
6b5132c169 | ||
|
1a7070f02b | ||
|
07d8d25dbf | ||
|
a0bad57a4e | ||
|
8c7ea235d9 | ||
|
06e204a7fe | ||
|
7b298c679e | ||
|
25fc24c746 | ||
|
4cb17c7440 | ||
|
46b6156620 | ||
|
23a4098bba | ||
|
000c68ad60 | ||
|
2c543f19ec | ||
|
37d20b8e0d | ||
|
c11d56da29 | ||
|
41e8bc44f8 | ||
|
11a6a3bd80 | ||
|
1ed33dc47a | ||
|
c91dd282a6 | ||
|
f91ef30bd5 | ||
|
187098136e | ||
|
0abbfa5d0c | ||
|
71f2c5535b | ||
|
49558ca048 | ||
|
c0a65a165a | ||
|
3fd15e5649 | ||
|
4158a72bd8 | ||
|
8630175a3f | ||
|
c3efa1b6e0 | ||
|
51d7758903 | ||
|
d28bf52683 | ||
|
455e48f792 | ||
|
6ad51e9431 | ||
|
0fee104bf9 | ||
|
0cc8f05a26 | ||
|
03c2f3aaf0 | ||
|
6c5404610b | ||
|
8143b0063c | ||
|
72745229d6 | ||
|
779ba4e415 | ||
|
b2ee9eef8c | ||
|
7065f6f073 | ||
|
dc24a36739 | ||
|
c05e7ff360 | ||
|
2b7e3c237d | ||
|
874aaca5bb | ||
|
7a2f54be8b | ||
|
b37c1e94a0 | ||
|
cb35a94baf | ||
|
02d4b0a574 | ||
|
4e29acaae3 | ||
|
7898d08174 | ||
|
ed1c585a6b | ||
|
0126da4778 | ||
|
04aa10b477 | ||
|
9ea878915e | ||
|
05db8c08d5 | ||
|
ffc95d2b7b | ||
|
c8019c4ff7 | ||
|
b546ff8625 | ||
|
14aa856a0f | ||
|
3db82acb02 | ||
|
4063529a25 | ||
|
b4c00ce199 | ||
|
fb3b94b1e3 | ||
|
43193e2808 | ||
|
bccd6cb3c3 | ||
|
a17d68eb11 | ||
|
9cf2a513f7 | ||
|
0123c37de2 | ||
|
418302a34b | ||
|
7fa87f53d1 | ||
|
b235ef3bc0 | ||
|
3743612d62 | ||
|
c3e8ef2ed2 | ||
|
ccc2b674db | ||
|
fdf64f71f4 | ||
|
9c4ba6650d | ||
|
1a6be003e2 | ||
|
c7e2c55996 | ||
|
0a3efe3442 | ||
|
7256ad4ee3 | ||
|
8ee2acc1fc | ||
|
72a4679c0a | ||
|
3bdcbc71eb | ||
|
9772aec2bd | ||
|
c18d82f9f8 | ||
|
114c835921 | ||
|
e6fed1fdb5 | ||
|
5d75ca924e | ||
|
2d102a054a | ||
|
a9c98d9655 | ||
|
ef4af79a11 | ||
|
806a1cba5f | ||
|
147f60d17e | ||
|
579636b0b4 | ||
|
ebb154840a | ||
|
ae6b6134f3 | ||
|
6ea9b85ff1 | ||
|
0b56e44c0e | ||
|
13001bc83e | ||
|
adec0d7501 | ||
|
db17c916ef | ||
|
e2085e058d | ||
|
b8f85692d6 | ||
|
da24642783 | ||
|
86b445c26a | ||
|
4e4d2de54a | ||
|
7770b378f3 | ||
|
0aa75c0422 | ||
|
87d65d3b1c | ||
|
f8467ceb17 | ||
|
babdb5d5cc | ||
|
8a170eeebd | ||
|
a19f895cd9 | ||
|
39aa7aa2de | ||
|
0e9e2e1dc0 | ||
|
237c6d227e | ||
|
2724169eb6 | ||
|
4031fafc1f | ||
|
456c318ac6 | ||
|
b5477e8996 | ||
|
59bec2d923 | ||
|
4c61c6ff3c | ||
|
123fd4f96c | ||
|
b2c61f0306 | ||
|
9cc7ee5c94 | ||
|
502a4d2b85 | ||
|
c1afb9dc50 | ||
|
d886e50fd3 | ||
|
4aae6a5297 | ||
|
b5551d674a | ||
|
6114b9f9bd | ||
|
ea02b23721 | ||
|
6ff86e7777 | ||
|
7bd1e47af9 | ||
|
62e4da053d | ||
|
0fae9932ac | ||
|
2400c02655 | ||
|
b3779a6313 | ||
|
99097b4632 | ||
|
1b5caa08be | ||
|
db747c4a03 | ||
|
8b6de484b8 | ||
|
697c1b6106 | ||
|
41349555bf | ||
|
dafb6fa06f | ||
|
840e7f99fc | ||
|
a24f963121 | ||
|
f59f34884c | ||
|
b4f3dd2f7a | ||
|
726abe4208 | ||
|
b43c925696 | ||
|
4955f1af89 | ||
|
f1b86ccb1d | ||
|
affcc26b3d | ||
|
03c42aa8cc | ||
|
c260fe26ae | ||
|
0ba19d5479 | ||
|
b30c0359f6 | ||
|
57b6d8a058 | ||
|
6a771eae42 | ||
|
c624f325c7 | ||
|
c65451b34c | ||
|
2372dc9bb7 | ||
|
d16a9d58a8 | ||
|
2371ba8448 | ||
|
4d8469e2d1 | ||
|
5023f37125 | ||
|
5534294163 | ||
|
5c49d24504 | ||
|
87143172e8 | ||
|
f6bab0cc9d | ||
|
1335d47ae8 | ||
|
bac1632457 | ||
|
fbf4cc430d | ||
|
55f7638531 | ||
|
36fe1dad0b | ||
|
ff4c8cc21c | ||
|
0cf09baef8 | ||
|
257bbfc527 | ||
|
748cfa2c62 | ||
|
d778560dbd | ||
|
a748166399 | ||
|
a176dc443e | ||
|
3346bcdfad | ||
|
1bac3b78d7 | ||
|
8081e19ebc | ||
|
96e4aff5c8 | ||
|
377db8298b | ||
|
a8a77ea5d7 | ||
|
e918a2c0f5 | ||
|
1ea4817f6a | ||
|
ecbb96f3a5 | ||
|
296a2d2f0e | ||
|
8c1ef4b0fd | ||
|
04d3330463 | ||
|
9e115daeb9 | ||
|
3eaf05bd4d | ||
|
a195980547 | ||
|
f04a929856 | ||
|
5b8af29496 | ||
|
766369f911 | ||
|
f6c6dbd312 | ||
|
783648f516 | ||
|
ead48f4502 | ||
|
305d95672a | ||
|
8a792c7d63 | ||
|
93ed5ad085 | ||
|
41f23386b2 | ||
|
c91644b829 | ||
|
073fd16bd7 | ||
|
f92ee770e0 | ||
|
1e6d52357e | ||
|
751ded44f3 | ||
|
8567f3e34e | ||
|
83f2514403 | ||
|
ad6ac7222c | ||
|
3ae1952624 | ||
|
3db549af40 | ||
|
94179ae552 | ||
|
7f35e87ed8 | ||
|
00da0009ef | ||
|
cffc30afa3 | ||
|
24cf1d9284 | ||
|
9296e6987d | ||
|
809fa85706 | ||
|
b3ae7605d3 | ||
|
864ec4737d | ||
|
854d0bcf20 | ||
|
458e387b68 | ||
|
56c770c49d | ||
|
946447394d | ||
|
44ba3273cb | ||
|
0f7b9d5e2b | ||
|
8a3f66db7b | ||
|
0eaa327d47 | ||
|
08e0cf5ad5 | ||
|
d7aea9d11c | ||
|
135ce77288 | ||
|
19141a73d2 | ||
|
9d1051b0bd | ||
|
c46a5920e5 | ||
|
43ac23f113 | ||
|
13f6cd8ef4 | ||
|
0e6d289128 | ||
|
bba68bff29 | ||
|
6e0cce4d49 | ||
|
d3ebe95076 | ||
|
cbda4a38a3 | ||
|
3318041b92 | ||
|
af53ec7625 | ||
|
de2829fde7 | ||
|
c1bee4046c | ||
|
473095b01b | ||
|
e6abf93457 | ||
|
882f281482 | ||
|
0b2f68ac04 | ||
|
2ca2b766f8 | ||
|
da611fb10b | ||
|
eb8e49e23c | ||
|
0907d38c06 | ||
|
2a9b725c6a | ||
|
8f24a94ed3 | ||
|
4eefc95baa | ||
|
1681c34a52 | ||
|
47ab0184b7 | ||
|
58591f660a | ||
|
3c7e1cf442 | ||
|
055d4cce33 | ||
|
a3dfe61a7b | ||
|
f9d47c081f | ||
|
ff5bf62989 | ||
|
1f6d079644 | ||
|
5c085a1986 | ||
|
9a23817473 | ||
|
56ea8937f6 | ||
|
4f51263501 | ||
|
bb2eab60f4 | ||
|
44e4c04811 | ||
|
b5839eab26 | ||
|
780ac75bf6 | ||
|
a252138594 | ||
|
270a055072 | ||
|
08e194efe9 | ||
|
5f6caab338 | ||
|
5aaa318142 | ||
|
cebbef680f | ||
|
0abde46ef4 | ||
|
f2b518ed26 | ||
|
c6207f35e1 | ||
|
ee8fa04814 | ||
|
7b746fa053 | ||
|
b7fea53107 | ||
|
f89f3e6a38 | ||
|
a0da2f6e16 | ||
|
3b5380e0d1 | ||
|
35276bfe41 | ||
|
215c1ecbd9 | ||
|
1698b21d7a | ||
|
ca1e66be47 | ||
|
22bf2823e8 | ||
|
32e98f1b3a | ||
|
c1c4335ce7 | ||
|
6c50662280 | ||
|
f3a1707b94 | ||
|
6ea755f2a8 | ||
|
a989b44a15 | ||
|
40f8587fd6 | ||
|
9f5638f16d | ||
|
9b19f96ff6 | ||
|
15da557892 | ||
|
2d23c9a2e6 | ||
|
ab49afd3db | ||
|
86a370fd69 | ||
|
36fc74ce07 | ||
|
8da8843fd0 | ||
|
f68285fbe5 | ||
|
85b8ef8f88 | ||
|
3d48aa8bbe | ||
|
a765da6e28 | ||
|
c57640acd0 | ||
|
c264216053 | ||
|
9d3c732993 | ||
|
34f023c4b1 | ||
|
5a4f842774 | ||
|
bae1767141 | ||
|
5957833a4f | ||
|
397092c21f | ||
|
f309003e67 | ||
|
eaf3678758 | ||
|
f2e82da7c8 | ||
|
211ae1f905 | ||
|
db629593c6 | ||
|
80d58cce2b | ||
|
a0f55aca69 | ||
|
d203a3586c | ||
|
f74a6424d0 | ||
|
209ef3d890 | ||
|
b549b32cbb | ||
|
54e2cb51cf | ||
|
42e6de395f | ||
|
c0066b22b0 | ||
|
aaef0bec27 | ||
|
912c3531c5 | ||
|
488e6e3204 | ||
|
f73e3f648d | ||
|
36e5feac98 | ||
|
cc13fcc8aa | ||
|
7f748f2a61 | ||
|
4a6fec8af0 | ||
|
e2b320ad27 | ||
|
21d5214247 | ||
|
44b544745d | ||
|
5499a559c8 | ||
|
ebe2c56348 | ||
|
8debfe7e95 | ||
|
1ef1b6bda9 | ||
|
fb46d7ec7c | ||
|
cea600f12c | ||
|
bf2f617255 | ||
|
cf8c7cb258 | ||
|
4e87f0b665 | ||
|
d8baba586b | ||
|
3cff2eb4ce | ||
|
f355a698ad | ||
|
c3d0b74c75 | ||
|
b0f98e4bfa | ||
|
506ffb8adf | ||
|
e1afc10b80 | ||
|
960b436c79 | ||
|
6059891556 | ||
|
3503dff663 | ||
|
61998886ac | ||
|
608d7fb34d | ||
|
6bb6d9f71e | ||
|
918894147a | ||
|
0b5afda287 | ||
|
a8a6ed97b9 | ||
|
919376b77c | ||
|
7e505f9b96 | ||
|
de517be613 | ||
|
bd5dd2cf28 | ||
|
da521b35e6 | ||
|
34ffd9c1f3 | ||
|
3131e00f0f | ||
|
f71812d622 | ||
|
0c12665fda | ||
|
458b7adb29 | ||
|
264da00e5d | ||
|
0882c25034 | ||
|
a3562d9212 | ||
|
b08c389e4a | ||
|
91bc1519f1 | ||
|
58b27c9693 | ||
|
25daa23606 | ||
|
9110d87580 | ||
|
c097b5681d | ||
|
beb705f8a9 | ||
|
fd9488673c | ||
|
70aeefea02 | ||
|
1b3d2a6168 | ||
|
fcb7ad965d | ||
|
828a2f5b60 | ||
|
be6d431485 | ||
|
61a43b8efd | ||
|
e3a9c77fd1 | ||
|
e772ff05fb | ||
|
d064f6285a | ||
|
091ca1a4fe | ||
|
9cec6a31a5 | ||
|
bb8af263e1 | ||
|
7211a17a81 | ||
|
810398abb8 | ||
|
f2b580fc06 | ||
|
3bd8858121 | ||
|
5efd1dbec4 | ||
|
971915948b | ||
|
a9a37036d5 | ||
|
7a58035514 | ||
|
57bfe27819 | ||
|
fcc65c3751 | ||
|
f2d8dfc3ef | ||
|
b5c570adf5 | ||
|
94098d02e8 | ||
|
436b15f010 | ||
|
0282feb173 | ||
|
8456750901 | ||
|
1a02cab97c | ||
|
1bdd81a1d8 | ||
|
a9d58f88aa | ||
|
6a344ff2c7 |
@@ -4,7 +4,7 @@ root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
|
46
.env
46
.env
@@ -1,20 +1,42 @@
|
||||
VITE_BASE_URL=/
|
||||
|
||||
VITE_APP_NAME=SoybeanAdmin
|
||||
VITE_APP_TITLE=SoybeanAdmin
|
||||
|
||||
VITE_APP_TITLE=Soybean管理系统
|
||||
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
|
||||
|
||||
VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
||||
# the prefix of the icon name
|
||||
VITE_ICON_PREFIX=icon
|
||||
|
||||
# 权限路由模式: static | dynamic
|
||||
VITE_AUTH_ROUTE_MODE=dynamic
|
||||
# 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
|
||||
|
||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||
# auth route mode: static | dynamic
|
||||
VITE_AUTH_ROUTE_MODE=static
|
||||
|
||||
# iconify图标作为组件的前缀
|
||||
VITE_ICON_PREFFIX=icon
|
||||
# static auth route home
|
||||
VITE_ROUTE_HOME=home
|
||||
|
||||
# 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFFIX
|
||||
# 格式 {VITE_ICON_PREFFIX}-{本地图标集合名称}
|
||||
VITE_ICON_LOCAL_PREFFIX=icon-local
|
||||
# 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=0000
|
||||
|
||||
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
|
||||
VITE_SERVICE_LOGOUT_CODES=8888,8889
|
||||
|
||||
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
|
||||
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
||||
|
||||
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
|
||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998
|
||||
|
||||
# when the route mode is static, the defined super role
|
||||
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||
|
@@ -1,36 +0,0 @@
|
||||
/** 请求服务的环境配置 */
|
||||
type ServiceEnv = Record<ServiceEnvType, ServiceEnvConfig>;
|
||||
|
||||
/** 不同请求服务的环境配置 */
|
||||
const serviceEnv: ServiceEnv = {
|
||||
dev: {
|
||||
url: 'http://localhost:8080',
|
||||
urlPattern: '/url-pattern',
|
||||
secondUrl: 'http://localhost:8081',
|
||||
secondUrlPattern: '/second-url-pattern'
|
||||
},
|
||||
test: {
|
||||
url: 'http://localhost:8080',
|
||||
urlPattern: '/url-pattern',
|
||||
secondUrl: 'http://localhost:8081',
|
||||
secondUrlPattern: '/second-url-pattern'
|
||||
},
|
||||
prod: {
|
||||
url: 'http://localhost:8080',
|
||||
urlPattern: '/url-pattern',
|
||||
secondUrl: 'http://localhost:8081',
|
||||
secondUrlPattern: '/second-url-pattern'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境模式下的请求服务的配置
|
||||
* @param env 环境
|
||||
*/
|
||||
export function getServiceEnvConfig(env: ImportMetaEnv) {
|
||||
const { VITE_SERVICE_ENV = 'dev' } = env;
|
||||
|
||||
const config = serviceEnv[VITE_SERVICE_ENV];
|
||||
|
||||
return config;
|
||||
}
|
@@ -1 +0,0 @@
|
||||
VITE_HTTP_PROXY=Y
|
7
.env.prod
Normal file
7
.env.prod
Normal file
@@ -0,0 +1,7 @@
|
||||
# backend service base url, prod environment
|
||||
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
|
||||
|
||||
# other backend service base url, prod environment
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
"demo": "http://localhost:9529"
|
||||
}`
|
@@ -1,6 +0,0 @@
|
||||
VITE_VISUALIZER=N
|
||||
|
||||
VITE_COMPRESS=N
|
||||
|
||||
# gzip | brotliCompress | deflate | deflateRaw
|
||||
VITE_COMPRESS_TYPE=gzip
|
7
.env.test
Normal file
7
.env.test
Normal file
@@ -0,0 +1,7 @@
|
||||
# backend service base url, test environment
|
||||
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
|
||||
|
||||
# other backend service base url, test environment
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
"demo": "http://localhost:9528"
|
||||
}`
|
@@ -1,3 +0,0 @@
|
||||
!.env-config.ts
|
||||
components.d.ts
|
||||
router-page.d.ts
|
126
.eslintrc.js
126
.eslintrc.js
@@ -1,126 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['soybeanjs-vue'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['./scripts/*.ts'],
|
||||
rules: {
|
||||
'no-unused-expressions': 'off'
|
||||
}
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
'import/core-modules': ['uno.css', '~icons/*', 'virtual:svg-icons-register']
|
||||
},
|
||||
rules: {
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'never',
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: 'vue',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'vue-router',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'pinia',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: 'naive-ui',
|
||||
group: 'external',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/config',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/settings',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/enum',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/plugins',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/layouts',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/views',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/components',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/router',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/service',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/store',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/context',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/composables',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/hooks',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/utils',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/assets',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/**',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
}
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'pinia', 'naive-ui']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -3,14 +3,11 @@
|
||||
"*.ts" eol=lf
|
||||
"*.jsx" eol=lf
|
||||
"*.tsx" eol=lf
|
||||
"*.cjs" eol=lf
|
||||
"*.cts" eol=lf
|
||||
"*.mjs" eol=lf
|
||||
"*.mts" eol=lf
|
||||
"*.json" eol=lf
|
||||
"*.html" eol=lf
|
||||
"*.css" eol=lf
|
||||
"*.less" eol=lf
|
||||
"*.scss" eol=lf
|
||||
"*.sass" eol=lf
|
||||
"*.styl" eol=lf
|
||||
"*.md" eol=lf
|
||||
"*.yaml" eol=lf
|
||||
"*.yml" eol=lf
|
||||
|
90
.github/ISSUE_TEMPLATE/bug-report_cn.yaml
vendored
Normal file
90
.github/ISSUE_TEMPLATE/bug-report_cn.yaml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Bug提交
|
||||
description: 在使用软件或功能的过程中遇到了错误
|
||||
title: '[Bug]: '
|
||||
labels: [ "bug?" ]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 请按照以下要求进行提交
|
||||
### 1. 提交后需要指定标签和截止时间。
|
||||
---
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 环境信息
|
||||
请根据实际使用环境修改以下信息。
|
||||
|
||||
- type: input
|
||||
id: env-program-ver
|
||||
attributes:
|
||||
label: 软件版本
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-ver
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: 选择运行软件的系统版本
|
||||
options:
|
||||
- Windows (64)
|
||||
- Windows (32/x84)
|
||||
- MacOS
|
||||
- Linux
|
||||
- Ubuntu
|
||||
- CentOS
|
||||
- ArchLinux
|
||||
- UNIX (Android)
|
||||
- 其它(请在下方说明)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-arch
|
||||
attributes:
|
||||
label: 运行架构
|
||||
description: (可选) 选择运行软件的系统架构
|
||||
options:
|
||||
- AMD64
|
||||
- x86
|
||||
- ARM [32] (别名:AArch32 / ARMv7)
|
||||
- ARM [64] (别名:AArch64 / ARMv8)
|
||||
- 其它
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: |
|
||||
我们需要执行哪些操作才能让 bug 出现?
|
||||
简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望的结果是什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际的结果是什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logging
|
||||
attributes:
|
||||
label: 日志记录(可选)
|
||||
render: golang
|
||||
|
||||
- type: textarea
|
||||
id: extra-desc
|
||||
attributes:
|
||||
label: 补充说明(可选)
|
90
.github/ISSUE_TEMPLATE/bug-report_en.yaml
vendored
Normal file
90
.github/ISSUE_TEMPLATE/bug-report_en.yaml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Bug Report
|
||||
description: Encountered an error while using the software or feature
|
||||
title: '[Bug]: '
|
||||
labels: [ "bug?" ]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Please submit according to the following requirements
|
||||
### 1. After submission, you need to specify the label and deadline.
|
||||
---
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment Information
|
||||
Please modify the following information according to the actual usage environment.
|
||||
|
||||
- type: input
|
||||
id: env-program-ver
|
||||
attributes:
|
||||
label: Software Version
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-ver
|
||||
attributes:
|
||||
label: Operating Environment
|
||||
description: Select the system version on which the software is running
|
||||
options:
|
||||
- Windows (64)
|
||||
- Windows (32/x84)
|
||||
- MacOS
|
||||
- Linux
|
||||
- Ubuntu
|
||||
- CentOS
|
||||
- ArchLinux
|
||||
- UNIX (Android)
|
||||
- Other (please specify below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: env-vm-arch
|
||||
attributes:
|
||||
label: Operating Architecture
|
||||
description: (Optional) Select the system architecture on which the software is running
|
||||
options:
|
||||
- AMD64
|
||||
- x86
|
||||
- ARM [32] (Alias:AArch32 / ARMv7)
|
||||
- ARM [64] (Alias:AArch64 / ARMv8)
|
||||
- Other
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Reproduce Steps
|
||||
description: |
|
||||
What operations do we need to perform to make the bug appear?
|
||||
The concise and clear reproduction steps can help us locate the problem more quickly.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected result?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What is the actual result?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logging
|
||||
attributes:
|
||||
label: Logging (Optional)
|
||||
render: golang
|
||||
|
||||
- type: textarea
|
||||
id: extra-desc
|
||||
attributes:
|
||||
label: Additional Description (Optional)
|
50
.github/PULL_REQUEST_TEMPLATE/pr_cn.md
vendored
Normal file
50
.github/PULL_REQUEST_TEMPLATE/pr_cn.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
首先,感谢你的贡献! 😄
|
||||
|
||||
新特性请提交至 feature 分支,其余可提交至 main 分支。在一个维护者审核通过后合并。请确保填写以下 pull request 的信息,谢谢!~
|
||||
|
||||
[[English Template / 英文模板](./pr_en.md)]
|
||||
|
||||
### 这个变动的性质是
|
||||
|
||||
- [ ] 新特性提交
|
||||
- [ ] 日常 bug 修复
|
||||
- [ ] 站点、文档改进
|
||||
- [ ] 组件样式改进
|
||||
- [ ] TypeScript 定义更新
|
||||
- [ ] 重构
|
||||
- [ ] 代码风格优化
|
||||
- [ ] 分支合并
|
||||
- [ ] 其他改动(是关于什么的改动?)
|
||||
|
||||
### 需求背景
|
||||
|
||||
> 1. 描述相关需求的来源。
|
||||
> 2. 要解决的问题。
|
||||
> 3. 相关的 issue 讨论链接。
|
||||
|
||||
### 实现方案和 API(非新功能可选)
|
||||
|
||||
> 1. 基本的解决思路和其他可选方案。
|
||||
> 2. 列出最终的 API 实现和用法。
|
||||
> 3. 涉及 UI/交互变动需要有截图或 GIF。
|
||||
|
||||
### 对用户的影响和可能的风险(非新功能可选)
|
||||
|
||||
> 1. 这个改动对用户端是否有影响?影响的方面有哪些?
|
||||
> 2. 是否有可能隐含的 break change 和其他风险?
|
||||
|
||||
### Changelog 描述(非新功能可选)
|
||||
|
||||
> 1. 英文描述
|
||||
> 2. 中文描述(可选)
|
||||
|
||||
### 请求合并前的自查清单
|
||||
|
||||
- [ ] 文档已补充或无须补充
|
||||
- [ ] 代码演示已提供或无须提供
|
||||
- [ ] TypeScript 定义已补充或无须补充
|
||||
- [ ] Changelog 已提供或无须提供
|
||||
|
||||
### 后续计划(非新功能可选)
|
||||
|
||||
> 如果这个提交后面还有相关的其他提交和跟进信息,可以写在这里。
|
51
.github/PULL_REQUEST_TEMPLATE/pr_en.md
vendored
Normal file
51
.github/PULL_REQUEST_TEMPLATE/pr_en.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
First of all, thank you for your contribution! 😄
|
||||
|
||||
New feature please send pull request to feature branch, and rest to main branch. Pull request will be merged after one of collaborators approve. Please makes sure that these form are filled before submitting your pull request, thank you!
|
||||
|
||||
[[中文版模板 / Chinese template](./pr_cn.md)]
|
||||
|
||||
### This is a ...
|
||||
|
||||
- [ ] New feature
|
||||
- [ ] Bug fix
|
||||
- [ ] Site / document update
|
||||
- [ ] Component style update
|
||||
- [ ] TypeScript definition update
|
||||
- [ ] Refactoring
|
||||
- [ ] Code style optimization
|
||||
- [ ] Branch merge
|
||||
- [ ] Other (about what?)
|
||||
|
||||
### What's the background?
|
||||
|
||||
> 1. Describe the source of requirement.
|
||||
> 2. Resolve what problem.
|
||||
> 3. Related issue link.
|
||||
|
||||
### API Realization (Optional if not new feature)
|
||||
|
||||
> 1. Basic thought of solution and other optional proposal.
|
||||
> 2. List final API realization and usage sample.
|
||||
> 3. GIF or snapshot should be provided if includes UI/interactive modification.
|
||||
|
||||
### What's the effect? (Optional if not new feature)
|
||||
|
||||
> 1. Does this PR affect user? Which part will be affected?
|
||||
> 2. What will say in changelog?
|
||||
> 3. Does this PR contains potential break change or other risk?
|
||||
|
||||
### Changelog description (Optional if not new feature)
|
||||
|
||||
> 1. English description
|
||||
> 2. Chinese description (optional)
|
||||
|
||||
### Self Check before Merge
|
||||
|
||||
- [ ] Doc is updated/provided or not needed
|
||||
- [ ] Demo is updated/provided or not needed
|
||||
- [ ] TypeScript definition is updated/provided or not needed
|
||||
- [ ] Changelog is provided or not needed
|
||||
|
||||
### Additional Plan? (Optional if not new feature)
|
||||
|
||||
> If this PR related with other PR or following info. You can type here.
|
30
.github/workflows/linter.yml
vendored
Normal file
30
.github/workflows/linter.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Lint Code
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint All Code
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Lint Code Base
|
||||
uses: github/super-linter@v4
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
# To change branch master or main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FILTER_REGEX_EXCLUDE: (docs|.github)
|
||||
VALIDATE_MARKDOWN: false
|
25
.github/workflows/release.yml
vendored
Normal file
25
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- run: npx githublogen
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -13,7 +13,6 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
stats.html
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -22,6 +21,7 @@ stats.html
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/launch.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
@@ -29,7 +29,7 @@ stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/src/typings/components.d.ts
|
||||
/src/typings/router-page.d.ts
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.VSCodeCounter
|
||||
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm soybean git-commit-verify
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint && pnpm typecheck
|
3
.npmrc
3
.npmrc
@@ -1,4 +1,3 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
ignore-workspace-root-check=true
|
||||
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -3,21 +3,18 @@
|
||||
"afzalsayed96.icones",
|
||||
"antfu.iconify",
|
||||
"antfu.unocss",
|
||||
"christian-kohler.path-intellisense",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"formulahendry.auto-complete-tag",
|
||||
"formulahendry.auto-close-tag",
|
||||
"formulahendry.auto-complete-tag",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"lokalise.i18n-ally",
|
||||
"mhutchie.git-graph",
|
||||
"mikestead.dotenv",
|
||||
"naumovs.color-highlight",
|
||||
"pkief.material-icon-theme",
|
||||
"steoates.autoimport",
|
||||
"sdras.vue-vscode-snippets",
|
||||
"vue.volar",
|
||||
"whtouche.vscode-js-console-utils",
|
||||
"zhuangtongfa.material-theme"
|
||||
|
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal 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}"
|
||||
}
|
||||
]
|
||||
}
|
99
.vscode/settings.json
vendored
99
.vscode/settings.json
vendored
@@ -1,90 +1,19 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.fontLigatures": true,
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"json",
|
||||
"jsonc",
|
||||
"json5",
|
||||
"yaml",
|
||||
"yml",
|
||||
"markdown"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"git.enableSmartCommit": true,
|
||||
"gutterpreview.paths": {
|
||||
"@": "/src",
|
||||
"~@": "/src"
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/locales", "src/locales/lang"],
|
||||
"material-icon-theme.activeIconPack": "angular",
|
||||
"material-icon-theme.files.associations": {},
|
||||
"material-icon-theme.folders.associations": {
|
||||
"src-tauri": "src",
|
||||
"enum": "typescript",
|
||||
"enums": "typescript",
|
||||
"store": "context",
|
||||
"stores": "context",
|
||||
"composable": "hook",
|
||||
"composables": "hook",
|
||||
"directive": "tools",
|
||||
"directives": "tools",
|
||||
"business": "core",
|
||||
"request": "api",
|
||||
"adapter": "middleware"
|
||||
},
|
||||
"path-intellisense.mappings": {
|
||||
"@": "${workspaceFolder}/src",
|
||||
"~@": "${workspaceFolder}/src"
|
||||
},
|
||||
"terminal.integrated.cursorStyle": "line",
|
||||
"terminal.integrated.fontSize": 14,
|
||||
"terminal.integrated.fontWeight": 500,
|
||||
"terminal.integrated.tabs.enabled": true,
|
||||
"workbench.iconTheme": "material-icon-theme",
|
||||
"workbench.colorTheme": "One Dark Pro",
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
}
|
||||
"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": ["./"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"vue.server.hybridMode": true
|
||||
}
|
||||
|
2027
CHANGELOG.md
2027
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
16
Makefile
16
Makefile
@@ -1,16 +0,0 @@
|
||||
ImageTag ?=v0.9.6
|
||||
SoybeanAdminImg ?= soybeanjs/soybean-admin:$(ImageTag)
|
||||
|
||||
VERSION=$(shell git rev-parse --short HEAD)
|
||||
|
||||
soybean-admin: soybean-admin-build soybean-admin-push
|
||||
|
||||
soybean-admin-build:
|
||||
docker build --build-arg version=$(VERSION) -t ${SoybeanAdminImg} -f docker/Dockerfile .
|
||||
|
||||
soybean-admin-push:
|
||||
docker push ${SoybeanAdminImg}
|
||||
|
||||
# run tauri app:
|
||||
run:
|
||||
pnpm tauri dev
|
191
README.md
191
README.md
@@ -1,160 +1,165 @@
|
||||
<div align="center">
|
||||
<img src="https://i.loli.net/2021/11/24/x5lLfuSnEawBAgi.png"/>
|
||||
<img src="./public/favicon.svg" width="160" />
|
||||
<h1>Soybean Admin</h1>
|
||||
<span>English | <a href="./README.zh_CN.md">中文</a></span>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
[](./LICENSE)
|
||||
[](./LICENSE)  
|
||||
|
||||
## 简介
|
||||
|
||||
[Soybean Admin](https://github.com/honghuangdc/soybean-admin) 是一个基于 Vue3、Vite3、TypeScript、NaiveUI、Pinia 和 UnoCSS 的清新优雅的中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于 mock 实现的动态权限路由,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
>[!CAUTION]
|
||||
> the old version of `Soybean Admin` is moved to branch [legacy](https://github.com/soybeanjs/soybean-admin/tree/legacy). It is recommended to use the latest version of `Soybean Admin`.
|
||||
|
||||
## 特性
|
||||
> [!NOTE]
|
||||
> If you think `Soybean Admin` is helpful to you, or you like our project, please give us a ⭐️ on GitHub. Your support is the driving force for us to continue to improve and add new features! Thank you for your support!
|
||||
|
||||
- **最新技术栈**:使用 Vue3/Vite3 等前端前沿技术开发, 使用高效率的 npm 包管理器 pnpm
|
||||
- **TypeScript**:应用程序级 JavaScript 的语言
|
||||
- **主题**:丰富可配置的主题、暗黑模式,基于原子 css 框架 - UnoCSS 的动态主题颜色
|
||||
- **代码规范**:丰富的规范插件及极高的代码规范
|
||||
- **权限路由**:简易的路由配置、基于 mock 的动态路由能快速实现后端动态路由
|
||||
- **请求函数**:基于 axios 的完善的请求函数封装,提供 Promise 和 hooks 两种请求函数,加入请求结果数据转换的适配器
|
||||
## Introduction
|
||||
|
||||
## 预览
|
||||
[`Soybean Admin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite5, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `Soybean Admin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
||||
|
||||
- [soybean-admin](https://soybean.pro/)
|
||||
|
||||
## 文档
|
||||
## Features
|
||||
|
||||
- [项目文档: docs.soybean.pro](https://docs.soybean.pro)
|
||||
- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite5, TypeScript, Pinia and UnoCSS.
|
||||
- **Clear project architecture**: using pnpm monorepo architecture, clear structure, elegant and easy to understand.
|
||||
- **Strict code specifications**: follow the [SoybeanJS specification](https://docs.soybeanjs.cn/standard), integrate eslint, prettier and simple-git-hooks to ensure the code is standardized.
|
||||
- **TypeScript**: support strict type checking to improve code maintainability.
|
||||
- **Rich theme configuration**: built-in a variety of theme configurations, perfectly integrated with UnoCSS.
|
||||
- **Built-in internationalization solution**: easily realize multi-language support.
|
||||
- **Automated file routing system**: automatically generate route import, declaration and type. For more details, please refer to [Elegant Router](https://github.com/soybeanjs/elegant-router).
|
||||
- **Flexible permission routing**: support both front-end static routing and back-end dynamic routing.
|
||||
- **Rich page components**: built-in a variety of pages and components, including 403, 404, 500 pages, as well as layout components, tag components, theme configuration components, etc.
|
||||
- **Command line tool**: built-in efficient command line tool, git commit, delete file, release, etc.
|
||||
- **Mobile adaptation**: perfectly support mobile terminal to realize adaptive layout.
|
||||
|
||||
## 代码仓库
|
||||
|
||||
- [github](https://github.com/honghuangdc/soybean-admin)
|
||||
- [gitee](https://gitee.com/honghuangdc/soybean-admin)
|
||||
## Version
|
||||
|
||||
## 更新日志
|
||||
- **NaiveUI Version:**
|
||||
- [Preview Link](https://naive.soybeanjs.cn/)
|
||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin)
|
||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin)
|
||||
|
||||
[CHANGELOG](./CHANGELOG.md)
|
||||
- **AntDesignVue Version:**
|
||||
- [Preview Link](https://antd.soybeanjs.cn/)
|
||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin-antd)
|
||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||
|
||||
## 后端服务
|
||||
- **Legacy Version:**
|
||||
- [Preview Link](https://legacy.soybeanjs.cn/)
|
||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
||||
|
||||
- [soybean-admin-java(开发中)](https://github.com/honghuangdc/soybean-admin-java)
|
||||
- [soybean-admin-go(开发中)](https://github.com/honghuangdc/soybean-admin-go)
|
||||
- [soybean-admin-nestjs(开发中)](https://github.com/honghuangdc/soybean-admin-nestjs)
|
||||
|
||||
## 项目示例图
|
||||
## Documentation
|
||||
|
||||

|
||||
- [Link](https://docs.soybeanjs.cn)
|
||||
- [Legacy Docs](https://legacy-docs.soybeanjs.cn)
|
||||
|
||||

|
||||
## Example Images
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
## Usage
|
||||
|
||||

|
||||
**Environment Preparation**
|
||||
|
||||

|
||||
Make sure your environment meets the following requirements:
|
||||
|
||||

|
||||
- **git**: you need git to clone and manage project versions.
|
||||
- **NodeJS**: >=18.0.0, recommended 18.19.0 or higher.
|
||||
> You can use [fnm](https://github.com/Schniz/fnm) to manage your NodeJS version, [installation tutorial](https://juejin.cn/post/7113462239734022158).
|
||||
- **pnpm**: >= 8.0.0, recommended 8.14.0 or higher.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 安装使用
|
||||
|
||||
- 环境配置
|
||||
**本地环境需要安装 pnpm 7.x 、Node.js 14.18+ 和 Git**
|
||||
|
||||
- 克隆代码
|
||||
**Clone Project**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/honghuangdc/soybean-admin.git
|
||||
git clone https://github.com/soybeanjs/soybean-admin.git
|
||||
```
|
||||
|
||||
- 安装依赖
|
||||
**Install Dependencies**
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
> Since this project uses the pnpm monorepo management method, please do not use npm or yarn to install dependencies.
|
||||
|
||||
- 运行
|
||||
**Start Project**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- 打包
|
||||
**Build Project**
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Docker 部署
|
||||
## How to Contribute
|
||||
|
||||
- Docker 部署 Soybean
|
||||
We warmly welcome and appreciate all forms of contributions. If you have any ideas or suggestions, please feel free to share them by submitting [pull requests](https://github.com/soybeanjs/soybean-admin/pulls) or creating GitHub [issue](https://github.com/soybeanjs/soybean-admin/issues/new).
|
||||
|
||||
```bash
|
||||
docker run --name soybean -p 80:80 -d soybeanjs/soybean-admin:v0.9.6
|
||||
```
|
||||
## Git Commit Guidelines
|
||||
|
||||
- 访问 SoybeanAdmin
|
||||
This project has built-in `commit` command, you can execute `pnpm commit` to generate commit information that conforms to [Conventional Commits](conventionalcommits) specification. When submitting PR, please be sure to use `commit` command to create commit information to ensure the standardization of information.
|
||||
|
||||
打开本地浏览器访问`http://localhost`
|
||||
## Browser Support
|
||||
|
||||
## 如何贡献
|
||||
It is recommended to use the latest version of Chrome in development for a better experience.
|
||||
|
||||
非常欢迎您的加入 或者提交一个 Pull Request。
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## Git 贡献提交规范
|
||||
## OpenSource Author
|
||||
|
||||
项目已经内置 angular 提交规范,通过 git cz 代替 git commit 命令即可。
|
||||
[Soybean](https://github.com/honghuangdc)
|
||||
|
||||
git cz 命令需要全局安装 commitizen
|
||||
## Contributors
|
||||
|
||||
```bash
|
||||
pnpm i -g commitizen
|
||||
```
|
||||
Thanks the following people for their contributions. If you want to contribute to this project, please refer to [How to Contribute](#how-to-contribute).
|
||||
|
||||
## 浏览器支持
|
||||
<a href="https://github.com/soybeanjs/soybean-admin/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
|
||||
</a>
|
||||
|
||||
本地开发推荐使用`Chrome 90+` 浏览器
|
||||
## Communication
|
||||
|
||||
支持现代浏览器, 不支持 IE
|
||||
`Soybean Admin` is a completely open source and free project, helping developers to develop medium and large-scale management systems more conveniently. It also provides WeChat and QQ communication groups. If you have any questions, please feel free to ask in the group.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## 开源作者
|
||||
|
||||
[@Soybean](https://github.com/honghuangdc)
|
||||
|
||||
## 交流
|
||||
|
||||
`Soybean Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和 QQ 交流群,使用问题欢迎在群内提问。
|
||||
|
||||
- 微信交流群(添加本人微信拉进群),欢迎来技术交流,业务咨询。
|
||||
<div style="text-align:left">
|
||||
<img src="https://s2.loli.net/2022/05/16/3YGBgXnVPJdslk8.jpg" style="width:200px" />
|
||||
<div>
|
||||
<p>QQ Group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-2.jpg" style="width:200px" />
|
||||
</div>
|
||||
<div>
|
||||
<p>WeChat Group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-0402.jpg" style="width:200px" />
|
||||
</div>
|
||||
<div>
|
||||
<p>Add the following WeChat to invite to the WeChat group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybeanjs.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>Add Soybean's WeChat for business consultation, cooperation, project architecture, one-on-one guidance, etc.</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybean.jpg" style="width:200px" /> -->
|
||||
</div>
|
||||
|
||||
- QQ 交流群 `711301266`
|
||||
## Star Trend
|
||||
|
||||
<div style="text-align:left">
|
||||
<img src="https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果你觉得这个项目对你有帮助,可以请 Soybean 喝杯饮料表示支持,Soybean 开源的动力离不开各位的支持和鼓励。
|
||||
|
||||

|
||||
[](https://star-history.com/#soybeanjs/soybean-admin&Date)
|
||||
|
||||
## License
|
||||
|
||||
[MIT © Soybean-2021](./LICENSE)
|
||||
This project is based on the [MIT © 2021 Soybean](./LICENSE) protocol, for learning purposes only, please retain the author's copyright information for commercial use, the author does not guarantee and is not responsible for the software.
|
||||
|
169
README.zh_CN.md
Normal file
169
README.zh_CN.md
Normal file
@@ -0,0 +1,169 @@
|
||||
<div align="center">
|
||||
<img src="./public/favicon.svg" width="160" />
|
||||
<h1>Soybean Admin</h1>
|
||||
<span><a href="./README.zh_CN.md">English</a> | 中文</span>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
[](./LICENSE)  
|
||||
|
||||
>[!CAUTION]
|
||||
> 旧版本的 `Soybean Admin` 已经移动到分支 [legacy](https://github.com/soybeanjs/soybean-admin/tree/legacy)。建议使用最新版本的 `Soybean Admin`。
|
||||
|
||||
> [!NOTE]
|
||||
> 如果您觉得 `Soybean Admin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!
|
||||
|
||||
<br />
|
||||
|
||||
[](https://star-history.com/#soybeanjs/soybean-admin&Date)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
[`Soybean Admin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`Soybean Admin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||
|
||||
## 特性
|
||||
|
||||
- **前沿技术应用**:采用 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
||||
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
|
||||
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
|
||||
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
|
||||
- **丰富的主题配置**:内置多样的主题配置,与 UnoCSS 完美结合。
|
||||
- **内置国际化方案**:轻松实现多语言支持。
|
||||
- **自动化文件路由系统**:自动生成路由导入、声明和类型。更多细节请查看 [Elegant Router](https://github.com/soybeanjs/elegant-router)。
|
||||
- **灵活的权限路由**:同时支持前端静态路由和后端动态路由。
|
||||
- **丰富的页面组件**:内置多样页面和组件,包括403、404、500页面,以及布局组件、标签组件、主题配置组件等。
|
||||
- **命令行工具**:内置高效的命令行工具,git提交、删除文件、发布等。
|
||||
- **移动端适配**:完美支持移动端,实现自适应布局。
|
||||
|
||||
|
||||
## 版本
|
||||
|
||||
- **NaiveUI 版本:**
|
||||
- [预览地址](https://naive.soybeanjs.cn/)
|
||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin)
|
||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin)
|
||||
|
||||
- **AntDesignVue 版本:**
|
||||
- [预览地址](https://antd.soybeanjs.cn/)
|
||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-antd)
|
||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||
|
||||
- **旧版:**
|
||||
- [预览地址](https://legacy.soybeanjs.cn/)
|
||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
||||
|
||||
|
||||
## 文档
|
||||
|
||||
- [地址](https://docs.soybeanjs.cn)
|
||||
- [旧版文档](https://legacy-docs.soybeanjs.cn)
|
||||
|
||||
## 示例图片
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
**环境准备**
|
||||
|
||||
确保你的环境满足以下要求:
|
||||
|
||||
- **git**: 你需要git来克隆和管理项目版本。
|
||||
- **NodeJS**: >=18.0.0,推荐 18.19.0 或更高。
|
||||
> 你可以使用 [fnm](https://github.com/Schniz/fnm) 来管理你的NodeJS版本,[安装教程](https://juejin.cn/post/7113462239734022158)。
|
||||
- **pnpm**: >= 8.0.0,推荐 8.14.0 或更高。
|
||||
|
||||
**克隆项目**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/soybeanjs/soybean-admin.git
|
||||
```
|
||||
|
||||
**安装依赖**
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
> 由于本项目采用了 pnpm monorepo 的管理方式,因此请不要使用 npm 或 yarn 来安装依赖。
|
||||
|
||||
**启动项目**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
**构建项目**
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 如何贡献
|
||||
|
||||
我们热烈欢迎并感谢所有形式的贡献。如果您有任何想法或建议,欢迎通过提交 [pull requests](https://github.com/soybeanjs/soybean-admin/pulls) 或创建 GitHub [issue](https://github.com/soybeanjs/soybean-admin/issues/new) 来分享。
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
本项目已内置 `commit` 命令,您可以通过执行 `pnpm commit` 来生成符合 [Conventional Commits](conventionalcommits) 规范的提交信息。在提交PR时,请务必使用 `commit` 命令来创建提交信息,以确保信息的规范性。
|
||||
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
推荐使用最新版的 Chrome 浏览器进行开发,以获得更好的体验。
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## 开源作者
|
||||
|
||||
[Soybean](https://github.com/honghuangdc)
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢以下贡献者的贡献。如果您想为本项目做出贡献,请参考 [如何贡献](#如何贡献)。
|
||||
|
||||
<a href="https://github.com/soybeanjs/soybean-admin/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
|
||||
</a>
|
||||
|
||||
## 交流
|
||||
|
||||
`Soybean Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和 QQ 交流群,使用问题欢迎在群内提问。
|
||||
|
||||
<div>
|
||||
<p>QQ交流群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-2.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>微信群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-0402.jpg" style="width:200px" />
|
||||
</div> -->
|
||||
<div>
|
||||
<p>添加下面微信邀请进微信群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybeanjs.jpg" style="width:200px" />
|
||||
</div>
|
||||
<div>
|
||||
<p>添加 Soybean 的微信,业务咨询、合作、项目架构、一对一指导等</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybean.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
## Star 趋势
|
||||
|
||||
[](https://star-history.com/#soybeanjs/soybean-admin&Date)
|
||||
|
||||
## 开源协议
|
||||
|
||||
项目基于 [MIT © 2021 Soybean](./LICENSE) 协议,仅供学习参考,商业使用请保留作者版权信息,作者不保证也不承担任何软件的使用风险。
|
@@ -1,8 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/** 项目构建时间 */
|
||||
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
export const viteDefine = {
|
||||
PROJECT_BUILD_TIME
|
||||
};
|
@@ -1,2 +1 @@
|
||||
export * from './define';
|
||||
export * from './proxy';
|
||||
|
@@ -1,24 +1,35 @@
|
||||
import type { ProxyOptions } from 'vite';
|
||||
import { createServiceConfig } from '../../src/utils/service';
|
||||
|
||||
/**
|
||||
* 设置网络代理
|
||||
* @param isOpenProxy - 是否开启代理
|
||||
* @param envConfig - env环境配置
|
||||
* Set http proxy
|
||||
*
|
||||
* @param env - The current env
|
||||
* @param isDev - Is development environment
|
||||
*/
|
||||
export function createViteProxy(isOpenProxy: boolean, envConfig: ServiceEnvConfig) {
|
||||
if (!isOpenProxy) return undefined;
|
||||
export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
|
||||
const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
|
||||
|
||||
const proxy: Record<string, string | ProxyOptions> = {
|
||||
[envConfig.urlPattern]: {
|
||||
target: envConfig.url,
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(new RegExp(`^${envConfig.urlPattern}`), '')
|
||||
},
|
||||
[envConfig.secondUrlPattern]: {
|
||||
target: envConfig.secondUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(new RegExp(`^${envConfig.secondUrlPattern}`), '')
|
||||
}
|
||||
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;
|
||||
|
@@ -1,3 +0,0 @@
|
||||
export * from './plugins';
|
||||
export * from './config';
|
||||
export * from './utils';
|
@@ -1,6 +0,0 @@
|
||||
import ViteCompression from 'vite-plugin-compression';
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv;
|
||||
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE });
|
||||
};
|
@@ -1,14 +0,0 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
|
||||
export default (viteEnv: ImportMetaEnv): PluginOption[] => {
|
||||
return createHtmlPlugin({
|
||||
minify: true,
|
||||
inject: {
|
||||
data: {
|
||||
appName: viteEnv.VITE_APP_NAME,
|
||||
appTitle: viteEnv.VITE_APP_TITLE
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,39 +1,26 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import unocss from '@unocss/vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||
import progress from 'vite-plugin-progress';
|
||||
import html from './html';
|
||||
import unplugin from './unplugin';
|
||||
import mock from './mock';
|
||||
import visualizer from './visualizer';
|
||||
import compress from './compress';
|
||||
import soybeanjs from './soybeanjs';
|
||||
import { setupElegantRouter } from './router';
|
||||
import { setupUnocss } from './unocss';
|
||||
import { setupUnplugin } from './unplugin';
|
||||
|
||||
/**
|
||||
* vite插件
|
||||
* @param viteEnv - 环境变量配置
|
||||
*/
|
||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [
|
||||
vue(),
|
||||
export function setupVitePlugins(viteEnv: Env.ImportMeta) {
|
||||
const plugins: PluginOption = [
|
||||
vue({
|
||||
script: {
|
||||
defineModel: true
|
||||
}
|
||||
}),
|
||||
vueJsx(),
|
||||
VitePWA(),
|
||||
html(viteEnv),
|
||||
...unplugin(viteEnv),
|
||||
unocss(),
|
||||
mock,
|
||||
progress(),
|
||||
soybeanjs()
|
||||
VueDevtools(),
|
||||
setupElegantRouter(),
|
||||
setupUnocss(viteEnv),
|
||||
...setupUnplugin(viteEnv),
|
||||
progress()
|
||||
];
|
||||
|
||||
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||
plugins.push(visualizer as PluginOption);
|
||||
}
|
||||
if (viteEnv.VITE_COMPRESS === 'Y') {
|
||||
plugins.push(compress(viteEnv));
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
@@ -1,9 +0,0 @@
|
||||
import { viteMockServe } from 'vite-plugin-mock';
|
||||
|
||||
export default viteMockServe({
|
||||
mockPath: 'mock',
|
||||
injectCode: `
|
||||
import { setupMockServer } from '../mock';
|
||||
setupMockServer();
|
||||
`
|
||||
});
|
44
build/plugins/router.ts
Normal file
44
build/plugins/router.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import routerPage from '@soybeanjs/router-page';
|
||||
|
||||
export default function createSoybeanjsPlugin() {
|
||||
return routerPage({
|
||||
pagesFormatter: names => {
|
||||
/** 系统的内置路由,该文件夹名称不作为RouteKey */
|
||||
const SYSTEM_VIEW = 'system-view';
|
||||
|
||||
const result = names
|
||||
.filter(name => name !== SYSTEM_VIEW)
|
||||
.map(name => {
|
||||
return name.replace(`${SYSTEM_VIEW}_`, '');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
32
build/plugins/unocss.ts
Normal file
32
build/plugins/unocss.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import process from 'node:process';
|
||||
import path from 'node:path';
|
||||
import unocss from '@unocss/vite';
|
||||
import presetIcons from '@unocss/preset-icons';
|
||||
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
|
||||
|
||||
export function setupUnocss(viteEnv: Env.ImportMeta) {
|
||||
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||
|
||||
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||
|
||||
/** The name of the local icon collection */
|
||||
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||
|
||||
return unocss({
|
||||
presets: [
|
||||
presetIcons({
|
||||
prefix: `${VITE_ICON_PREFIX}-`,
|
||||
scale: 1,
|
||||
extraProperties: {
|
||||
display: 'inline-block'
|
||||
},
|
||||
collections: {
|
||||
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||
)
|
||||
},
|
||||
warn: true
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
@@ -1,27 +1,28 @@
|
||||
import VueMacros from 'unplugin-vue-macros/vite';
|
||||
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 { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { AntDesignVueResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import { getSrcPath } from '../utils';
|
||||
|
||||
export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||
const { VITE_ICON_PREFFIX, VITE_ICON_LOCAL_PREFFIX } = viteEnv;
|
||||
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||
|
||||
const srcPath = getSrcPath();
|
||||
const localIconPath = `${srcPath}/assets/svg-icon`;
|
||||
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||
|
||||
/** 本地svg图标集合名称 */
|
||||
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
|
||||
/** The name of the local icon collection */
|
||||
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||
|
||||
return [
|
||||
VueMacros(),
|
||||
const plugins: PluginOption[] = [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
[collectionName]: FileSystemIconLoader(localIconPath)
|
||||
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||
)
|
||||
},
|
||||
scale: 1,
|
||||
defaultClass: 'inline-block'
|
||||
@@ -30,15 +31,20 @@ export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||
dts: 'src/typings/components.d.ts',
|
||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: false
|
||||
}),
|
||||
NaiveUiResolver(),
|
||||
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFFIX })
|
||||
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||
]
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [localIconPath],
|
||||
symbolId: `${VITE_ICON_LOCAL_PREFFIX}-[dir]-[name]`,
|
||||
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
|
||||
inject: 'body-last',
|
||||
customDomId: '__SVG_ICON_LOCAL__'
|
||||
})
|
||||
];
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default visualizer({
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
open: true
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 获取项目根路径
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目src路径
|
||||
* @param srcName - src目录名称(默认: "src")
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
const rootPath = getRootPath();
|
||||
|
||||
return `${rootPath}/${srcName}`;
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
.npmrc
|
||||
.cache
|
||||
|
||||
tests/server/static
|
||||
tests/server/static/upload
|
||||
|
||||
.local
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.eslintcache
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
# .vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
/vite-profile.cpuprofile
|
@@ -1,24 +0,0 @@
|
||||
FROM node:16.17.0 as builder
|
||||
|
||||
ENV WORKDIR=/soybean-admin
|
||||
|
||||
WORKDIR $WORKDIR
|
||||
|
||||
COPY ./ $WORKDIR/
|
||||
|
||||
ARG version
|
||||
ENV COMMITID=$version
|
||||
|
||||
RUN npm i -g pnpm
|
||||
|
||||
RUN pnpm install
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:alpine as prod
|
||||
|
||||
RUN mkdir /soybean
|
||||
|
||||
COPY --from=builder /soybean-admin/dist /soybean-admin
|
||||
COPY --from=builder /soybean-admin/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
@@ -1,54 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
# 不缓存html,防止程序更新后缓存继续生效
|
||||
if ($request_filename ~* .*\.(?:htm|html)$) {
|
||||
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||
access_log on;
|
||||
}
|
||||
root /soybean-admin/;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# location /soybean/soybean-webserver/v1 {
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
|
||||
# # 后台接口地址
|
||||
# proxy_pass http://192.168.1.99:30597/v1;
|
||||
# proxy_redirect default;
|
||||
# add_header Access-Control-Allow-Origin *;
|
||||
# add_header Access-Control-Allow-Headers X-Requested-With;
|
||||
# add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
|
||||
# }
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
24
eslint.config.js
Normal file
24
eslint.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from '@soybeanjs/eslint-config';
|
||||
|
||||
export default defineConfig(
|
||||
{ vue: true, unocss: true, ignores: ['src-tauri/target'] },
|
||||
{
|
||||
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'
|
||||
}
|
||||
}
|
||||
);
|
23
index.html
23
index.html
@@ -1,28 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-cmn-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/resource/loading.css" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= appName %></title>
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="loading-container">
|
||||
<div id="loadingLogo" class="loading-svg"></div>
|
||||
<div class="loading-spin__container">
|
||||
<div class="loading-spin">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-title"><%= appTitle %></div>
|
||||
</div>
|
||||
<script src="/resource/loading.js"></script>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
128
mock/api/auth.ts
128
mock/api/auth.ts
@@ -1,128 +0,0 @@
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
import { userModel } from '../model';
|
||||
|
||||
/** 参数错误的状态码 */
|
||||
const ERROR_PARAM_CODE = 10000;
|
||||
|
||||
const ERROR_PARAM_MSG = '参数校验失败!';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
// 获取验证码
|
||||
{
|
||||
url: '/mock/getSmsCode',
|
||||
method: 'post',
|
||||
response: (): Service.MockServiceResult<boolean> => {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: true
|
||||
};
|
||||
}
|
||||
},
|
||||
// 用户+密码 登录
|
||||
{
|
||||
url: '/mock/login',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
|
||||
const { userName = undefined, password = undefined } = options.body;
|
||||
|
||||
if (!userName || !password) {
|
||||
return {
|
||||
code: ERROR_PARAM_CODE,
|
||||
message: ERROR_PARAM_MSG,
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
const findItem = userModel.find(item => item.userName === userName && item.password === password);
|
||||
|
||||
if (findItem) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
token: findItem.token,
|
||||
refreshToken: findItem.refreshToken
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 1000,
|
||||
message: '用户名或密码错误!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
// 获取用户信息(请求头携带token, 根据token获取用户信息)
|
||||
{
|
||||
url: '/mock/getUserInfo',
|
||||
method: 'get',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.UserInfo | null> => {
|
||||
// 这里的mock插件得到的字段是authorization, 前端传递的是Authorization字段
|
||||
const { authorization = '' } = options.headers;
|
||||
const REFRESH_TOKEN_CODE = 66666;
|
||||
|
||||
if (!authorization) {
|
||||
return {
|
||||
code: REFRESH_TOKEN_CODE,
|
||||
message: '用户已失效或不存在!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
const userInfo: Auth.UserInfo = {
|
||||
userId: '',
|
||||
userName: '',
|
||||
userRole: 'user'
|
||||
};
|
||||
const isInUser = userModel.some(item => {
|
||||
const flag = item.token === authorization;
|
||||
if (flag) {
|
||||
const { userId: itemUserId, userName, userRole } = item;
|
||||
Object.assign(userInfo, { userId: itemUserId, userName, userRole });
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
|
||||
if (isInUser) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: userInfo
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: REFRESH_TOKEN_CODE,
|
||||
message: '用户信息异常!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/updateToken',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
|
||||
const { refreshToken = '' } = options.body;
|
||||
|
||||
const findItem = userModel.find(item => item.refreshToken === refreshToken);
|
||||
|
||||
if (findItem) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
token: findItem.token,
|
||||
refreshToken: findItem.refreshToken
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 3000,
|
||||
message: '用户已失效或不存在!',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@@ -1,5 +0,0 @@
|
||||
import auth from './auth';
|
||||
import route from './route';
|
||||
import management from './management';
|
||||
|
||||
export default [...auth, ...route, ...management];
|
@@ -1,33 +0,0 @@
|
||||
import { mock } from 'mockjs';
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
{
|
||||
url: '/mock/getAllUserList',
|
||||
method: 'post',
|
||||
response: (): Service.MockServiceResult<ApiUserManagement.User[]> => {
|
||||
const data = mock({
|
||||
'list|1000': [
|
||||
{
|
||||
id: '@id',
|
||||
userName: '@cname',
|
||||
'age|18-56': 56,
|
||||
'gender|1': ['0', '1', null],
|
||||
phone:
|
||||
/^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/,
|
||||
'email|1': ['@email("qq.com")', null],
|
||||
'userStatus|1': ['1', '2', '3', '4', null]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: data.list
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@@ -1,29 +0,0 @@
|
||||
import type { MockMethod } from 'vite-plugin-mock';
|
||||
import { routeModel, userModel } from '../model';
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
{
|
||||
url: '/mock/getUserRoutes',
|
||||
method: 'post',
|
||||
response: (options: Service.MockOption): Service.MockServiceResult => {
|
||||
const { userId = undefined } = options.body;
|
||||
|
||||
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis';
|
||||
|
||||
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
||||
|
||||
const filterRoutes = routeModel[role];
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
routes: filterRoutes,
|
||||
home: routeHomeName
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default apis;
|
@@ -1,6 +0,0 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
|
||||
import api from './api';
|
||||
|
||||
export function setupMockServer() {
|
||||
createProdMockServer(api);
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
interface UserModel extends Auth.UserInfo {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const userModel: UserModel[] = [
|
||||
{
|
||||
token: '__TOKEN_SOYBEAN__',
|
||||
refreshToken: '__REFRESH_TOKEN_SOYBEAN__',
|
||||
userId: '0',
|
||||
userName: 'Soybean',
|
||||
userRole: 'super',
|
||||
password: 'soybean123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_SUPER__',
|
||||
refreshToken: '__REFRESH_TOKEN_SUPER__',
|
||||
userId: '1',
|
||||
userName: 'Super',
|
||||
userRole: 'super',
|
||||
password: 'super123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_ADMIN__',
|
||||
refreshToken: '__REFRESH_TOKEN_ADMIN__',
|
||||
userId: '2',
|
||||
userName: 'Admin',
|
||||
userRole: 'admin',
|
||||
password: 'admin123'
|
||||
},
|
||||
{
|
||||
token: '__TOKEN_USER01__',
|
||||
refreshToken: '__REFRESH_TOKEN_USER01__',
|
||||
userId: '3',
|
||||
userName: 'User01',
|
||||
userRole: 'user',
|
||||
password: 'user01123'
|
||||
}
|
||||
];
|
@@ -1,2 +0,0 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
1126
mock/model/route.ts
1126
mock/model/route.ts
File diff suppressed because it is too large
Load Diff
192
package.json
192
package.json
@@ -1,133 +1,111 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"version": "0.9.7",
|
||||
"type": "module",
|
||||
"version": "1.0.2",
|
||||
"packageManager": "pnpm@8.15.6",
|
||||
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||
"author": {
|
||||
"name": "Soybean",
|
||||
"email": "honghuangdc@gmail.com",
|
||||
"url": "https://github.com/honghuangdc"
|
||||
"email": "soybeanjs@outlook.com",
|
||||
"url": "https://github.com/soybeanjs"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/honghuangdc/soybean-admin",
|
||||
"homepage": "https://github.com/soybeanjs/soybean-admin",
|
||||
"repository": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin.git"
|
||||
"url": "https://github.com/soybeanjs/soybean-admin.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin/issues"
|
||||
"url": "https://github.com/soybeanjs/soybean-admin/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"Vue",
|
||||
"Vue3",
|
||||
"admin",
|
||||
"admin-template",
|
||||
"vue-admin",
|
||||
"Vue3 admin ",
|
||||
"vue-admin-template",
|
||||
"Vite3",
|
||||
"Vite",
|
||||
"vite-admin",
|
||||
"Vite5",
|
||||
"TypeScript",
|
||||
"TS",
|
||||
"NaiveUI",
|
||||
"naive-ui",
|
||||
"naive-admin",
|
||||
"NaiveUI-Admin",
|
||||
"naive-ui-admin",
|
||||
"ant-design-vue v4",
|
||||
"UnoCSS"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_SERVICE_ENV=dev vite",
|
||||
"dev:test": "cross-env VITE_SERVICE_ENV=test vite",
|
||||
"dev:prod": "cross-env VITE_SERVICE_ENV=prod vite",
|
||||
"build": "npm run typecheck && cross-env VITE_SERVICE_ENV=prod vite build",
|
||||
"build:dev": "npm run typecheck && cross-env VITE_SERVICE_ENV=dev vite build",
|
||||
"build:test": "npm run typecheck && cross-env VITE_SERVICE_ENV=test vite build",
|
||||
"build:vercel": "cross-env VITE_HASH_ROUTE=Y VITE_VERCEL=Y vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||
"build": "vite build --mode prod",
|
||||
"build:tauri": "pnpm tauri build",
|
||||
"build:test": "vite build --mode test",
|
||||
"cleanup": "sa cleanup",
|
||||
"commit": "sa git-commit",
|
||||
"dev": "vite --mode test",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"dev:tauri": "pnpm tauri dev",
|
||||
"gen-route": "sa gen-route",
|
||||
"lint": "eslint . --fix",
|
||||
"commit": "soybean git-commit",
|
||||
"esno": "esno",
|
||||
"cleanup": "esno ./scripts/cleanup.ts",
|
||||
"update-pkg": "ncu --deep -u",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
||||
"release": "standard-version",
|
||||
"prepare": "husky install"
|
||||
"prepare": "simple-git-hooks",
|
||||
"preview": "vite preview",
|
||||
"release": "sa release",
|
||||
"tauri-icon": "pnpm tauri icon ./public/logo.png",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||
"update-pkg": "sa update-pkg"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "^0.11.8",
|
||||
"@antv/g2": "^4.2.8",
|
||||
"@better-scroll/core": "^2.5.0",
|
||||
"@soybeanjs/vue-admin-layout": "^1.1.1",
|
||||
"@soybeanjs/vue-admin-tab": "^1.0.5",
|
||||
"@vueuse/core": "^9.4.0",
|
||||
"axios": "0.27.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"colord": "^2.9.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"echarts": "^5.4.0",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "2.33.5",
|
||||
"pinia": "^2.0.23",
|
||||
"print-js": "^1.6.0",
|
||||
"qs": "^6.11.0",
|
||||
"swiper": "^8.4.4",
|
||||
"ua-parser-js": "^1.0.32",
|
||||
"vditor": "^3.8.18",
|
||||
"vue": "3.2.41",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"wangeditor": "^4.7.15",
|
||||
"xgplayer": "^2.32.1"
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color-palette": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.10",
|
||||
"echarts": "5.5.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"naive-ui": "2.38.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.1.7",
|
||||
"vue": "3.4.21",
|
||||
"vue-draggable-plus": "0.4.0",
|
||||
"vue-i18n": "9.11.0",
|
||||
"vue-router": "4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@amap/amap-jsapi-types": "^0.0.10",
|
||||
"@iconify/json": "^2.1.133",
|
||||
"@iconify/vue": "^4.0.0",
|
||||
"@soybeanjs/cli": "^0.1.2",
|
||||
"@soybeanjs/router-page": "1.0.3",
|
||||
"@tauri-apps/cli": "^1.1.1",
|
||||
"@types/bmapgl": "^0.0.5",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/node": "18.8.3",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@unocss/preset-uno": "^0.46.3",
|
||||
"@unocss/vite": "^0.46.3",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vitejs/plugin-vue-jsx": "^2.1.0",
|
||||
"conventional-changelog": "^3.1.25",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-soybeanjs-vue": "^0.1.2",
|
||||
"esno": "^0.16.3",
|
||||
"husky": "^8.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"npm-check-updates": "^16.3.16",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-visualizer": "^5.8.3",
|
||||
"sass": "^1.56.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "4.8.4",
|
||||
"unplugin-icons": "^0.14.13",
|
||||
"unplugin-vue-components": "0.22.8",
|
||||
"unplugin-vue-macros": "^0.16.0",
|
||||
"utility-types": "^3.10.0",
|
||||
"vite": "^3.2.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-progress": "^0.0.6",
|
||||
"vite-plugin-pwa": "^0.13.2",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^1.0.9",
|
||||
"zx": "^7.1.1"
|
||||
"@elegant-router/vue": "0.3.6",
|
||||
"@iconify/json": "2.2.198",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.2.5",
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "20.12.5",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "0.59.0",
|
||||
"@unocss/preset-icons": "0.59.0",
|
||||
"@unocss/preset-uno": "0.59.0",
|
||||
"@unocss/transformer-directives": "0.59.0",
|
||||
"@unocss/transformer-variant-group": "0.59.0",
|
||||
"@unocss/vite": "0.59.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "3.1.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.24.0",
|
||||
"lint-staged": "15.2.2",
|
||||
"sass": "1.74.1",
|
||||
"simple-git-hooks": "2.11.1",
|
||||
"tsx": "4.7.2",
|
||||
"typescript": "5.4.4",
|
||||
"unplugin-icons": "0.18.5",
|
||||
"unplugin-vue-components": "0.26.0",
|
||||
"vite": "5.2.8",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "7.0.25",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-tsc": "2.0.11"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
||||
}
|
||||
}
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
"pre-commit": "pnpm typecheck && pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
"website": "https://admin.soybeanjs.cn"
|
||||
}
|
||||
|
21
packages/axios/package.json
Normal file
21
packages/axios/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@sa/axios",
|
||||
"version": "1.0.2",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.6.8",
|
||||
"axios-retry": "4.1.0",
|
||||
"qs": "6.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "6.9.14"
|
||||
}
|
||||
}
|
5
packages/axios/src/constant.ts
Normal file
5
packages/axios/src/constant.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/** request id key */
|
||||
export const REQUEST_ID_KEY = 'X-Request-Id';
|
||||
|
||||
/** the backend error code key */
|
||||
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
178
packages/axios/src/index.ts
Normal file
178
packages/axios/src/index.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
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;
|
||||
|
||||
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 };
|
48
packages/axios/src/options.ts
Normal file
48
packages/axios/src/options.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { CreateAxiosDefaults } from 'axios';
|
||||
import type { IAxiosRetryConfig } from 'axios-retry';
|
||||
import { stringify } from 'qs';
|
||||
import { isHttpSuccess } from './shared';
|
||||
import type { RequestOption } from './type';
|
||||
|
||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
||||
const opts: RequestOption<ResponseData> = {
|
||||
onRequest: async config => config,
|
||||
isBackendSuccess: _response => true,
|
||||
onBackendFail: async () => {},
|
||||
transformBackendResponse: async response => response.data,
|
||||
onError: async () => {}
|
||||
};
|
||||
|
||||
Object.assign(opts, options);
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
|
||||
const retryConfig: IAxiosRetryConfig = {
|
||||
retries: 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;
|
||||
}
|
28
packages/axios/src/shared.ts
Normal file
28
packages/axios/src/shared.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export function getContentType(config: InternalAxiosRequestConfig) {
|
||||
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if http status is success
|
||||
*
|
||||
* @param status
|
||||
*/
|
||||
export function isHttpSuccess(status: number) {
|
||||
const isSuccessCode = status >= 200 && status < 300;
|
||||
return isSuccessCode || status === 304;
|
||||
}
|
||||
|
||||
/**
|
||||
* is response json
|
||||
*
|
||||
* @param response axios response
|
||||
*/
|
||||
export function isResponseJson(response: AxiosResponse) {
|
||||
const { responseType } = response.config;
|
||||
|
||||
return responseType === 'json' || responseType === undefined;
|
||||
}
|
101
packages/axios/src/type.ts
Normal file
101
packages/axios/src/type.ts
Normal 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>>;
|
||||
}
|
20
packages/axios/tsconfig.json
Normal file
20
packages/axios/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
15
packages/color-palette/package.json
Normal file
15
packages/color-palette/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@sa/color-palette",
|
||||
"version": "1.0.2",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"colord": "2.9.3"
|
||||
}
|
||||
}
|
29
packages/color-palette/src/color.ts
Normal file
29
packages/color-palette/src/color.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { colord, extend } from 'colord';
|
||||
import type { HslColor } from 'colord';
|
||||
import labPlugin from 'colord/plugins/lab';
|
||||
|
||||
extend([labPlugin]);
|
||||
|
||||
export function isValidColor(color: string) {
|
||||
return colord(color).isValid();
|
||||
}
|
||||
|
||||
export function getHex(color: string) {
|
||||
return colord(color).toHex();
|
||||
}
|
||||
|
||||
export function getRgb(color: string) {
|
||||
return colord(color).toRgb();
|
||||
}
|
||||
|
||||
export function getHsl(color: string) {
|
||||
return colord(color).toHsl();
|
||||
}
|
||||
|
||||
export function getDeltaE(color1: string, color2: string) {
|
||||
return colord(color1).delta(color2);
|
||||
}
|
||||
|
||||
export function transformHslToHex(color: HslColor) {
|
||||
return colord(color).toHex();
|
||||
}
|
56
packages/color-palette/src/index.ts
Normal file
56
packages/color-palette/src/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { getColorPaletteFamily } from './palette';
|
||||
import { getColorName } from './name';
|
||||
import type { ColorPalette, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color by color palette number
|
||||
*
|
||||
* @param color Color
|
||||
* @param num Color palette number
|
||||
* @returns Color hexcode
|
||||
*/
|
||||
export function getColorByColorPaletteNumber(color: string, num: ColorPaletteNumber) {
|
||||
const colorPalette = getColorPalette(color, color);
|
||||
|
||||
const colorItem = colorPalette.colorMap.get(num) as ColorPaletteItem;
|
||||
|
||||
return colorItem.hexcode;
|
||||
}
|
||||
|
||||
export default getColorPalette;
|
||||
|
||||
/** The builtin color palettes */
|
||||
const colorPalettes = defaultPalettes as ColorPaletteFamily[];
|
||||
|
||||
export { getColorName, colorPalettes };
|
||||
|
||||
export type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily };
|
1568
packages/color-palette/src/json/color-name.json
Normal file
1568
packages/color-palette/src/json/color-name.json
Normal file
File diff suppressed because it is too large
Load Diff
274
packages/color-palette/src/json/palette.json
Normal file
274
packages/color-palette/src/json/palette.json
Normal file
@@ -0,0 +1,274 @@
|
||||
[
|
||||
{
|
||||
"key": "red",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fef2f2", "number": 50, "name": "Bridesmaid" },
|
||||
{ "hexcode": "#fee2e2", "number": 100, "name": "Pippin" },
|
||||
{ "hexcode": "#fecaca", "number": 200, "name": "Your Pink" },
|
||||
{ "hexcode": "#fca5a5", "number": 300, "name": "Cornflower Lilac" },
|
||||
{ "hexcode": "#f87171", "number": 400, "name": "Bittersweet" },
|
||||
{ "hexcode": "#ef4444", "number": 500, "name": "Cinnabar" },
|
||||
{ "hexcode": "#dc2626", "number": 600, "name": "Persian Red" },
|
||||
{ "hexcode": "#b91c1c", "number": 700, "name": "Thunderbird" },
|
||||
{ "hexcode": "#991b1b", "number": 800, "name": "Old Brick" },
|
||||
{ "hexcode": "#7f1d1d", "number": 900, "name": "Falu Red" },
|
||||
{ "hexcode": "#450a0a", "number": 950, "name": "Mahogany" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "orange",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff7ed", "number": 50, "name": "Serenade" },
|
||||
{ "hexcode": "#ffedd5", "number": 100, "name": "Derby" },
|
||||
{ "hexcode": "#fed7aa", "number": 200, "name": "Caramel" },
|
||||
{ "hexcode": "#fdba74", "number": 300, "name": "Macaroni and Cheese" },
|
||||
{ "hexcode": "#fb923c", "number": 400, "name": "Neon Carrot" },
|
||||
{ "hexcode": "#f97316", "number": 500, "name": "Ecstasy" },
|
||||
{ "hexcode": "#ea580c", "number": 600, "name": "Trinidad" },
|
||||
{ "hexcode": "#c2410c", "number": 700, "name": "Tia Maria" },
|
||||
{ "hexcode": "#9a3412", "number": 800, "name": "Tabasco" },
|
||||
{ "hexcode": "#7c2d12", "number": 900, "name": "Pueblo" },
|
||||
{ "hexcode": "#431407", "number": 950, "name": "Rebel" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "amber",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fffbeb", "number": 50, "name": "Island Spice" },
|
||||
{ "hexcode": "#fef3c7", "number": 100, "name": "Beeswax" },
|
||||
{ "hexcode": "#fde68a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fcd34d", "number": 300, "name": "Mustard" },
|
||||
{ "hexcode": "#fbbf24", "number": 400, "name": "Lightning Yellow" },
|
||||
{ "hexcode": "#f59e0b", "number": 500, "name": "California" },
|
||||
{ "hexcode": "#d97706", "number": 600, "name": "Christine" },
|
||||
{ "hexcode": "#b45309", "number": 700, "name": "Vesuvius" },
|
||||
{ "hexcode": "#92400e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#78350f", "number": 900, "name": "Copper Canyon" },
|
||||
{ "hexcode": "#451a03", "number": 950, "name": "Brown Pod" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "yellow",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fefce8", "number": 50, "name": "Orange White" },
|
||||
{ "hexcode": "#fef9c3", "number": 100, "name": "Lemon Chiffon" },
|
||||
{ "hexcode": "#fef08a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fde047", "number": 300, "name": "Bright Sun" },
|
||||
{ "hexcode": "#facc15", "number": 400, "name": "Candlelight" },
|
||||
{ "hexcode": "#eab308", "number": 500, "name": "Corn" },
|
||||
{ "hexcode": "#ca8a04", "number": 600, "name": "Pirate Gold" },
|
||||
{ "hexcode": "#a16207", "number": 700, "name": "Mai Tai" },
|
||||
{ "hexcode": "#854d0e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#713f12", "number": 900, "name": "Sepia" },
|
||||
{ "hexcode": "#422006", "number": 950, "name": "Dark Ebony" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lime",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f7fee7", "number": 50, "name": "Spring Sun" },
|
||||
{ "hexcode": "#ecfccb", "number": 100, "name": "Chiffon" },
|
||||
{ "hexcode": "#d9f99d", "number": 200, "name": "Gossip" },
|
||||
{ "hexcode": "#bef264", "number": 300, "name": "Sulu" },
|
||||
{ "hexcode": "#a3e635", "number": 400, "name": "Conifer" },
|
||||
{ "hexcode": "#84cc16", "number": 500, "name": "Lima" },
|
||||
{ "hexcode": "#65a30d", "number": 600, "name": "Christi" },
|
||||
{ "hexcode": "#4d7c0f", "number": 700, "name": "Green Leaf" },
|
||||
{ "hexcode": "#3f6212", "number": 800, "name": "Dell" },
|
||||
{ "hexcode": "#365314", "number": 900, "name": "Clover" },
|
||||
{ "hexcode": "#1a2e05", "number": 950, "name": "Deep Forest Green" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "green",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdf4", "number": 50, "name": "Ottoman" },
|
||||
{ "hexcode": "#dcfce7", "number": 100, "name": "Blue Romance" },
|
||||
{ "hexcode": "#bbf7d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#86efac", "number": 300, "name": "Algae Green" },
|
||||
{ "hexcode": "#4ade80", "number": 400, "name": "Emerald" },
|
||||
{ "hexcode": "#22c55e", "number": 500, "name": "Malachite" },
|
||||
{ "hexcode": "#16a34a", "number": 600, "name": "Salem" },
|
||||
{ "hexcode": "#15803d", "number": 700, "name": "Jewel" },
|
||||
{ "hexcode": "#166534", "number": 800, "name": "Jewel" },
|
||||
{ "hexcode": "#14532d", "number": 900, "name": "Green Pea" },
|
||||
{ "hexcode": "#052e16", "number": 950, "name": "English Holly" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "emerald",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfdf5", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#d1fae5", "number": 100, "name": "Granny Apple" },
|
||||
{ "hexcode": "#a7f3d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#6ee7b7", "number": 300, "name": "Bermuda" },
|
||||
{ "hexcode": "#34d399", "number": 400, "name": "Shamrock" },
|
||||
{ "hexcode": "#10b981", "number": 500, "name": "Mountain Meadow" },
|
||||
{ "hexcode": "#059669", "number": 600, "name": "Green Haze" },
|
||||
{ "hexcode": "#047857", "number": 700, "name": "Watercourse" },
|
||||
{ "hexcode": "#065f46", "number": 800, "name": "Watercourse" },
|
||||
{ "hexcode": "#064e3b", "number": 900, "name": "Evening Sea" },
|
||||
{ "hexcode": "#022c22", "number": 950, "name": "Burnham" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "teal",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdfa", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#ccfbf1", "number": 100, "name": "Scandal" },
|
||||
{ "hexcode": "#99f6e4", "number": 200, "name": "Ice Cold" },
|
||||
{ "hexcode": "#5eead4", "number": 300, "name": "Turquoise Blue" },
|
||||
{ "hexcode": "#2dd4bf", "number": 400, "name": "Turquoise" },
|
||||
{ "hexcode": "#14b8a6", "number": 500, "name": "Java" },
|
||||
{ "hexcode": "#0d9488", "number": 600, "name": "Blue Chill" },
|
||||
{ "hexcode": "#0f766e", "number": 700, "name": "Genoa" },
|
||||
{ "hexcode": "#115e59", "number": 800, "name": "Eden" },
|
||||
{ "hexcode": "#134e4a", "number": 900, "name": "Eden" },
|
||||
{ "hexcode": "#042f2e", "number": 950, "name": "Tiber" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "cyan",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfeff", "number": 50, "name": "Bubbles" },
|
||||
{ "hexcode": "#cffafe", "number": 100, "name": "Oyster Bay" },
|
||||
{ "hexcode": "#a5f3fc", "number": 200, "name": "Anakiwa" },
|
||||
{ "hexcode": "#67e8f9", "number": 300, "name": "Spray" },
|
||||
{ "hexcode": "#22d3ee", "number": 400, "name": "Bright Turquoise" },
|
||||
{ "hexcode": "#06b6d4", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0891b2", "number": 600, "name": "Bondi Blue" },
|
||||
{ "hexcode": "#0e7490", "number": 700, "name": "Blue Chill" },
|
||||
{ "hexcode": "#155e75", "number": 800, "name": "Blumine" },
|
||||
{ "hexcode": "#164e63", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#083344", "number": 950, "name": "Tarawera" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sky",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0f9ff", "number": 50, "name": "Alice Blue" },
|
||||
{ "hexcode": "#e0f2fe", "number": 100, "name": "Pattens Blue" },
|
||||
{ "hexcode": "#bae6fd", "number": 200, "name": "French Pass" },
|
||||
{ "hexcode": "#7dd3fc", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#38bdf8", "number": 400, "name": "Picton Blue" },
|
||||
{ "hexcode": "#0ea5e9", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0284c7", "number": 600, "name": "Lochmara" },
|
||||
{ "hexcode": "#0369a1", "number": 700, "name": "Bahama Blue" },
|
||||
{ "hexcode": "#075985", "number": 800, "name": "Venice Blue" },
|
||||
{ "hexcode": "#0c4a6e", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#082f49", "number": 950, "name": "Blue Whale" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "blue",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eff6ff", "number": 50, "name": "Zumthor" },
|
||||
{ "hexcode": "#dbeafe", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#bfdbfe", "number": 200, "name": "Tropical Blue" },
|
||||
{ "hexcode": "#93c5fd", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#60a5fa", "number": 400, "name": "Cornflower Blue" },
|
||||
{ "hexcode": "#3b82f6", "number": 500, "name": "Dodger Blue" },
|
||||
{ "hexcode": "#2563eb", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#1d4ed8", "number": 700, "name": "Cerulean Blue" },
|
||||
{ "hexcode": "#1e40af", "number": 800, "name": "Persian Blue" },
|
||||
{ "hexcode": "#1e3a8a", "number": 900, "name": "Bay of Many" },
|
||||
{ "hexcode": "#172554", "number": 950, "name": "Bunting" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "indigo",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eef2ff", "number": 50, "name": "Zircon" },
|
||||
{ "hexcode": "#e0e7ff", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#c7d2fe", "number": 200, "name": "Periwinkle" },
|
||||
{ "hexcode": "#a5b4fc", "number": 300, "name": "Perano" },
|
||||
{ "hexcode": "#818cf8", "number": 400, "name": "Portage" },
|
||||
{ "hexcode": "#6366f1", "number": 500, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4f46e5", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4338ca", "number": 700, "name": "Governor Bay" },
|
||||
{ "hexcode": "#3730a3", "number": 800, "name": "Governor Bay" },
|
||||
{ "hexcode": "#312e81", "number": 900, "name": "Minsk" },
|
||||
{ "hexcode": "#1e1b4b", "number": 950, "name": "Port Gore" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "violet",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f5f3ff", "number": 50, "name": "Titan White" },
|
||||
{ "hexcode": "#ede9fe", "number": 100, "name": "Titan White" },
|
||||
{ "hexcode": "#ddd6fe", "number": 200, "name": "Fog" },
|
||||
{ "hexcode": "#c4b5fd", "number": 300, "name": "Melrose" },
|
||||
{ "hexcode": "#a78bfa", "number": 400, "name": "Dull Lavender" },
|
||||
{ "hexcode": "#8b5cf6", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#7c3aed", "number": 600, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6d28d9", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#5b21b6", "number": 800, "name": "Purple Heart" },
|
||||
{ "hexcode": "#4c1d95", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#2e1065", "number": 950, "name": "Violent Violet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "purple",
|
||||
"palettes": [
|
||||
{ "hexcode": "#faf5ff", "number": 50, "name": "Magnolia" },
|
||||
{ "hexcode": "#f3e8ff", "number": 100, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#e9d5ff", "number": 200, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#d8b4fe", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#c084fc", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#a855f7", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#9333ea", "number": 600, "name": "Electric Violet" },
|
||||
{ "hexcode": "#7e22ce", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6b21a8", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#581c87", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#3b0764", "number": 950, "name": "Christalle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "fuchsia",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf4ff", "number": 50, "name": "White Pointer" },
|
||||
{ "hexcode": "#fae8ff", "number": 100, "name": "White Pointer" },
|
||||
{ "hexcode": "#f5d0fe", "number": 200, "name": "Mauve" },
|
||||
{ "hexcode": "#f0abfc", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#e879f9", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#d946ef", "number": 500, "name": "Heliotrope" },
|
||||
{ "hexcode": "#c026d3", "number": 600, "name": "Fuchsia Pink" },
|
||||
{ "hexcode": "#a21caf", "number": 700, "name": "Violet Eggplant" },
|
||||
{ "hexcode": "#86198f", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#701a75", "number": 900, "name": "Seance" },
|
||||
{ "hexcode": "#4a044e", "number": 950, "name": "Clairvoyant" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pink",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf2f8", "number": 50, "name": "Wisp Pink" },
|
||||
{ "hexcode": "#fce7f3", "number": 100, "name": "Carousel Pink" },
|
||||
{ "hexcode": "#fbcfe8", "number": 200, "name": "Classic Rose" },
|
||||
{ "hexcode": "#f9a8d4", "number": 300, "name": "Lavender Pink" },
|
||||
{ "hexcode": "#f472b6", "number": 400, "name": "Persian Pink" },
|
||||
{ "hexcode": "#ec4899", "number": 500, "name": "Brilliant Rose" },
|
||||
{ "hexcode": "#db2777", "number": 600, "name": "Cerise" },
|
||||
{ "hexcode": "#be185d", "number": 700, "name": "Maroon Flush" },
|
||||
{ "hexcode": "#9d174d", "number": 800, "name": "Disco" },
|
||||
{ "hexcode": "#831843", "number": 900, "name": "Disco" },
|
||||
{ "hexcode": "#500724", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "rose",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff1f2", "number": 50, "name": "Lavender blush" },
|
||||
{ "hexcode": "#ffe4e6", "number": 100, "name": "Cosmos" },
|
||||
{ "hexcode": "#fecdd3", "number": 200, "name": "Pastel Pink" },
|
||||
{ "hexcode": "#fda4af", "number": 300, "name": "Sweet Pink" },
|
||||
{ "hexcode": "#fb7185", "number": 400, "name": "Froly" },
|
||||
{ "hexcode": "#f43f5e", "number": 500, "name": "Radical Red" },
|
||||
{ "hexcode": "#e11d48", "number": 600, "name": "Amaranth" },
|
||||
{ "hexcode": "#be123c", "number": 700, "name": "Cardinal" },
|
||||
{ "hexcode": "#9f1239", "number": 800, "name": "Shiraz" },
|
||||
{ "hexcode": "#881337", "number": 900, "name": "Claret" },
|
||||
{ "hexcode": "#4c0519", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
}
|
||||
]
|
46
packages/color-palette/src/name.ts
Normal file
46
packages/color-palette/src/name.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getHex, 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;
|
||||
}
|
95
packages/color-palette/src/palette.ts
Normal file
95
packages/color-palette/src/palette.ts
Normal 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;
|
||||
}
|
49
packages/color-palette/src/type.ts
Normal file
49
packages/color-palette/src/type.ts
Normal 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;
|
||||
};
|
15
packages/hooks/package.json
Normal file
15
packages/hooks/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.0.2",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/axios": "workspace:*"
|
||||
}
|
||||
}
|
10
packages/hooks/src/index.ts
Normal file
10
packages/hooks/src/index.ts
Normal 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';
|
@@ -1,8 +1,9 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* boolean组合式函数
|
||||
* @param initValue 初始值
|
||||
* Boolean
|
||||
*
|
||||
* @param initValue Init value
|
||||
*/
|
||||
export default function useBoolean(initValue = false) {
|
||||
const bool = ref(initValue);
|
96
packages/hooks/src/use-context.ts
Normal file
96
packages/hooks/src/use-context.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* Use context
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
|
||||
*
|
||||
* // context.ts
|
||||
* import { ref } from 'vue';
|
||||
* import { useContext } from '@sa/hooks';
|
||||
*
|
||||
* export const { setupStore, useStore } = useContext('demo', () => {
|
||||
* const count = ref(0);
|
||||
*
|
||||
* function increment() {
|
||||
* count.value++;
|
||||
* }
|
||||
*
|
||||
* function decrement() {
|
||||
* count.value--;
|
||||
* }
|
||||
*
|
||||
* return {
|
||||
* count,
|
||||
* increment,
|
||||
* decrement
|
||||
* };
|
||||
* })
|
||||
* ``` // A.vue
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div>A</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { setupStore } from './context';
|
||||
*
|
||||
* setupStore();
|
||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||
* </script>
|
||||
* ``` // B.vue
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div>B</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { useStore } from './context';
|
||||
*
|
||||
* const { count, increment } = useStore();
|
||||
* </script>
|
||||
* ```;
|
||||
*
|
||||
* // C.vue is same as B.vue
|
||||
*
|
||||
* @param contextName Context name
|
||||
* @param fn Context function
|
||||
*/
|
||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||
type Context = ReturnType<T>;
|
||||
|
||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||
|
||||
function setupStore(...args: Parameters<T>) {
|
||||
const context: Context = fn(...args);
|
||||
return useProvide(context);
|
||||
}
|
||||
|
||||
return {
|
||||
/** Setup store in the parent component */
|
||||
setupStore,
|
||||
/** Use store in the child component */
|
||||
useStore
|
||||
};
|
||||
}
|
||||
|
||||
/** Create context */
|
||||
function createContext<T>(contextName: string) {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
};
|
||||
}
|
49
packages/hooks/src/use-count-down.ts
Normal file
49
packages/hooks/src/use-count-down.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { computed, onScopeDispose, ref } from 'vue';
|
||||
import { useRafFn } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* count down
|
||||
*
|
||||
* @param seconds - count down seconds
|
||||
*/
|
||||
export default function useCountDown(seconds: number) {
|
||||
const FPS_PER_SECOND = 60;
|
||||
|
||||
const fps = ref(0);
|
||||
|
||||
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
|
||||
|
||||
const isCounting = computed(() => fps.value > 0);
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => {
|
||||
if (fps.value > 0) {
|
||||
fps.value -= 1;
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
function start(updateSeconds: number = seconds) {
|
||||
fps.value = FPS_PER_SECOND * updateSeconds;
|
||||
resume();
|
||||
}
|
||||
|
||||
function stop() {
|
||||
fps.value = 0;
|
||||
pause();
|
||||
}
|
||||
|
||||
onScopeDispose(() => {
|
||||
pause();
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
isCounting,
|
||||
start,
|
||||
stop
|
||||
};
|
||||
}
|
@@ -1,5 +1,10 @@
|
||||
import useBoolean from './useBoolean';
|
||||
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);
|
||||
|
79
packages/hooks/src/use-request.ts
Normal file
79
packages/hooks/src/use-request.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { createFlatRequest } from '@sa/axios';
|
||||
import type {
|
||||
AxiosError,
|
||||
CreateAxiosDefaults,
|
||||
CustomAxiosRequestConfig,
|
||||
MappedType,
|
||||
RequestOption,
|
||||
ResponseType
|
||||
} from '@sa/axios';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||
data: Ref<T>;
|
||||
error: Ref<null>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||
data: Ref<null>;
|
||||
error: Ref<AxiosError<ResponseData>>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||
loading: Ref<boolean>;
|
||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
|
||||
export interface HookRequestInstance<ResponseData = any> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||
cancelRequest: (requestId: string) => void;
|
||||
cancelAllRequest: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a hook request instance
|
||||
*
|
||||
* @param axiosConfig
|
||||
* @param options
|
||||
*/
|
||||
export default function createHookRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||
|
||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
|
||||
startLoading();
|
||||
|
||||
request(config).then(res => {
|
||||
if (res.data) {
|
||||
data.value = res.data;
|
||||
} else {
|
||||
error.value = res.error;
|
||||
}
|
||||
|
||||
endLoading();
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
data,
|
||||
error
|
||||
};
|
||||
} as HookRequestInstance<ResponseData>;
|
||||
|
||||
hookRequest.cancelRequest = request.cancelRequest;
|
||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||
|
||||
return hookRequest;
|
||||
}
|
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { h } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
/**
|
||||
* Svg icon render hook
|
||||
*
|
||||
* @param SvgIcon Svg icon component
|
||||
*/
|
||||
export default function useSvgIconRender(SvgIcon: Component) {
|
||||
interface IconConfig {
|
||||
/** Iconify icon name */
|
||||
icon?: string;
|
||||
/** Local icon name */
|
||||
localIcon?: string;
|
||||
/** Icon color */
|
||||
color?: string;
|
||||
/** Icon size */
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
|
||||
|
||||
/**
|
||||
* Svg icon VNode
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
const SvgIconVNode = (config: IconConfig) => {
|
||||
const { color, fontSize, icon, localIcon } = config;
|
||||
|
||||
const style: IconStyle = {};
|
||||
|
||||
if (color) {
|
||||
style.color = color;
|
||||
}
|
||||
if (fontSize) {
|
||||
style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
if (!icon && !localIcon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => h(SvgIcon, { icon, localIcon, style });
|
||||
};
|
||||
|
||||
return {
|
||||
SvgIconVNode
|
||||
};
|
||||
}
|
151
packages/hooks/src/use-table.ts
Normal file
151
packages/hooks/src/use-table.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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> = {
|
||||
data: TableDataWithIndex<T>[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
||||
|
||||
export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api function to get table data */
|
||||
apiFn: A;
|
||||
/** api params */
|
||||
apiParams?: Parameters<A>[0];
|
||||
/** transform api response to table data */
|
||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||
/** columns factory */
|
||||
columns: () => C[];
|
||||
/**
|
||||
* get column checks
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
||||
/**
|
||||
* get columns
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||
/**
|
||||
* callback when response fetched
|
||||
*
|
||||
* @param transformed transformed data
|
||||
*/
|
||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
};
|
||||
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||
|
||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||
|
||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...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.data;
|
||||
|
||||
setEmpty(transformed.data.length === 0);
|
||||
|
||||
await config.onFetched?.(transformed);
|
||||
|
||||
endLoading();
|
||||
}
|
||||
|
||||
function formatSearchParams(params: Record<string, unknown>) {
|
||||
const formattedParams: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formattedParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* update search params
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||
Object.assign(searchParams, params);
|
||||
}
|
||||
|
||||
/** reset search params */
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, apiParams);
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
getData();
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
};
|
||||
}
|
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
19
packages/materials/package.json
Normal file
19
packages/materials/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.0.2",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"simplebar-vue": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.9.1"
|
||||
}
|
||||
}
|
6
packages/materials/src/index.ts
Normal file
6
packages/materials/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
|
||||
import PageTab from './libs/page-tab';
|
||||
import SimpleScrollbar from './libs/simple-scrollbar';
|
||||
|
||||
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
|
||||
export * from './types';
|
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
@@ -0,0 +1,63 @@
|
||||
/* @type */
|
||||
|
||||
.layout-header,
|
||||
.layout-header-placement {
|
||||
height: var(--soy-header-height);
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
z-index: var(--soy-header-z-index);
|
||||
}
|
||||
|
||||
.layout-tab {
|
||||
top: var(--soy-header-height);
|
||||
height: var(--soy-tab-height);
|
||||
z-index: var(--soy-tab-z-index);
|
||||
}
|
||||
|
||||
.layout-tab-placement {
|
||||
height: var(--soy-tab-height);
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
width: var(--soy-sider-width);
|
||||
z-index: var(--soy-sider-z-index);
|
||||
}
|
||||
|
||||
.layout-mobile-sider {
|
||||
z-index: var(--soy-sider-z-index);
|
||||
}
|
||||
|
||||
.layout-mobile-sider-mask {
|
||||
z-index: var(--soy-mobile-sider-z-index);
|
||||
}
|
||||
|
||||
.layout-sider_collapsed {
|
||||
width: var(--soy-sider-collapsed-width);
|
||||
z-index: var(--soy-sider-z-index);
|
||||
}
|
||||
|
||||
.layout-footer,
|
||||
.layout-footer-placement {
|
||||
height: var(--soy-footer-height);
|
||||
}
|
||||
|
||||
.layout-footer {
|
||||
z-index: var(--soy-footer-z-index);
|
||||
}
|
||||
|
||||
.left-gap {
|
||||
padding-left: var(--soy-sider-width);
|
||||
}
|
||||
|
||||
.left-gap_collapsed {
|
||||
padding-left: var(--soy-sider-collapsed-width);
|
||||
}
|
||||
|
||||
.sider-padding-top {
|
||||
padding-top: var(--soy-header-height);
|
||||
}
|
||||
|
||||
.sider-padding-bottom {
|
||||
padding-bottom: var(--soy-footer-height);
|
||||
}
|
18
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
18
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
declare const styles: {
|
||||
readonly 'layout-header': string;
|
||||
readonly 'layout-header-placement': string;
|
||||
readonly 'layout-tab': string;
|
||||
readonly 'layout-tab-placement': string;
|
||||
readonly 'layout-sider': string;
|
||||
readonly 'layout-mobile-sider': string;
|
||||
readonly 'layout-mobile-sider-mask': string;
|
||||
readonly 'layout-sider_collapsed': string;
|
||||
readonly 'layout-footer': string;
|
||||
readonly 'layout-footer-placement': string;
|
||||
readonly 'left-gap': string;
|
||||
readonly 'left-gap_collapsed': string;
|
||||
readonly 'sider-padding-top': string;
|
||||
readonly 'sider-padding-bottom': string;
|
||||
};
|
||||
|
||||
export default styles;
|
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import AdminLayout from './index.vue';
|
||||
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
|
||||
|
||||
export default AdminLayout;
|
||||
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
|
237
packages/materials/src/libs/admin-layout/index.vue
Normal file
237
packages/materials/src/libs/admin-layout/index.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { AdminLayoutProps } from '../../types';
|
||||
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
|
||||
import style from './index.module.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'AdminLayout'
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<AdminLayoutProps>(), {
|
||||
mode: 'vertical',
|
||||
scrollMode: 'content',
|
||||
scrollElId: LAYOUT_SCROLL_EL_ID,
|
||||
commonClass: 'transition-all-300',
|
||||
fixedTop: true,
|
||||
maxZIndex: LAYOUT_MAX_Z_INDEX,
|
||||
headerVisible: true,
|
||||
headerHeight: 56,
|
||||
tabVisible: true,
|
||||
tabHeight: 48,
|
||||
siderVisible: true,
|
||||
siderCollapse: false,
|
||||
siderWidth: 220,
|
||||
siderCollapsedWidth: 64,
|
||||
footerVisible: true,
|
||||
footerHeight: 48,
|
||||
rightFooter: false
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
/** Update siderCollapse */
|
||||
(e: 'update:siderCollapse', collapse: boolean): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/** Main */
|
||||
default?: SlotFn;
|
||||
/** Header */
|
||||
header?: SlotFn;
|
||||
/** Tab */
|
||||
tab?: SlotFn;
|
||||
/** Sider */
|
||||
sider?: SlotFn;
|
||||
/** Footer */
|
||||
footer?: SlotFn;
|
||||
};
|
||||
|
||||
const slots = defineSlots<Slots>();
|
||||
|
||||
const cssVars = computed(() => createLayoutCssVars(props));
|
||||
|
||||
// config visible
|
||||
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
|
||||
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
|
||||
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
|
||||
|
||||
// scroll mode
|
||||
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
|
||||
const isContentScroll = computed(() => props.scrollMode === 'content');
|
||||
|
||||
// layout direction
|
||||
const isVertical = computed(() => props.mode === 'vertical');
|
||||
const isHorizontal = computed(() => props.mode === 'horizontal');
|
||||
|
||||
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
|
||||
|
||||
// css
|
||||
const leftGapClass = computed(() => {
|
||||
if (!props.fullContent && showSider.value) {
|
||||
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
|
||||
|
||||
const footerLeftGapClass = computed(() => {
|
||||
const condition1 = isVertical.value;
|
||||
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
|
||||
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
|
||||
|
||||
if (condition1 || condition2 || condition3) {
|
||||
return leftGapClass.value;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const siderPaddingClass = computed(() => {
|
||||
let cls = '';
|
||||
|
||||
if (showHeader.value && !headerLeftGapClass.value) {
|
||||
cls += style['sider-padding-top'];
|
||||
}
|
||||
if (showFooter.value && !footerLeftGapClass.value) {
|
||||
cls += ` ${style['sider-padding-bottom']}`;
|
||||
}
|
||||
|
||||
return cls;
|
||||
});
|
||||
|
||||
function handleClickMask() {
|
||||
emit('update:siderCollapse', true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
|
||||
<div
|
||||
:id="isWrapperScroll ? scrollElId : undefined"
|
||||
class="h-full flex flex-col"
|
||||
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<template v-if="showHeader">
|
||||
<header
|
||||
v-show="!fullContent"
|
||||
class="flex-shrink-0"
|
||||
:class="[
|
||||
style['layout-header'],
|
||||
commonClass,
|
||||
headerClass,
|
||||
headerLeftGapClass,
|
||||
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<div
|
||||
v-show="!fullContent && fixedHeaderAndTab"
|
||||
class="flex-shrink-0 overflow-hidden"
|
||||
:class="[style['layout-header-placement']]"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Tab -->
|
||||
<template v-if="showTab">
|
||||
<div
|
||||
class="flex-shrink-0"
|
||||
:class="[
|
||||
style['layout-tab'],
|
||||
commonClass,
|
||||
tabClass,
|
||||
{ 'top-0!': fullContent || !showHeader },
|
||||
leftGapClass,
|
||||
{ 'absolute left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="tab"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-show="fullContent || fixedHeaderAndTab"
|
||||
class="flex-shrink-0 overflow-hidden"
|
||||
:class="[style['layout-tab-placement']]"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Sider -->
|
||||
<template v-if="showSider">
|
||||
<aside
|
||||
v-show="!fullContent"
|
||||
class="absolute left-0 top-0 h-full"
|
||||
:class="[
|
||||
commonClass,
|
||||
siderClass,
|
||||
siderPaddingClass,
|
||||
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
|
||||
]"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<!-- Mobile Sider -->
|
||||
<template v-if="showMobileSider">
|
||||
<aside
|
||||
class="absolute left-0 top-0 h-full w-0 bg-white"
|
||||
:class="[
|
||||
commonClass,
|
||||
mobileSiderClass,
|
||||
style['layout-mobile-sider'],
|
||||
siderCollapse ? 'overflow-hidden' : style['layout-sider']
|
||||
]"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</aside>
|
||||
<div
|
||||
v-show="!siderCollapse"
|
||||
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
|
||||
:class="[style['layout-mobile-sider-mask']]"
|
||||
@click="handleClickMask"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
:id="isContentScroll ? scrollElId : undefined"
|
||||
class="flex flex-col flex-grow"
|
||||
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<template v-if="showFooter">
|
||||
<footer
|
||||
v-show="!fullContent"
|
||||
class="flex-shrink-0"
|
||||
:class="[
|
||||
style['layout-footer'],
|
||||
commonClass,
|
||||
footerClass,
|
||||
footerLeftGapClass,
|
||||
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
|
||||
]"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
<div
|
||||
v-show="!fullContent && fixedFooter"
|
||||
class="flex-shrink-0 overflow-hidden"
|
||||
:class="[style['layout-footer-placement']]"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
68
packages/materials/src/libs/admin-layout/shared.ts
Normal file
68
packages/materials/src/libs/admin-layout/shared.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
|
||||
|
||||
/** The id of the scroll element of the layout */
|
||||
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
|
||||
|
||||
/** The max z-index of the layout */
|
||||
export const LAYOUT_MAX_Z_INDEX = 100;
|
||||
|
||||
/**
|
||||
* Create layout css vars by css vars props
|
||||
*
|
||||
* @param props Css vars props
|
||||
*/
|
||||
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
|
||||
const cssVars: LayoutCssVars = {
|
||||
'--soy-header-height': `${props.headerHeight}px`,
|
||||
'--soy-header-z-index': props.headerZIndex,
|
||||
'--soy-tab-height': `${props.tabHeight}px`,
|
||||
'--soy-tab-z-index': props.tabZIndex,
|
||||
'--soy-sider-width': `${props.siderWidth}px`,
|
||||
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
|
||||
'--soy-sider-z-index': props.siderZIndex,
|
||||
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
|
||||
'--soy-footer-height': `${props.footerHeight}px`,
|
||||
'--soy-footer-z-index': props.footerZIndex
|
||||
};
|
||||
|
||||
return cssVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create layout css vars
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export function createLayoutCssVars(props: AdminLayoutProps) {
|
||||
const {
|
||||
mode,
|
||||
isMobile,
|
||||
maxZIndex = LAYOUT_MAX_Z_INDEX,
|
||||
headerHeight,
|
||||
tabHeight,
|
||||
siderWidth,
|
||||
siderCollapsedWidth,
|
||||
footerHeight
|
||||
} = props;
|
||||
|
||||
const headerZIndex = maxZIndex - 3;
|
||||
const tabZIndex = maxZIndex - 5;
|
||||
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
|
||||
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
|
||||
const footerZIndex = maxZIndex - 5;
|
||||
|
||||
const cssProps: LayoutCssVarsProps = {
|
||||
headerHeight,
|
||||
headerZIndex,
|
||||
tabHeight,
|
||||
tabZIndex,
|
||||
siderWidth,
|
||||
siderZIndex,
|
||||
mobileSiderZIndex,
|
||||
siderCollapsedWidth,
|
||||
footerHeight,
|
||||
footerZIndex
|
||||
};
|
||||
|
||||
return createLayoutCssVarsByCssVarsProps(cssProps);
|
||||
}
|
53
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
53
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageTabProps } from '../../types';
|
||||
import style from './index.module.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'ButtonTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-1px 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>
|
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ChromeTabBg'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="size-full">
|
||||
<defs>
|
||||
<symbol id="geometry-left" viewBox="0 0 214 36">
|
||||
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
|
||||
</symbol>
|
||||
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||
<use xlink:href="#geometry-left" />
|
||||
</symbol>
|
||||
<clipPath>
|
||||
<rect width="100%" height="100%" x="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="51%" height="100%">
|
||||
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
|
||||
</svg>
|
||||
<g transform="scale(-1, 1)">
|
||||
<svg width="51%" height="100%" x="-100%" y="0">
|
||||
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
58
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
58
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageTabProps } from '../../types';
|
||||
import ChromeTabBg from './chrome-tab-bg.vue';
|
||||
import style from './index.module.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChromeTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* Slot
|
||||
*
|
||||
* The right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
|
||||
:class="[
|
||||
style['chrome-tab'],
|
||||
{ [style['chrome-tab_dark']]: darkMode },
|
||||
{ [style['chrome-tab_active']]: active },
|
||||
{ [style['chrome-tab_active_dark']]: active && darkMode }
|
||||
]"
|
||||
>
|
||||
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
|
||||
<ChromeTabBg />
|
||||
</div>
|
||||
<slot name="prefix"></slot>
|
||||
<slot></slot>
|
||||
<slot name="suffix"></slot>
|
||||
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* @type */
|
||||
|
||||
.button-tab {
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.button-tab_dark {
|
||||
border-color: #ffffff3d;
|
||||
}
|
||||
|
||||
.button-tab:hover {
|
||||
color: var(--soy-primary-color);
|
||||
border-color: var(--soy-primary-color-opacity3);
|
||||
}
|
||||
|
||||
.button-tab_active {
|
||||
color: var(--soy-primary-color);
|
||||
border-color: var(--soy-primary-color-opacity3);
|
||||
background-color: var(--soy-primary-color-opacity1);
|
||||
}
|
||||
|
||||
.button-tab_active_dark {
|
||||
background-color: var(--soy-primary-color-opacity2);
|
||||
}
|
||||
|
||||
.button-tab .svg-close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.button-tab_dark .svg-close:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.chrome-tab:hover {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.chrome-tab_active {
|
||||
z-index: 10;
|
||||
color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.chrome-tab__bg {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.chrome-tab_active .chrome-tab__bg {
|
||||
color: var(--soy-primary-color1);
|
||||
}
|
||||
|
||||
.chrome-tab_active_dark .chrome-tab__bg {
|
||||
color: var(--soy-primary-color2);
|
||||
}
|
||||
|
||||
.chrome-tab:hover .chrome-tab__bg {
|
||||
color: #dee1e6;
|
||||
}
|
||||
|
||||
.chrome-tab_active:hover .chrome-tab__bg {
|
||||
color: var(--soy-primary-color1);
|
||||
}
|
||||
|
||||
.chrome-tab_dark:hover .chrome-tab__bg {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.chrome-tab_active_dark:hover .chrome-tab__bg {
|
||||
color: var(--soy-primary-color2);
|
||||
}
|
||||
|
||||
.chrome-tab .svg-close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
.chrome-tab_active .svg-close:hover {
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.chrome-tab_dark .svg-close:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.chrome-tab_active .chrome-tab-divider {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.chrome-tab:hover .chrome-tab-divider {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.chrome-tab_dark .chrome-tab-divider {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
15
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
15
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
declare const styles: {
|
||||
readonly 'button-tab': string;
|
||||
readonly 'button-tab_dark': string;
|
||||
readonly 'button-tab_active': string;
|
||||
readonly 'button-tab_active_dark': string;
|
||||
readonly 'chrome-tab': string;
|
||||
readonly 'chrome-tab_active': string;
|
||||
readonly 'chrome-tab__bg': string;
|
||||
readonly 'chrome-tab_active_dark': string;
|
||||
readonly 'chrome-tab_dark': string;
|
||||
readonly 'chrome-tab-divider': string;
|
||||
readonly 'svg-close': string;
|
||||
};
|
||||
|
||||
export default styles;
|
3
packages/materials/src/libs/page-tab/index.ts
Normal file
3
packages/materials/src/libs/page-tab/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import PageTab from './index.vue';
|
||||
|
||||
export default PageTab;
|
97
packages/materials/src/libs/page-tab/index.vue
Normal file
97
packages/materials/src/libs/page-tab/index.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<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>();
|
||||
|
||||
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>();
|
||||
|
||||
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>
|
31
packages/materials/src/libs/page-tab/shared.ts
Normal file
31
packages/materials/src/libs/page-tab/shared.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { addColorAlpha, transformColorWithOpacity } from '@sa/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);
|
||||
}
|
31
packages/materials/src/libs/page-tab/svg-close.vue
Normal file
31
packages/materials/src/libs/page-tab/svg-close.vue
Normal 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>
|
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SimpleScrollbar from './index.vue';
|
||||
|
||||
export default SimpleScrollbar;
|
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import Simplebar from 'simplebar-vue';
|
||||
import 'simplebar-vue/dist/simplebar.min.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'SimpleScrollbar'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex-1-hidden">
|
||||
<Simplebar class="h-full">
|
||||
<slot />
|
||||
</Simplebar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
294
packages/materials/src/types/index.ts
Normal file
294
packages/materials/src/types/index.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/** Header config */
|
||||
interface AdminLayoutHeaderConfig {
|
||||
/**
|
||||
* Whether header is visible
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
headerVisible?: boolean;
|
||||
/**
|
||||
* Header class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
headerClass?: string;
|
||||
/**
|
||||
* Header height
|
||||
*
|
||||
* @default 56px
|
||||
*/
|
||||
headerHeight?: number;
|
||||
}
|
||||
|
||||
/** Tab config */
|
||||
interface AdminLayoutTabConfig {
|
||||
/**
|
||||
* Whether tab is visible
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
tabVisible?: boolean;
|
||||
/**
|
||||
* Tab class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
tabClass?: string;
|
||||
/**
|
||||
* Tab height
|
||||
*
|
||||
* @default 48px
|
||||
*/
|
||||
tabHeight?: number;
|
||||
}
|
||||
|
||||
/** Sider config */
|
||||
interface AdminLayoutSiderConfig {
|
||||
/**
|
||||
* Whether sider is visible
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
siderVisible?: boolean;
|
||||
/**
|
||||
* Sider class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
siderClass?: string;
|
||||
/**
|
||||
* Mobile sider class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
mobileSiderClass?: string;
|
||||
/**
|
||||
* Sider collapse status
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
siderCollapse?: boolean;
|
||||
/**
|
||||
* Sider width when collapse is false
|
||||
*
|
||||
* @default '220px'
|
||||
*/
|
||||
siderWidth?: number;
|
||||
/**
|
||||
* Sider width when collapse is true
|
||||
*
|
||||
* @default '64px'
|
||||
*/
|
||||
siderCollapsedWidth?: number;
|
||||
}
|
||||
|
||||
/** Content config */
|
||||
export interface AdminLayoutContentConfig {
|
||||
/**
|
||||
* Content class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
contentClass?: string;
|
||||
/**
|
||||
* Whether content is full the page
|
||||
*
|
||||
* If true, other elements will be hidden by `display: none`
|
||||
*/
|
||||
fullContent?: boolean;
|
||||
}
|
||||
|
||||
/** Footer config */
|
||||
export interface AdminLayoutFooterConfig {
|
||||
/**
|
||||
* Whether footer is visible
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
footerVisible?: boolean;
|
||||
/**
|
||||
* Whether footer is fixed
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
fixedFooter?: boolean;
|
||||
/**
|
||||
* Footer class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
footerClass?: string;
|
||||
/**
|
||||
* Footer height
|
||||
*
|
||||
* @default 48px
|
||||
*/
|
||||
footerHeight?: number;
|
||||
/**
|
||||
* Whether footer is on the right side
|
||||
*
|
||||
* When the layout is vertical, the footer is on the right side
|
||||
*/
|
||||
rightFooter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout mode
|
||||
*
|
||||
* - Horizontal
|
||||
* - Vertical
|
||||
*/
|
||||
export type LayoutMode = 'horizontal' | 'vertical';
|
||||
|
||||
/**
|
||||
* The scroll mode when content overflow
|
||||
*
|
||||
* - Wrapper: the layout component's wrapper element has a scrollbar
|
||||
* - Content: the layout component's content element has a scrollbar
|
||||
*
|
||||
* @default 'wrapper'
|
||||
*/
|
||||
export type LayoutScrollMode = 'wrapper' | 'content';
|
||||
|
||||
/** Admin layout props */
|
||||
export interface AdminLayoutProps
|
||||
extends AdminLayoutHeaderConfig,
|
||||
AdminLayoutTabConfig,
|
||||
AdminLayoutSiderConfig,
|
||||
AdminLayoutContentConfig,
|
||||
AdminLayoutFooterConfig {
|
||||
/**
|
||||
* Layout mode
|
||||
*
|
||||
* - {@link LayoutMode}
|
||||
*/
|
||||
mode?: LayoutMode;
|
||||
/** Is mobile layout */
|
||||
isMobile?: boolean;
|
||||
/**
|
||||
* Scroll mode
|
||||
*
|
||||
* - {@link ScrollMode}
|
||||
*/
|
||||
scrollMode?: LayoutScrollMode;
|
||||
/**
|
||||
* The id of the scroll element of the layout
|
||||
*
|
||||
* It can be used to get the corresponding Dom and scroll it
|
||||
*
|
||||
* @example
|
||||
* use the default id by import
|
||||
* ```ts
|
||||
* import { adminLayoutScrollElId } from '@sa/vue-materials';
|
||||
* ```
|
||||
*
|
||||
* @default
|
||||
* ```ts
|
||||
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
|
||||
* ```
|
||||
*/
|
||||
scrollElId?: string;
|
||||
/** The class of the scroll element */
|
||||
scrollElClass?: string;
|
||||
/** The class of the scroll wrapper element */
|
||||
scrollWrapperClass?: string;
|
||||
/**
|
||||
* The common class of the layout
|
||||
*
|
||||
* Is can be used to configure the transition animation
|
||||
*
|
||||
* @default 'transition-all-300'
|
||||
*/
|
||||
commonClass?: string;
|
||||
/**
|
||||
* Whether fix the header and tab
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
fixedTop?: boolean;
|
||||
/**
|
||||
* The max z-index of the layout
|
||||
*
|
||||
* The z-index of Header,Tab,Sider and Footer will not exceed this value
|
||||
*/
|
||||
maxZIndex?: number;
|
||||
}
|
||||
|
||||
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
|
||||
|
||||
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
|
||||
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
|
||||
: S;
|
||||
|
||||
type Prefix = '--soy-';
|
||||
|
||||
export type LayoutCssVarsProps = Pick<
|
||||
AdminLayoutProps,
|
||||
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
|
||||
> & {
|
||||
headerZIndex?: number;
|
||||
tabZIndex?: number;
|
||||
siderZIndex?: number;
|
||||
mobileSiderZIndex?: number;
|
||||
footerZIndex?: number;
|
||||
};
|
||||
|
||||
export type LayoutCssVars = {
|
||||
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The mode of the tab
|
||||
*
|
||||
* - Button: button style
|
||||
* - Chrome: chrome style
|
||||
*
|
||||
* @default chrome
|
||||
*/
|
||||
export type PageTabMode = 'button' | 'chrome';
|
||||
|
||||
export interface PageTabProps {
|
||||
/** Whether is dark mode */
|
||||
darkMode?: boolean;
|
||||
/**
|
||||
* The mode of the tab
|
||||
*
|
||||
* - {@link TabMode}
|
||||
*/
|
||||
mode?: PageTabMode;
|
||||
/**
|
||||
* The common class of the layout
|
||||
*
|
||||
* Is can be used to configure the transition animation
|
||||
*
|
||||
* @default 'transition-all-300'
|
||||
*/
|
||||
commonClass?: string;
|
||||
/** The class of the button tab */
|
||||
buttonClass?: string;
|
||||
/** The class of the chrome tab */
|
||||
chromeClass?: string;
|
||||
/** Whether the tab is active */
|
||||
active?: boolean;
|
||||
/** The color of the active tab */
|
||||
activeColor?: string;
|
||||
/**
|
||||
* Whether the tab is closable
|
||||
*
|
||||
* Show the close icon when true
|
||||
*/
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
export type PageTabCssVarsProps = {
|
||||
primaryColor: string;
|
||||
primaryColor1: string;
|
||||
primaryColor2: string;
|
||||
primaryColorOpacity1: string;
|
||||
primaryColorOpacity2: string;
|
||||
primaryColorOpacity3: string;
|
||||
};
|
||||
|
||||
export type PageTabCssVars = {
|
||||
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||
};
|
20
packages/materials/tsconfig.json
Normal file
20
packages/materials/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user