Compare commits

...

115 Commits

Author SHA1 Message Date
Soybean
65c21812bb chore(release): 0.9.6 2022-06-16 01:18:18 +08:00
Soybean
c3c975ee11 feat(projects): 本地svg动态渲染图标
ISSUES CLOSED: #61
2022-06-16 01:17:31 +08:00
Soybean
833018a831 build(deps): update deps 2022-06-15 23:44:46 +08:00
Soybean
3eb7f6f593 Merge pull request #102 from yanbowe/main
feat(projects): 增加设置当前Tab页签名称功能
2022-06-14 16:54:03 +08:00
燕博文
efcfa576d5 fix(projects): 设置tab标题导致meta属性丢失 2022-06-14 16:09:33 +08:00
燕博文
487213b648 feat(projects): 增加设置当前Tab页签名称功能 2022-06-14 15:42:59 +08:00
Soybean
4ee0d94f1b fix(projects): 修复顶部菜单的位置失效问题 2022-06-11 15:27:19 +08:00
Soybean
5fa822f4d4 refactor(projects): 代码优化 2022-06-11 13:37:52 +08:00
Soybean
8e6e787543 build(projects): update deps, update config 2022-06-11 13:30:25 +08:00
Soybean
9917b5e53c build(deps): update deps 2022-06-10 01:02:50 +08:00
Soybean
8f3e855f41 refactor(projects): 优化菜单支持横向滚动
ISSUES CLOSED: #76
2022-06-10 01:02:50 +08:00
tanminglin
808051b29d feat(projects): 上下结构,菜单支持横向滚动 2022-06-10 01:02:24 +08:00
Soybean
906aed5e75 build(deps): update deps 2022-06-08 00:08:16 +08:00
Soybean
2d64a2e57c feat(projects): 新增Antv G2图表示例 2022-06-07 23:58:47 +08:00
Soybean
0c70a9e083 docs(projects): update README.md 2022-06-07 23:49:02 +08:00
Soybean
08d83ecbea chore(release): 0.9.5 2022-06-07 01:55:57 +08:00
Soybean
4122685803 feat(projects): 支持同一路由根据不同query和hash同时显示不同Tab
ISSUES CLOSED: #64
2022-06-07 01:51:40 +08:00
Soybean
434ab1c560 feat(projects): 动态路由根路由重定向只需取决于后端返回的路由首页
ISSUES CLOSED: \
2022-06-06 22:47:46 +08:00
Soybean
ae99e57c52 docs(projects): update README.md 2022-06-06 18:37:27 +08:00
Soybean
e3c4a6ece6 build(deps): 依赖升级 2022-06-06 12:14:19 +08:00
Soybean
c8717c25b8 build(projects): 配置更改 2022-06-06 12:11:01 +08:00
Soybean
e9656c6e76 docs(projects): update docs 2022-06-04 12:04:44 +08:00
Soybean
fd78791229 build(projects): 配置优化 2022-06-04 11:48:24 +08:00
Soybean
de09f82586 build(projects): 代码优化
ISSUES CLOSED: \
2022-06-02 02:39:26 +08:00
Soybean
c7762490de feat(projects): 补充更多的ECharts示例
ISSUES CLOSED: \
2022-06-01 00:27:28 +08:00
Soybean
4558c24d1c fix(projects): 修复@antv/g2生产环境报错
ISSUES CLOSED: \
2022-05-31 23:02:24 +08:00
Soybean
d9ac7e4de0 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-31 00:26:52 +08:00
Soybean
6a5a357f50 build(deps): update deps
ISSUES CLOSED: \
2022-05-30 22:53:44 +08:00
Soybean
44b022aefd feat(projects): 添加antv g2图表示例
ISSUES CLOSED: \
2022-05-30 22:39:44 +08:00
Soybean
0a46ea0844 feat(projects): 添加插件页面:图表
ISSUES CLOSED: \
2022-05-29 23:44:47 +08:00
Soybean
39854a492b feat(projects): 添加百度地图、升级依赖
ISSUES CLOSED: \
2022-05-29 22:39:07 +08:00
Soybean
4c2f535a9b refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-28 20:26:29 +08:00
Soybean
be45d83766 build(deps): update deps
ISSUES CLOSED: \
2022-05-28 17:06:55 +08:00
Soybean
d28b9039bb refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-28 13:33:09 +08:00
Soybean
8f6d6ce3cb refactor(styles): 代码格式
ISSUES CLOSED: \
2022-05-28 12:30:17 +08:00
Soybean
07baac7cf8 build(other): update cz config 2022-05-27 09:53:40 +08:00
Soybean
7487ab79b3 chore(deps): update deps
ISSUES CLOSED: \
2022-05-27 00:54:13 +08:00
Soybean
a70e4161be chore(deps): update deps
ISSUES CLOSED: \
2022-05-19 23:51:28 +08:00
Soybean
095c432363 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-19 00:15:37 +08:00
Soybean
028096e53f build(deps): update deps
ISSUES CLOSED: \
2022-05-19 00:00:47 +08:00
Soybean
44ab55d594 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-18 23:43:41 +08:00
Soybean
4b80a66114 docs(projects): update README.md
ISSUES CLOSED: \
2022-05-18 23:23:04 +08:00
Soybean
cc0bb088ec Merge pull request #91 from yanbowe/main
路由meta新增activeMenu属性
2022-05-17 19:09:05 +08:00
燕博文
3e4f9e2824 fix(route): 当为左侧混合菜单时activeMenu无效情况 2022-05-17 14:06:20 +08:00
燕博文
ebd16a4d1a feat(route): 路由meta新增activeMenu属性 2022-05-17 13:54:48 +08:00
Soybean
84cb07baec docs(projects): update README.md
ISSUES CLOSED: \
2022-05-16 22:17:01 +08:00
Soybean
0811ffa5ae docs(projects): update README.md 2022-05-16 20:09:07 +08:00
Soybean
3f822a7d76 build(deps): update deps
ISSUES CLOSED: \
2022-05-14 00:57:23 +08:00
Soybean
a1c7e10574 refactor(projects): 代码优化 2022-05-11 00:18:36 +08:00
Soybean
50d7ccd82d build(deps): update deps 2022-05-10 23:53:56 +08:00
Soybean
e0233061d3 fix(projects): 修复页面切换时导致的溢出滚动条 2022-05-10 23:22:24 +08:00
Soybean
a0c405dadd build(projects): update config 2022-05-10 22:07:53 +08:00
Soybean
60f912508b fix(projects): 修复权限切换路由数据未更新的问题 2022-05-09 23:53:09 +08:00
Soybean
3590b65e22 refactor(projects): 代码优化 2022-05-09 20:52:43 +08:00
Soybean
92b8406444 build(deps): update deps 2022-05-07 01:01:46 +08:00
Soybean
e7ad08685e feat(projects): 引入echarts替换antvG2plot 2022-05-07 00:58:24 +08:00
Soybean
14c145eef1 refactor(projects): 代码优化 2022-05-05 18:42:29 +08:00
Soybean
518f7eed28 build(deps): update deps 2022-05-05 13:11:16 +08:00
Soybean
21e63998d0 docs(projects): update README.md 2022-05-05 12:50:14 +08:00
Soybean
38ee2a62cd Merge pull request #86 from dxxzst/patch-1
fix(projects): 修复插件不存在的错误提示
2022-05-04 21:14:17 +08:00
Grazing Wind
716528206e fix(projects): 修复插件不存在的错误提示
已经改成小写
2022-05-04 09:35:19 +08:00
Soybean
b81143e55e Merge pull request #84 from tclyjy/main
refactor(layouts): layout/header 反转色样式补充
2022-04-29 20:59:59 +08:00
Eric_Yuan
0243b27505 style(GlobalBreadcrumb): 代码格式fix 2022-04-29 19:19:10 +08:00
Soybean
909c12d3c6 Merge pull request #83 from toolvcn/相思
feat(projects): 添加自动跟随系统主题设置
2022-04-29 17:56:27 +08:00
元家怿
01d0bcbfd0 refactor(layouts): layout/header 反转色样式补充 2022-04-29 15:53:12 +08:00
相思
ba07b695dd feat(projects): 添加自动跟随系统主题设置 2022-04-29 15:02:51 +08:00
Soybean
3d8befa376 docs(projects): update README.md 2022-04-29 02:05:00 +08:00
Soybean
97c92626cc chore(release): 0.9.4 2022-04-29 02:02:52 +08:00
Soybean
55ddc9cab0 refactor(projects): 动态路由权限完善 2022-04-29 02:00:51 +08:00
Soybean
401f0c748d build(projects): 细节调整 2022-04-28 01:02:41 +08:00
Soybean
d5c751153c docs(projects): update README.md 2022-04-28 00:54:40 +08:00
Soybean
69d51318ff refactor(projects): merge branch unocss to main 2022-04-28 00:50:22 +08:00
Soybean
de5fb84215 refactor(projects): layout和tab组件依赖名称变更、样式修复 2022-04-27 22:51:28 +08:00
Soybean
c275f2632c refactor(projects): 细节优化 2022-04-27 22:27:46 +08:00
Soybean
e899914426 fix(projects): 修复样式 2022-04-27 19:31:40 +08:00
元家怿
861c8b9852 feat(layouts): 添加侧边栏/头部的反转模式来增加对比度 2022-04-27 19:15:23 +08:00
Soybean
a782461453 refactor(projects): 代码优化 2022-04-27 19:05:58 +08:00
Soybean
e8488e4d52 fix(projects): 添加.npmrc修复无法获取自动引入的全局组件声明类型 2022-04-27 18:08:39 +08:00
Soybean
889c859865 Merge pull request #82 from tclyjy/main
feat(layouts): 添加侧边栏/头部的反转模式来增加对比度
2022-04-27 16:46:09 +08:00
元家怿
3c8dd772f8 feat(layouts): 添加侧边栏/头部的反转模式来增加对比度 2022-04-27 16:39:20 +08:00
Soybean
251b5b9664 refactor(projects): 代码优化 2022-04-27 07:47:26 +08:00
Soybean
41e46a5d80 refactor(projects): mock权限相关数据优化 2022-04-26 00:12:20 +08:00
Soybean
5c75e9d958 build(deps): update deps 2022-04-24 00:23:45 +08:00
Soybean
7f4350aeb6 feat(projects): mock添加权限过滤 2022-04-23 12:31:06 +08:00
Soybean
807448aec5 feat(projects): 权限完善及权限示例页面 2022-04-23 02:21:02 +08:00
相思
b9c5c34979 feat(projects): HTML lang 修改为 zh-cmn-Hans 2022-04-22 13:35:22 +08:00
Soybean
20347b7d65 Merge pull request #79 from toolvcn/相思
feat(projects): HTML lang 修改为 zh-cmn-Hans
2022-04-22 13:31:11 +08:00
相思
dbeb595c0b feat(projects): HTML lang 修改为 zh-cmn-Hans 2022-04-22 13:00:50 +08:00
Soybean
c9d3e5a3fd feat(projects): 引入unocss替换windicss 2022-04-22 12:29:22 +08:00
Soybean
5e276421ad refactor(projects): 代码优化 2022-04-22 09:07:53 +08:00
Soybean
219f87f467 fix(projects): 添加获取路由组件文件未找到时的错误提示 2022-04-21 09:04:18 +08:00
Soybean
b35ed8960d refactor(projects): 去除在pinia的getters的函数调用副作用,用watch代替 2022-04-21 00:46:03 +08:00
Soybean
24010d05fb feat(projects): 登录页背景图片位置适配移动端 2022-04-21 00:26:21 +08:00
Soybean
ec0776e268 feat(projects): 登录页面适配移动端 2022-04-21 00:15:34 +08:00
Soybean
cecce83bc3 build(deps): update deps 2022-04-18 23:47:38 +08:00
Soybean
4eb46ea3dd chore(deps): update deps 2022-04-14 00:02:19 +08:00
Soybean
e8b534b84e refactor(projects): 代码优化 2022-04-13 23:45:15 +08:00
Soybean
46e1ae7825 fix(projects): 修复获取vite环境变量的方式 2022-04-07 22:48:52 +08:00
Soybean
60a55a776e docs(projects): update README.md 2022-04-06 14:34:52 +08:00
Soybean
bed4292ed3 feat(projects): 添加请求适配器的请求示例 2022-04-04 19:13:15 +08:00
Soybean
6bed9ead38 feat(projects): 插件方式按需引入naiveUI 2022-04-04 17:41:55 +08:00
Soybean
3fb13ca9e7 fix(projects): 修复在新版vite下环境变量获取不到的问题 2022-04-04 17:30:26 +08:00
Soybean
2d6d179d66 fix(projects): 去除从环境文件引入端口号导致的错误 2022-04-03 01:07:25 +08:00
Soybean
eebb753884 Merge pull request #70 from yanbowe/main
fix(projects): 全局搜索弹窗弹出时动画闪屏问题
2022-04-02 14:13:39 +08:00
yanbowen
bb1bbf2724 fix(projects): 全局搜索弹窗弹出时动画闪屏问题 2022-04-02 10:35:21 +08:00
Soybean
df56abe18d style(projects): update prettier config 2022-04-01 14:47:57 +08:00
Soybean
ca2dfa6185 feat(projects): 新增静态路由 2022-03-30 01:30:12 +08:00
Soybean
bbfdcc8276 Merge pull request #67 from Southliu/main
perf: refresh-koken命名
2022-03-24 15:59:00 +08:00
“Southliu”
1715504789 perf: refresh-koken命名 2022-03-24 11:49:07 +08:00
Soybean
9a90f18e77 docs(projects): update README.md 2022-03-23 15:21:09 +08:00
Soybean
21645537d5 docs(projects): update README.md 2022-03-15 01:34:31 +08:00
Soybean
e6c26fcb4a fix(projects): 修复路由守卫的动态路由逻辑 2022-03-14 15:16:28 +08:00
Soybean
20911dd882 refactor(projects): lint命令修改 2022-03-13 19:42:01 +08:00
Soybean
cd7ca8f4c7 fix(projects): 修复vite alias 2022-03-13 18:37:44 +08:00
Soybean
ca707a456b build(projects): vite.config代码优化 2022-03-13 18:14:27 +08:00
265 changed files with 11325 additions and 7885 deletions

6
.env
View File

@@ -6,4 +6,8 @@ VITE_APP_TITLE=Soybean管理系统
VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版 VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
VITE_HTTP_PROXY=true # 权限路由模式: static dynamic
VITE_AUTH_ROUTE_MODE=dynamic
# 路由首页(根路由重定向), 用于static模式的权限路由dynamic模式取决于后端返回的路由首页
VITE_ROUTE_HOME_PATH=/dashboard/analysis

View File

@@ -1,28 +1,20 @@
/** 请求环境配置 */ /** 请求环境配置 */
type ServiceEnv = Record< type ServiceEnv = Record<EnvType, EnvConfig>;
EnvType,
{
/** 请求地址 */
url: string;
/** 代理地址 */
proxy: string;
}
>;
/** 请求的环境 */ /** 环境配置 */
const serviceEnvConfig: ServiceEnv = { const serviceEnvConfig: ServiceEnv = {
dev: { dev: {
url: 'http://localhost:8080', url: 'http://localhost:8080',
proxy: '/api', proxy: '/api'
}, },
test: { test: {
url: 'http://localhost:8080', url: 'http://localhost:8080',
proxy: '/api', proxy: '/api'
}, },
prod: { prod: {
url: 'http://localhost:8080', url: 'http://localhost:8080',
proxy: '/api', proxy: '/api'
}, }
}; };
/** /**
@@ -31,8 +23,8 @@ const serviceEnvConfig: ServiceEnv = {
*/ */
export function getEnvConfig(env: ImportMetaEnv) { export function getEnvConfig(env: ImportMetaEnv) {
const { VITE_ENV_TYPE = 'dev' } = env; const { VITE_ENV_TYPE = 'dev' } = env;
const envConfig = {
http: serviceEnvConfig[VITE_ENV_TYPE], const envConfig = serviceEnvConfig[VITE_ENV_TYPE];
};
return envConfig; return envConfig;
} }

View File

@@ -1,2 +1 @@
# 是否开启打包文件大小结果分析 VITE_HTTP_PROXY=true
VITE_VISUALIZER=false

View File

@@ -0,0 +1,6 @@
VITE_VISUALIZER=false
VITE_COMPRESS=false
# gzip | brotliCompress | deflate | deflateRaw
VITE_COMPRESS_TYPE=gzip

View File

@@ -13,3 +13,4 @@ lib
.local .local
package.json package.json
!.env-config.ts !.env-config.ts
components.d.ts

View File

@@ -2,19 +2,19 @@ module.exports = {
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true,
'vue/setup-compiler-macros': true, 'vue/setup-compiler-macros': true
}, },
globals: { globals: {
PROJECT_BUILD_TIME: 'readonly', PROJECT_BUILD_TIME: 'readonly',
AMap: 'readonly', AMap: 'readonly',
BMap: 'readonly', BMap: 'readonly',
TMap: 'readonly', TMap: 'readonly'
}, },
parser: 'vue-eslint-parser', parser: 'vue-eslint-parser',
parserOptions: { parserOptions: {
ecmaVersion: 12, ecmaVersion: 12,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
sourceType: 'module', sourceType: 'module'
}, },
plugins: ['vue', '@typescript-eslint'], plugins: ['vue', '@typescript-eslint'],
extends: [ extends: [
@@ -24,11 +24,49 @@ module.exports = {
'plugin:prettier/recommended', 'plugin:prettier/recommended',
'@vue/eslint-config-typescript/recommended', '@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier', '@vue/eslint-config-prettier',
'@vue/typescript/recommended', '@vue/typescript/recommended'
], ],
overrides: [
{
files: ['*.vue'],
rules: {
'no-undef': 'off'
}
},
{
files: ['*.html'],
rules: {
'vue/comment-directive': 'off'
}
}
],
settings: {
'import/resolver': {
alias: {
map: [
['~', '.'],
['@', './src']
],
extensions: ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.d.ts']
},
node: {
extensions: ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.d.ts']
}
}
},
rules: { rules: {
'import/extensions': 'off', 'import/extensions': [
'import/no-extraneous-dependencies': 'off', 'warn',
'ignorePackages',
{
js: 'never',
jsx: 'never',
mjs: 'never',
ts: 'never',
tsx: 'never'
}
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true, peerDependencies: true }],
'import/order': [ 'import/order': [
'error', 'error',
{ {
@@ -38,174 +76,141 @@ module.exports = {
{ {
pattern: 'vue', pattern: 'vue',
group: 'external', group: 'external',
position: 'before', position: 'before'
}, },
{ {
pattern: 'vue-router', pattern: 'vue-router',
group: 'external', group: 'external',
position: 'before', position: 'before'
}, },
{ {
pattern: 'vuex', pattern: 'vuex',
group: 'external', group: 'external',
position: 'before', position: 'before'
}, },
{ {
pattern: 'pinia', pattern: 'pinia',
group: 'external', group: 'external',
position: 'before', position: 'before'
},
{
pattern: 'naive-ui',
group: 'external',
position: 'before'
}, },
// ui framework, such as "naive-ui"
// {
// pattern: 'naive-ui',
// group: 'external',
// position: 'before'
// },
{ {
pattern: '@/config', pattern: '@/config',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/settings', pattern: '@/settings',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/enum', pattern: '@/enum',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/plugins', pattern: '@/plugins',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/layouts', pattern: '@/layouts',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/views', pattern: '@/views',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/components', pattern: '@/components',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/router', pattern: '@/router',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/store', pattern: '@/store',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/composables', pattern: '@/composables',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/hooks', pattern: '@/hooks',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/service', pattern: '@/service',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/utils', pattern: '@/utils',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/assets', pattern: '@/assets',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/**', pattern: '@/**',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, },
{ {
pattern: '@/interface', pattern: '@/interface',
group: 'internal', group: 'internal',
position: 'before', position: 'before'
}, }
], ],
pathGroupsExcludedImportTypes: [ pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'vuex', 'pinia', 'naive-ui']
'vue', }
'vue-router',
'vuex',
'pinia',
// 'naive-ui'
],
},
], ],
'import/no-unresolved': 'off', 'import/no-unresolved': ['error', { ignore: ['uno.css', '~icons/*', 'virtual:svg-icons-register'] }],
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
'max-classes-per-file': 'off', 'max-classes-per-file': 'off',
'no-param-reassign': [ 'no-param-reassign': [
'error', 'error',
{ {
props: true, props: true,
ignorePropertyModificationsFor: ['state', 'acc', 'e'], ignorePropertyModificationsFor: ['state', 'acc', 'e']
}, }
], ],
'no-plusplus': 'off',
'no-shadow': 'off', 'no-shadow': 'off',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'no-use-before-define': 'off', 'no-use-before-define': 'off',
'vue/multi-word-component-names': [ 'vue/multi-word-component-names': [
'error', 'error',
{ {
ignores: ['index'], ignores: ['index']
}, }
],
'@typescript-eslint/ban-types': [
'error',
{
types: {
'{}': {
message: 'Use object instead',
fixWith: 'object',
},
},
},
], ],
'@typescript-eslint/no-empty-interface': [ '@typescript-eslint/no-empty-interface': [
'error', 'error',
{ {
allowSingleExtends: true, allowSingleExtends: true
}, }
], ],
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true, varsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true, varsIgnorePattern: '^_' }],
'@typescript-eslint/no-use-before-define': ['error', { classes: true, functions: false, typedefs: false }], '@typescript-eslint/no-use-before-define': ['error', { classes: true, functions: false, typedefs: false }]
}, }
overrides: [
{
files: ['*.vue'],
rules: {
'no-undef': 'off',
},
},
{
files: ['*.html'],
rules: {
'vue/comment-directive': 'off',
},
},
],
}; };

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ stats.html
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/src/typings/components.d.ts

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
registry=https://registry.npmmirror.com/
shamefully-hoist=true
strict-peer-dependencies=false

View File

@@ -1,5 +1,5 @@
module.exports = { // https://prettier.io/docs/en/options.html module.exports = { // https://prettier.io/docs/en/options.html
arrowParens: 'always', arrowParens: 'avoid',
bracketSameLine: false, bracketSameLine: false,
bracketSpacing: true, bracketSpacing: true,
embeddedLanguageFormatting: 'auto', embeddedLanguageFormatting: 'auto',
@@ -13,7 +13,7 @@ module.exports = { // https://prettier.io/docs/en/options.html
semi: true, semi: true,
singleQuote: true, singleQuote: true,
tabWidth: 2, tabWidth: 2,
trailingComma: 'es5', trailingComma: 'none',
useTabs: false, useTabs: false,
vueIndentScriptAndStyle: false, vueIndentScriptAndStyle: false,
overrides: [ overrides: [

View File

@@ -1,36 +1,26 @@
{ {
"recommendations": [ "recommendations": [
"formulahendry.auto-close-tag",
"formulahendry.auto-complete-tag",
"steoates.autoimport",
"formulahendry.auto-rename-tag",
"coenraads.bracket-pair-colorizer-2",
"naumovs.color-highlight",
"pranaygp.vscode-css-peek",
"mikestead.dotenv",
"editorconfig.editorconfig",
"dsznajder.es7-react-js-snippets",
"dbaeumer.vscode-eslint",
"miguelsolorio.fluent-icons",
"mhutchie.git-graph",
"eamodio.gitlens",
"lokalise.i18n-ally",
"afzalsayed96.icones", "afzalsayed96.icones",
"antfu.iconify", "antfu.iconify",
"kisstkondoros.vscode-gutter-preview", "antfu.unocss",
"xabikos.javascriptsnippets",
"whtouche.vscode-js-console-utils",
"ritwickdey.liveserver",
"yzhang.markdown-all-in-one",
"pkief.material-icon-theme",
"zhuangtongfa.material-theme",
"jimdong.naive-ui-snippets",
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"johnsoncodehk.volar", "formulahendry.auto-complete-tag",
"johnsoncodehk.vscode-typescript-vue-plugin", "formulahendry.auto-close-tag",
"dariofuzinato.vue-peek", "formulahendry.auto-rename-tag",
"wscats.vue", "kisstkondoros.vscode-gutter-preview",
"voorjaar.windicss-intellisense" "lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",
"naumovs.color-highlight",
"pkief.material-icon-theme",
"steoates.autoimport",
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"whtouche.vscode-js-console-utils",
"zhuangtongfa.material-theme"
] ]
} }

101
.vscode/settings.json vendored
View File

@@ -1,63 +1,22 @@
{ {
"editor.quickSuggestions": {
"strings": true
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "One Dark Pro",
"editor.tabSize": 2,
"editor.fontLigatures": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"editor.bracketPairColorization.enabled": true, "editor.fontLigatures": true,
"editor.formatOnSave": false,
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
"git.enableSmartCommit": true, "editor.quickSuggestions": {
"path-intellisense.mappings": { "strings": true
"@": "${workspaceFolder}/src",
"~@": "${workspaceFolder}/src",
}, },
"editor.tabSize": 2,
"files.associations": {
"*.env.*": "dotenv"
},
"git.enableSmartCommit": true,
"gutterpreview.paths": { "gutterpreview.paths": {
"@": "/src", "@": "/src",
"~@": "/src" "~@": "/src"
}, },
"terminal.integrated.cursorStyle": "line",
"files.associations": {
"*.env.*": "dotenv"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"javascript.updateImportsOnFileMove.enabled": "always",
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"terminal.integrated.fontSize": 14,
"terminal.integrated.fontWeight": 500,
"i18n-ally.displayLanguage": "zh",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "johnsoncodehk.volar"
},
"terminal.integrated.tabs.enabled": true,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"editor.formatOnSave": false,
"material-icon-theme.activeIconPack": "angular", "material-icon-theme.activeIconPack": "angular",
"material-icon-theme.files.associations": {}, "material-icon-theme.files.associations": {},
"material-icon-theme.folders.associations": { "material-icon-theme.folders.associations": {
@@ -69,6 +28,46 @@
"composables": "hook", "composables": "hook",
"directive": "tools", "directive": "tools",
"directives": "tools", "directives": "tools",
"business": "core" "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,
"unocss.root": "src",
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "One Dark Pro",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[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"
} }
} }

View File

@@ -2,6 +2,77 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.9.6](https://github.com/honghuangdc/soybean-admin/compare/v0.9.5...v0.9.6) (2022-06-15)
### Features
* **projects:** 本地svg动态渲染图标 ([c3c975e](https://github.com/honghuangdc/soybean-admin/commit/c3c975ee1142987b7ded0107bf91d0080d5651fe)), closes [#61](https://github.com/honghuangdc/soybean-admin/issues/61)
* **projects:** 上下结构,菜单支持横向滚动 ([808051b](https://github.com/honghuangdc/soybean-admin/commit/808051b29dd682e1cbcf0e211774efb9cc12713a))
* **projects:** 新增Antv G2图表示例 ([2d64a2e](https://github.com/honghuangdc/soybean-admin/commit/2d64a2e57c8d83c8d06f210eeefef8f31b3abeb9))
* **projects:** 增加设置当前Tab页签名称功能 ([487213b](https://github.com/honghuangdc/soybean-admin/commit/487213b64853765e2bd186474e4607572624a33e))
### Bug Fixes
* **projects:** 设置tab标题导致meta属性丢失 ([efcfa57](https://github.com/honghuangdc/soybean-admin/commit/efcfa576d52a7eab644f3b4c65af153442887fab))
* **projects:** 修复顶部菜单的位置失效问题 ([4ee0d94](https://github.com/honghuangdc/soybean-admin/commit/4ee0d94f1bde83c788fc0dcb084402359c04fb1b))
### [0.9.5](https://github.com/honghuangdc/soybean-admin/compare/v0.9.4...v0.9.5) (2022-06-06)
### Features
* **projects:** 支持同一路由根据不同query和hash同时显示不同Tab ([4122685](https://github.com/honghuangdc/soybean-admin/commit/4122685803f8a0a485682d16cec74e27945adc47)), closes [#64](https://github.com/honghuangdc/soybean-admin/issues/64)
* **projects:** 动态路由根路由重定向只需取决于后端返回的路由首页 ([434ab1c](https://github.com/honghuangdc/soybean-admin/commit/434ab1c560b260f8a19895405eb1d3c3313052d7))
* **projects:** 补充更多的ECharts示例 ([c776249](https://github.com/honghuangdc/soybean-admin/commit/c7762490def77695bedf179ffc63e3e95d15e14d))
* **projects:** 添加百度地图、升级依赖 ([39854a4](https://github.com/honghuangdc/soybean-admin/commit/39854a492b9cce71e0c7ed52af9985cb4abd6a97))
* **projects:** 添加插件页面:图表 ([0a46ea0](https://github.com/honghuangdc/soybean-admin/commit/0a46ea08443f6b879434e925d440cf07e9494fcb))
* **projects:** 添加自动跟随系统主题设置 ([ba07b69](https://github.com/honghuangdc/soybean-admin/commit/ba07b695dd9dc5d3f8ebf57d0f2e69d624994962))
* **projects:** 添加antv g2图表示例 ([44b022a](https://github.com/honghuangdc/soybean-admin/commit/44b022aefd7dbb4c34886814cf04767450dec026))
* **projects:** 引入echarts替换antvG2plot ([e7ad086](https://github.com/honghuangdc/soybean-admin/commit/e7ad08685e8ac52a8906fc94e656192275f9764c))
* **route:** 路由meta新增activeMenu属性 ([ebd16a4](https://github.com/honghuangdc/soybean-admin/commit/ebd16a4d1ab1a95a27838a2d4f20cc1d1e7309ae))
### Bug Fixes
* **projects:** 修复@antv/g2生产环境报错 ([4558c24](https://github.com/honghuangdc/soybean-admin/commit/4558c24d1c1e1faa3326650fc16e6baf384509ac))
* **projects:** 修复插件不存在的错误提示 ([7165282](https://github.com/honghuangdc/soybean-admin/commit/716528206e9f63e873607d0afd59d83f6984e3fe))
* **projects:** 修复权限切换路由数据未更新的问题 ([60f9125](https://github.com/honghuangdc/soybean-admin/commit/60f912508b0e685957fb22ef0ed1f83272847263))
* **projects:** 修复页面切换时导致的溢出滚动条 ([e023306](https://github.com/honghuangdc/soybean-admin/commit/e0233061d3bca236b4c4bb462ce00f7ca186b9fa))
* **route:** 当为左侧混合菜单时activeMenu无效情况 ([3e4f9e2](https://github.com/honghuangdc/soybean-admin/commit/3e4f9e282442073447c5c24c33d65bc6130978ee))
### [0.9.4](https://github.com/honghuangdc/soybean-admin/compare/v0.9.3...v0.9.4) (2022-04-28)
### Features
* **layouts:** 添加侧边栏/头部的反转模式来增加对比度 ([861c8b9](https://github.com/honghuangdc/soybean-admin/commit/861c8b9852e0097a1f6b79ac2c10d19add123bde))
* **layouts:** 添加侧边栏/头部的反转模式来增加对比度 ([3c8dd77](https://github.com/honghuangdc/soybean-admin/commit/3c8dd772f89d2b656a42c4f7164e581acdb2b1a5))
* **projects:** 插件方式按需引入naiveUI ([6bed9ea](https://github.com/honghuangdc/soybean-admin/commit/6bed9ead38af6d58f6cd9e520db848ae5cbfa4db))
* **projects:** 登录页背景图片位置适配移动端 ([24010d0](https://github.com/honghuangdc/soybean-admin/commit/24010d05fb1ff51cb5e5d94ffe310206a9638711))
* **projects:** 登录页面适配移动端 ([ec0776e](https://github.com/honghuangdc/soybean-admin/commit/ec0776e268cd3d1031e9ecd794abce271a675793))
* **projects:** 权限完善及权限示例页面 ([807448a](https://github.com/honghuangdc/soybean-admin/commit/807448aec5b041535fe4fbac90eca1138b2f439c))
* **projects:** 添加请求适配器的请求示例 ([bed4292](https://github.com/honghuangdc/soybean-admin/commit/bed4292ed380e77ac428ab057abc42eceb72af53))
* **projects:** 新增静态路由 ([ca2dfa6](https://github.com/honghuangdc/soybean-admin/commit/ca2dfa6185aa7a4e58184bcfef2a1246a52f88fd))
* **projects:** 引入unocss替换windicss ([c9d3e5a](https://github.com/honghuangdc/soybean-admin/commit/c9d3e5a3fdf59179dcfc122ab8369c492ea7832e))
* **projects:** HTML lang 修改为 zh-cmn-Hans ([b9c5c34](https://github.com/honghuangdc/soybean-admin/commit/b9c5c349790b1e83a7acd1f2c53a86c9221944ff))
* **projects:** HTML lang 修改为 zh-cmn-Hans ([dbeb595](https://github.com/honghuangdc/soybean-admin/commit/dbeb595c0b9fc11e7d166a7684af37cc971f1a11))
* **projects:** mock添加权限过滤 ([7f4350a](https://github.com/honghuangdc/soybean-admin/commit/7f4350aeb673dab59192584177a897aacebe4b28))
### Bug Fixes
* **projects:** 去除从环境文件引入端口号导致的错误 ([2d6d179](https://github.com/honghuangdc/soybean-admin/commit/2d6d179d669ea71cca3fe97ac840e4856bff4051))
* **projects:** 全局搜索弹窗弹出时动画闪屏问题 ([bb1bbf2](https://github.com/honghuangdc/soybean-admin/commit/bb1bbf272438f4ed440735118c6a9ec04c7d109f))
* **projects:** 添加.npmrc修复无法获取自动引入的全局组件声明类型 ([e8488e4](https://github.com/honghuangdc/soybean-admin/commit/e8488e4d5237e5e03ec07ff07d03115389d5b1ef))
* **projects:** 添加获取路由组件文件未找到时的错误提示 ([219f87f](https://github.com/honghuangdc/soybean-admin/commit/219f87f46758f328f26697f66d8583f49c0d41de))
* **projects:** 修复获取vite环境变量的方式 ([46e1ae7](https://github.com/honghuangdc/soybean-admin/commit/46e1ae7825b2b204ce3cdd63b3c64f39bff096d0))
* **projects:** 修复路由守卫的动态路由逻辑 ([e6c26fc](https://github.com/honghuangdc/soybean-admin/commit/e6c26fcb4ae085f9fd7d7eb9183ddba020d0b5da))
* **projects:** 修复样式 ([e899914](https://github.com/honghuangdc/soybean-admin/commit/e8999144266761b3b701442975c3c00251240d53))
* **projects:** 修复在新版vite下环境变量获取不到的问题 ([3fb13ca](https://github.com/honghuangdc/soybean-admin/commit/3fb13ca9e710549d2ddeb774fe08fabd27d5ae11))
* **projects:** 修复vite alias ([cd7ca8f](https://github.com/honghuangdc/soybean-admin/commit/cd7ca8f4c77ac8c753b753ba698a9573d6c37bf9))
### [0.9.3](https://github.com/honghuangdc/soybean-admin/compare/v0.9.2...v0.9.3) (2022-03-12) ### [0.9.3](https://github.com/honghuangdc/soybean-admin/compare/v0.9.2...v0.9.3) (2022-03-12)

View File

@@ -7,16 +7,16 @@
## 简介 ## 简介
Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版它使用了最新的前端技术栈内置丰富的主题配置有着极高的代码规范基于mock实现的动态权限路由开箱即用的中后台前端解决方案也可用于学习参考。 Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版它使用了最新的前端技术栈内置丰富的主题配置有着极高的代码规范基于mock实现的动态权限路由开箱即用的中后台前端解决方案也可用于学习参考。
## 特性 ## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm - **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm
- **TypeScript**: 应用程序级 JavaScript 的语言 - **TypeScript**应用程序级 JavaScript 的语言
- **主题**:丰富可配置的主题、暗黑模式,基于windicss的动态主题颜色 - **主题**:丰富可配置的主题、暗黑模式,基于原子css - unocss的动态主题颜色
- **代码规范**:丰富的规范插件及极高的代码规范 - **代码规范**:丰富的规范插件及极高的代码规范
- **权限路由**简易的路由配置、基于mock的动态路由能快速实现后端动态路由 - **权限路由**简易的路由配置、基于mock的动态路由能快速实现后端动态路由
- **请求函数**基于axios的完善的请求函数封装提供Promise和hooks两种请求函数 - **请求函数**基于axios的完善的请求函数封装提供Promise和hooks两种请求函数,加入请求结果数据转换的适配器
## 预览 ## 预览
@@ -32,34 +32,65 @@ Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中
- [gitee](https://gitee.com/honghuangdc/soybean-admin) - [gitee](https://gitee.com/honghuangdc/soybean-admin)
## 更新日志
[CHANGELOG](./CHANGELOG.md)
## 后端服务
- [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)
## 项目示例图 ## 项目示例图
![](https://s2.loli.net/2022/01/24/ovK6Oyqr7gIMu2n.png) ![](https://s2.loli.net/2022/05/16/keOtgFH27r9nqYS.png)
![](https://s2.loli.net/2022/01/24/O8loxYhMySHwGfJ.png) ![](https://s2.loli.net/2022/05/18/bW7mftiQexkvSTG.png)
![](https://s2.loli.net/2022/01/24/HKwpJ7Ab6j8fVvk.png) ![](https://s2.loli.net/2022/05/16/uV5nzjb3gYptAEl.png)
![](https://s2.loli.net/2022/05/16/rSnNHLdpuvkKxWq.png)
![](https://s2.loli.net/2022/05/18/Mt6YZqmDxO8v4uR.png)
![](https://s2.loli.net/2022/05/16/ktH5dcG3fuFOoKA.png)
![](https://s2.loli.net/2022/05/16/VPl6Ru1iCAhLcS4.png)
![](https://s2.loli.net/2022/05/16/bRlAKuHW7ZVh9DT.png)
![](https://s2.loli.net/2022/06/07/rY8TyAftM5dxspv.png)
![](https://s2.loli.net/2022/06/07/5GNBAd31IzQVjLP.png)
![](https://s2.loli.net/2022/06/07/rRSG6mEZpujOACT.png)
![](https://s2.loli.net/2022/01/24/bqJRSDZHBv3jsif.png)
![](https://s2.loli.net/2022/01/24/wXpHeau6UrSTWdF.png)
![](https://s2.loli.net/2022/02/16/pBwF2gaxXnKZe3D.png)
![](https://s2.loli.net/2022/02/16/pfuxVEPsTJIXw5n.png)
## 开发计划 ## 开发计划
- [x] 引入ECharts替换AntV G2Plot
- [x] 图表示例ECharts、AntV G2
- [x] 多页签支持query、hash等参数同一页面支持多个Tab
- [ ] 缓存主题配置
- [ ] 添加锁屏组件、全局Iframe组件
- [ ] 示例页面完善 - [ ] 示例页面完善
- [ ] 表单、表格示例 - [ ] 表单、表格示例
- [ ] 添加锁屏组件、全局Iframe组件
- [ ] 用户角色切换示例、按钮级别权限指令
- [ ] 性能优化(优化递归函数) - [ ] 性能优化(优化递归函数)
- [ ] 精简版(新分支thin)
- [ ] 文档完善
- [ ] i18n国际化
- [ ] element-plus版本 - [ ] element-plus版本
- [ ] 其他UI版本 - [ ] 其他UI版本
- [ ] soybean-admin cli工具(选择不同UI) - [ ] soybean-admin cli工具(选择不同UI)
- [ ] soybean-admin 后台服务java版: [soybean-admin-java](https://github.com/honghuangdc/soybean-admin-java)
- [ ] soybean-admin 后台服务go版: [soybean-admin-go](https://github.com/honghuangdc/soybean-admin-go)
- [ ] soybean-admin 后台服务nodejs版: [soybean-admin-nestjs](https://github.com/honghuangdc/soybean-admin-nestjs)
- [ ] 前端可视化创建路由页面 - [ ] 前端可视化创建路由页面
- [ ] soybean-admin 后台nodejs服务
## 安装使用 ## 安装使用
@@ -87,6 +118,9 @@ pnpm dev
pnpm build pnpm build
``` ```
**本地环境需要安装 pnpm 6.x 、Node.js 14.x 和 Git**
## 如何贡献 ## 如何贡献
非常欢迎您的加入![提一个 Issue](https://github.com/honghuangdc/soybean-admin/issues/new) 或者提交一个 Pull Request。 非常欢迎您的加入![提一个 Issue](https://github.com/honghuangdc/soybean-admin/issues/new) 或者提交一个 Pull Request。
@@ -115,13 +149,18 @@ pnpm i -g commitizen
[@Soybean](https://github.com/honghuangdc) [@Soybean](https://github.com/honghuangdc)
## 捐赠
如果你觉得这个项目对你有帮助可以请Soybean喝杯饮料表示支持Soybean开源的动力离不开各位的支持和鼓励。
![赞助](https://s2.loli.net/2022/01/24/i9cpq7lTCrKUoFf.png)
## 交流 ## 交流
`Soybean Admin` 是完全开源免费的项目在帮助开发者更方便地进行中大型管理系统开发同时也提供微信和QQ交流群使用问题欢迎在群内提问。 `Soybean Admin` 是完全开源免费的项目在帮助开发者更方便地进行中大型管理系统开发同时也提供微信和QQ交流群使用问题欢迎在群内提问。
- 微信交流群 - 微信交流群(添加本人微信拉进群),欢迎来技术交流,业务咨询。
<div style="text-align:left"> <div style="text-align:left">
<img src="https://s2.loli.net/2022/03/06/4wokvQ7R5B62Ei1.jpg" style="width:200px" /> <img src="https://s2.loli.net/2022/05/16/3YGBgXnVPJdslk8.jpg" style="width:200px" />
</div> </div>
- QQ交流群 `711301266` - QQ交流群 `711301266`
@@ -130,8 +169,6 @@ pnpm i -g commitizen
<img src="https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg" style="width:200px" /> <img src="https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg" style="width:200px" />
</div> </div>
- 本人微信号honghuangdc欢迎来技术交流业务咨询。
## License ## License
[MIT © Soybean-2021](./LICENSE) [MIT © Soybean-2021](./LICENSE)

View File

@@ -3,6 +3,6 @@ import dayjs from 'dayjs';
/** 项目构建时间 */ /** 项目构建时间 */
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')); const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
export const define = { export const viteDefine = {
PROJECT_BUILD_TIME, PROJECT_BUILD_TIME
}; };

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

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

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

@@ -0,0 +1,20 @@
import type { ProxyOptions } from 'vite';
/**
* 设置网络代理
* @param isOpenProxy - 是否开启代理
* @param envConfig - env环境配置
*/
export function createViteProxy(isOpenProxy: boolean, envConfig: EnvConfig) {
if (!isOpenProxy) return undefined;
const proxy: Record<string, string | ProxyOptions> = {
[envConfig.proxy]: {
target: envConfig.url,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${envConfig.proxy}`), '')
}
};
return proxy;
}

View File

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

View File

@@ -1,21 +0,0 @@
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import Components from 'unplugin-vue-components/vite';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
export default (srcPath: string) => {
return [
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(`${srcPath}/assets/svg`),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
dts: true,
resolvers: [IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
}),
];
};

View File

@@ -0,0 +1,6 @@
import ViteCompression from 'vite-plugin-compression';
export default (viteEnv: ImportMetaEnv) => {
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv;
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE });
};

View File

@@ -1,17 +1,14 @@
import { loadEnv } from 'vite'; import type { PluginOption } from 'vite';
import type { ConfigEnv, PluginOption } from 'vite'; import { createHtmlPlugin } from 'vite-plugin-html';
import { createHtmlPlugin } from 'vite-plugin-html'; // html插件(使用变量、压缩)
export default (config: ConfigEnv): PluginOption[] => {
const viteEnv = loadEnv(config.mode, `.env.${config.mode}`);
export default (viteEnv: ImportMetaEnv): PluginOption[] => {
return createHtmlPlugin({ return createHtmlPlugin({
minify: true, minify: true,
inject: { inject: {
data: { data: {
appName: viteEnv.VITE_APP_NAME, appName: viteEnv.VITE_APP_NAME,
appTitle: viteEnv.VITE_APP_TITLE, appTitle: viteEnv.VITE_APP_TITLE
}, }
}, }
}); });
}; };

View File

@@ -1,27 +1,25 @@
import type { ConfigEnv, PluginOption } from 'vite'; import type { PluginOption } from 'vite';
import vue from './vue'; import vue from './vue';
import html from './html'; import html from './html';
import autoImport from './auto-import'; import unplugin from './unplugin';
import windicss from './windicss'; import unocss from './unocss';
import mock from './mock'; import mock from './mock';
import visualizer from './visualizer'; import visualizer from './visualizer';
import compress from './compress';
/** /**
* vite插件 * vite插件
* @param configEnv - 环境
* @param srcPath - src路径
* @param viteEnv - 环境变量配置 * @param viteEnv - 环境变量配置
*/ */
export function setupVitePlugins( export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
configEnv: ConfigEnv, const plugins = [...vue, html(viteEnv), ...unplugin, unocss, mock];
srcPath: string,
viteEnv: ImportMetaEnv
): (PluginOption | PluginOption[])[] {
const plugins = [vue, html(configEnv), ...autoImport(srcPath), windicss, mock];
if (configEnv.command === 'build' && viteEnv.VITE_VISUALIZER === 'true') { if (viteEnv.VITE_VISUALIZER === 'true') {
plugins.push(visualizer); plugins.push(visualizer);
} }
if (viteEnv.VITE_COMPRESS === 'true') {
plugins.push(compress(viteEnv));
}
return plugins; return plugins;
} }

View File

@@ -5,5 +5,5 @@ export default viteMockServe({
injectCode: ` injectCode: `
import { setupMockServer } from '../mock'; import { setupMockServer } from '../mock';
setupMockServer(); setupMockServer();
`, `
}); });

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

@@ -0,0 +1,3 @@
import unocss from 'unocss/vite';
export default unocss();

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

@@ -0,0 +1,35 @@
import DefineOptions from 'unplugin-vue-define-options/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 { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import { getSrcPath } from '../utils';
const srcPath = getSrcPath();
const customIconPath = `${srcPath}/assets/svg`;
export default [
DefineOptions(),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dts: 'src/typings/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })]
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__'
})
];

View File

@@ -3,4 +3,5 @@ import { visualizer } from 'rollup-plugin-visualizer';
export default visualizer({ export default visualizer({
gzipSize: true, gzipSize: true,
brotliSize: true, brotliSize: true,
open: true
}); });

View File

@@ -1,3 +1,6 @@
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default vue({}); const plugins = [vue(), vueJsx()];
export default plugins;

View File

@@ -1,3 +0,0 @@
import windiCSS from 'vite-plugin-windicss';
export default windiCSS();

20
build/utils/index.ts Normal file
View File

@@ -0,0 +1,20 @@
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}`;
}

48
components.d.ts vendored
View File

@@ -1,48 +0,0 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399
declare module 'vue' {
export interface GlobalComponents {
BetterScroll: typeof import('./src/components/custom/BetterScroll.vue')['default'];
CountTo: typeof import('./src/components/custom/CountTo.vue')['default'];
DarkModeContainer: typeof import('./src/components/common/DarkModeContainer.vue')['default'];
DarkModeSwitch: typeof import('./src/components/common/DarkModeSwitch.vue')['default'];
GithubLink: typeof import('./src/components/custom/GithubLink.vue')['default'];
HoverContainer: typeof import('./src/components/common/HoverContainer.vue')['default'];
IconAntDesignCloseOutlined: typeof import('~icons/ant-design/close-outlined')['default'];
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default'];
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default'];
IconCustomActivity: typeof import('~icons/custom/activity')['default'];
IconCustomAvatar: typeof import('~icons/custom/avatar')['default'];
IconCustomCast: typeof import('~icons/custom/cast')['default'];
IconCustomLogo: typeof import('~icons/custom/logo')['default'];
IconCustomLogoFill: typeof import('~icons/custom/logo-fill')['default'];
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'];
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'];
IconIcOutlineCheck: typeof import('~icons/ic/outline-check')['default'];
IconLineMdMenuFoldLeft: typeof import('~icons/line-md/menu-fold-left')['default'];
IconLineMdMenuUnfoldLeft: typeof import('~icons/line-md/menu-unfold-left')['default'];
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'];
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'];
IconMdiClose: typeof import('~icons/mdi/close')['default'];
IconMdiGithub: typeof import('~icons/mdi/github')['default'];
IconMdiMoonWaningCrescent: typeof import('~icons/mdi/moon-waning-crescent')['default'];
IconMdiPin: typeof import('~icons/mdi/pin')['default'];
IconMdiPinOff: typeof import('~icons/mdi/pin-off')['default'];
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'];
IconMdiWhiteBalanceSunny: typeof import('~icons/mdi/white-balance-sunny')['default'];
IconPhCaretDoubleLeftBold: typeof import('~icons/ph/caret-double-left-bold')['default'];
IconPhCaretDoubleRightBold: typeof import('~icons/ph/caret-double-right-bold')['default'];
IconSelect: typeof import('./src/components/custom/IconSelect.vue')['default'];
IconUilSearch: typeof import('~icons/uil/search')['default'];
ImageVerify: typeof import('./src/components/custom/ImageVerify.vue')['default'];
LoadingEmptyWrapper: typeof import('./src/components/business/LoadingEmptyWrapper.vue')['default'];
LoginAgreement: typeof import('./src/components/business/LoginAgreement.vue')['default'];
NaiveProvider: typeof import('./src/components/common/NaiveProvider.vue')['default'];
SystemLogo: typeof import('./src/components/common/SystemLogo.vue')['default'];
WebSiteLink: typeof import('./src/components/custom/WebSiteLink.vue')['default'];
}
}
export {};

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh-cmn-Hans">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
@@ -19,7 +19,7 @@
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div> <div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
</div> </div>
</div> </div>
<h2 class="loading-title"><%= appTitle %></h2> <div class="loading-title"><%= appTitle %></div>
</div> </div>
<script src="/resource/loading.js"></script> <script src="/resource/loading.js"></script>
</div> </div>

View File

@@ -1,9 +1,10 @@
import type { MockMethod } from 'vite-plugin-mock'; import type { MockMethod } from 'vite-plugin-mock';
import { userModel } from '../model';
const token: ApiAuth.Token = { /** 参数错误的状态码 */
token: '__TEMP_TOKEN__', const ERROR_PARAM_CODE = 10000;
refreshToken: '__TEMP_REFRESH_TOKEN__',
}; const ERROR_PARAM_MSG = '参数校验失败!';
const apis: MockMethod[] = [ const apis: MockMethod[] = [
// 获取验证码 // 获取验证码
@@ -14,80 +15,114 @@ const apis: MockMethod[] = [
return { return {
code: 200, code: 200,
message: 'ok', message: 'ok',
data: true, data: true
}; };
}, }
}, },
// 密码登录 // 用户+密码 登录
{ {
url: '/mock/loginByPwd', url: '/mock/login',
method: 'post', method: 'post',
response: (): Service.MockServiceResult<ApiAuth.Token> => { response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
return { const { userName = undefined, password = undefined } = options.body;
code: 200,
message: 'ok', if (!userName || !password) {
data: token,
};
},
},
// 验证码登录
{
url: '/mock/loginByCode',
method: 'post',
response: (): Service.MockServiceResult<ApiAuth.Token> => {
return {
code: 200,
message: 'ok',
data: token,
};
},
},
// 获取用户信息(请求头携带token)
{
url: '/mock/getUserInfo',
method: 'get',
response: (): Service.MockServiceResult<ApiAuth.UserInfo> => {
return {
code: 200,
message: 'ok',
data: {
userId: '0',
userName: 'Soybean',
userPhone: '15170283876',
userRole: 'super',
},
};
},
},
{
url: '/mock/testToken',
method: 'post',
response: (option: any): Service.MockServiceResult<true | null> => {
if (option.headers?.authorization !== token.token) {
return { return {
code: 66666, code: ERROR_PARAM_CODE,
message: 'token 失效', message: ERROR_PARAM_MSG,
data: null, 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 { return {
code: 200, code: 1000,
message: 'ok', message: '用户名或密码错误!',
data: true, 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', url: '/mock/updateToken',
method: 'post', method: 'post',
response: (): Service.MockServiceResult<ApiAuth.Token> => { 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 { return {
code: 200, code: 3000,
message: 'ok', message: '用户已失效或不存在!',
data: token, data: null
}; };
}, }
}, }
]; ];
export default apis; export default apis;

20
mock/api/demo.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { MockMethod } from 'vite-plugin-mock';
const apis: MockMethod[] = [
{
url: '/mock/apiDemoWithAdapter',
method: 'post',
response: (): Service.MockServiceResult<ApiDemo.DataWithAdapter> => {
return {
code: 200,
message: 'ok',
data: {
dataId: '123',
dataName: 'demoName'
}
};
}
}
];
export default apis;

View File

@@ -1,382 +1,29 @@
import type { MockMethod } from 'vite-plugin-mock'; import type { MockMethod } from 'vite-plugin-mock';
import { userModel, routeModel } from '../model';
const routes: AuthRoute.Route[] = [
{
name: 'dashboard',
path: '/dashboard',
component: 'basic',
children: [
{
name: 'dashboard_analysis',
path: '/dashboard/analysis',
component: 'self',
meta: {
title: '分析页',
requiresAuth: true,
icon: 'icon-park-outline:analysis',
},
},
{
name: 'dashboard_workbench',
path: '/dashboard/workbench',
component: 'self',
meta: {
title: '工作台',
requiresAuth: true,
permissions: ['super', 'admin'],
icon: 'icon-park-outline:workbench',
},
},
],
meta: {
title: '仪表盘',
icon: 'carbon:dashboard',
order: 1,
},
},
{
name: 'document',
path: '/document',
component: 'basic',
children: [
{
name: 'document_vue',
path: '/document/vue',
component: 'self',
meta: {
title: 'vue文档',
requiresAuth: true,
icon: 'mdi:vuejs',
},
},
{
name: 'document_vue-new',
path: '/document/vue-new',
component: 'self',
meta: {
title: 'vue文档(新版)',
requiresAuth: true,
icon: 'mdi:vuejs',
},
},
{
name: 'document_vite',
path: '/document/vite',
component: 'self',
meta: {
title: 'vite文档',
requiresAuth: true,
icon: 'simple-icons:vite',
},
},
{
name: 'document_naive',
path: '/document/naive',
component: 'self',
meta: {
title: 'naive文档',
requiresAuth: true,
icon: 'mdi:alpha-n-box-outline',
},
},
{
name: 'document_project',
path: '/document/project',
meta: {
title: '项目文档(外链)',
requiresAuth: true,
icon: 'mdi:file-link-outline',
href: 'https://docs.soybean.pro/',
},
},
],
meta: {
title: '文档',
icon: 'carbon:document',
order: 2,
},
},
{
name: 'component',
path: '/component',
component: 'basic',
children: [
{
name: 'component_button',
path: '/component/button',
component: 'self',
meta: {
title: '按钮',
requiresAuth: true,
icon: 'ic:baseline-radio-button-checked',
},
},
{
name: 'component_card',
path: '/component/card',
component: 'self',
meta: {
title: '卡片',
requiresAuth: true,
icon: 'mdi:card-outline',
},
},
{
name: 'component_table',
path: '/component/table',
component: 'self',
meta: {
title: '表格',
requiresAuth: true,
icon: 'mdi:table-large',
},
},
],
meta: {
title: '组件示例',
icon: 'fluent:app-store-24-regular',
order: 3,
},
},
{
name: 'plugin',
path: '/plugin',
component: 'basic',
children: [
{
name: 'plugin_map',
path: '/plugin/map',
component: 'self',
meta: {
title: '地图',
requiresAuth: true,
icon: 'mdi:map',
},
},
{
name: 'plugin_video',
path: '/plugin/video',
component: 'self',
meta: {
title: '视频',
requiresAuth: true,
icon: 'mdi:video',
},
},
{
name: 'plugin_editor',
path: '/plugin/editor',
component: 'multi',
children: [
{
name: 'plugin_editor_quill',
path: '/plugin/editor/quill',
component: 'self',
meta: {
title: '富文本编辑器',
requiresAuth: true,
icon: 'mdi:file-document-edit-outline',
},
},
{
name: 'plugin_editor_markdown',
path: '/plugin/editor/markdown',
component: 'self',
meta: {
title: 'markdown编辑器',
requiresAuth: true,
icon: 'ri:markdown-line',
},
},
],
meta: {
title: '编辑器',
icon: 'icon-park-outline:editor',
},
},
{
name: 'plugin_swiper',
path: '/plugin/swiper',
component: 'self',
meta: {
title: 'Swiper插件',
requiresAuth: true,
icon: 'simple-icons:swiper',
},
},
{
name: 'plugin_copy',
path: '/plugin/copy',
component: 'self',
meta: {
title: '剪贴板',
requiresAuth: true,
icon: 'mdi:clipboard-outline',
},
},
{
name: 'plugin_icon',
path: '/plugin/icon',
component: 'self',
meta: {
title: '图标',
requiresAuth: true,
icon: 'ic:baseline-insert-emoticon',
},
},
{
name: 'plugin_print',
path: '/plugin/print',
component: 'self',
meta: {
title: '打印',
requiresAuth: true,
icon: 'ic:baseline-local-printshop',
},
},
],
meta: {
title: '插件示例',
icon: 'clarity:plugin-line',
order: 4,
},
},
{
name: 'exception',
path: '/exception',
component: 'basic',
children: [
{
name: 'exception_403',
path: '/exception/403',
component: 'self',
meta: {
title: '异常页403',
requiresAuth: true,
icon: 'ic:baseline-block',
},
},
{
name: 'exception_404',
path: '/exception/404',
component: 'self',
meta: {
title: '异常页404',
requiresAuth: true,
icon: 'ic:baseline-web-asset-off',
},
},
{
name: 'exception_500',
path: '/exception/500',
component: 'self',
meta: {
title: '异常页500',
requiresAuth: true,
icon: 'ic:baseline-wifi-off',
},
},
],
meta: {
title: '异常页',
icon: 'ant-design:exception-outlined',
order: 5,
},
},
{
name: 'multi-menu',
path: '/multi-menu',
component: 'basic',
children: [
{
name: 'multi-menu_first',
path: '/multi-menu/first',
component: 'multi',
children: [
{
name: 'multi-menu_first_second',
path: '/multi-menu/first/second',
component: 'self',
meta: {
title: '二级菜单',
requiresAuth: true,
icon: 'ic:outline-menu',
},
},
{
name: 'multi-menu_first_second-new',
path: '/multi-menu/first/second-new',
component: 'multi',
children: [
{
name: 'multi-menu_first_second-new_third',
path: '/multi-menu/first/second-new/third',
component: 'self',
meta: {
title: '三级菜单',
requiresAuth: true,
icon: 'ic:outline-menu',
},
},
],
meta: {
title: '二级菜单(有子菜单)',
icon: 'ic:outline-menu',
},
},
],
meta: {
title: '一级菜单',
icon: 'ic:outline-menu',
},
},
],
meta: {
title: '多级菜单',
icon: 'carbon:menu',
order: 6,
},
},
{
name: 'about',
path: '/about',
component: 'self',
meta: {
title: '关于',
requiresAuth: true,
singleLayout: 'basic',
permissions: ['super', 'admin', 'test'],
icon: 'fluent:book-information-24-regular',
order: 7,
},
},
];
function dataMiddleware(data: AuthRoute.Route[]): ApiRoute.Route {
const routeHomeName: AuthRoute.RouteKey = 'dashboard_analysis';
function sortRoutes(sorts: AuthRoute.Route[]) {
return sorts.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
}
return {
routes: sortRoutes(data),
home: routeHomeName,
};
}
const apis: MockMethod[] = [ const apis: MockMethod[] = [
{ {
url: '/mock/getUserRoutes', url: '/mock/getUserRoutes',
method: 'post', method: 'post',
response: (): Service.MockServiceResult => { response: (options: Service.MockOption): Service.MockServiceResult => {
const { userId = undefined } = options.body;
const routeHomeName: AuthRoute.RouteKey = 'dashboard_analysis';
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
const filterRoutes = routeModel[role];
return { return {
code: 200, code: 200,
message: 'ok', message: 'ok',
data: dataMiddleware(routes), data: {
routes: filterRoutes,
home: routeHomeName
}
}; };
}, }
}, }
]; ];
export default apis; export default apis;

40
mock/model/auth.ts Normal file
View File

@@ -0,0 +1,40 @@
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'
}
];

2
mock/model/index.ts Normal file
View File

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

1022
mock/model/route.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,11 @@
{ {
"name": "soybean-admin", "name": "soybean-admin",
"version": "0.9.3", "version": "0.9.6",
"author": {
"name": "Soybean",
"email": "honghuangdc@gmail.com",
"url": "https://github.com/honghuangdc"
},
"scripts": { "scripts": {
"dev": "cross-env VITE_ENV_TYPE=dev vite", "dev": "cross-env VITE_ENV_TYPE=dev vite",
"dev:test": "cross-env VITE_ENV_TYPE=test vite", "dev:test": "cross-env VITE_ENV_TYPE=test vite",
@@ -9,9 +14,9 @@
"build:dev": "npm run typecheck && cross-env VITE_ENV_TYPE=dev vite build", "build:dev": "npm run typecheck && cross-env VITE_ENV_TYPE=dev vite build",
"build:test": "npm run typecheck && cross-env VITE_ENV_TYPE=test vite build", "build:test": "npm run typecheck && cross-env VITE_ENV_TYPE=test vite build",
"build:vercel": "cross-env VITE_HASH_ROUTE=true vite build", "build:vercel": "cross-env VITE_HASH_ROUTE=true vite build",
"preview": "vite preview --port 5050", "preview": "vite preview",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "patch-package", "postinstall": "patch-package",
"release": "standard-version", "release": "standard-version",
@@ -26,74 +31,88 @@
} }
}, },
"dependencies": { "dependencies": {
"@antv/g2plot": "^2.4.10", "@antv/data-set": "^0.11.8",
"@antv/g2": "^4.2.3",
"@better-scroll/core": "^2.4.2", "@better-scroll/core": "^2.4.2",
"@vueuse/core": "^8.0.0", "@soybeanjs/vue-admin-layout": "^1.0.4",
"axios": "^0.26.1", "@soybeanjs/vue-admin-tab": "^1.0.2",
"clipboard": "^2.0.10", "@vueuse/core": "^8.6.0",
"axios": "^0.27.2",
"clipboard": "^2.0.11",
"colord": "^2.9.2", "colord": "^2.9.2",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"dayjs": "^1.10.8", "dayjs": "^1.11.3",
"echarts": "^5.3.3",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"naive-ui": "^2.26.4", "naive-ui": "^2.30.4",
"pinia": "^2.0.11", "pinia": "^2.0.14",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qs": "^6.10.3", "qs": "^6.10.5",
"soybean-admin-layout": "^1.0.4", "swiper": "^8.2.4",
"soybean-admin-tab": "^1.2.3",
"swiper": "^8.0.7",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"vditor": "^3.8.12", "vditor": "^3.8.15",
"vue": "^3.2.31", "vue": "3.2.37",
"vue-router": "^4.0.14", "vue-router": "^4.0.16",
"wangeditor": "^4.7.12", "wangeditor": "^4.7.15",
"xgplayer": "^2.31.4" "xgplayer": "^2.31.6"
}, },
"devDependencies": { "devDependencies": {
"@amap/amap-jsapi-types": "^0.0.8", "@amap/amap-jsapi-types": "^0.0.8",
"@commitlint/cli": "^16.2.1", "@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^16.2.1", "@commitlint/config-conventional": "^17.0.2",
"@iconify/json": "^2.1.14", "@iconify/json": "^2.1.62",
"@iconify/vue": "^3.1.4", "@iconify/vue": "^3.2.1",
"@types/bmapgl": "^0.0.5", "@types/bmapgl": "^0.0.5",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/node": "^17.0.21", "@types/node": "^17.0.44",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.14.0", "@typescript-eslint/parser": "^5.28.0",
"@vitejs/plugin-vue": "^2.2.4", "@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.3.0", "cz-customizable": "^6.3.0",
"eslint": "^8.11.0", "eslint": "^8.17.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.4", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "9.1.1",
"husky": "^7.0.4", "husky": "^8.0.1",
"lint-staged": "^12.3.5", "lint-staged": "^13.0.1",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.5.1", "prettier": "^2.7.0",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.9", "sass": "^1.52.3",
"typescript": "~4.6.2", "standard-version": "^9.5.0",
"unplugin-icons": "^0.13.3", "typescript": "^4.7.3",
"unplugin-vue-components": "^0.18.0", "unocss": "^0.39.0",
"vite": "2.8.6", "unplugin-icons": "^0.14.3",
"vite-plugin-html": "^3.1.0", "unplugin-vue-components": "0.19.6",
"unplugin-vue-define-options": "^0.6.1",
"vite": "^2.9.12",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-windicss": "^1.8.3", "vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^0.32.1", "vue-eslint-parser": "^9.0.2",
"vueuc": "^0.4.27", "vue-tsc": "^0.37.8"
"windicss": "^3.5.1" },
} "homepage": "https://github.com/honghuangdc/soybean-admin",
"repository": {
"url": "https://github.com/honghuangdc/soybean-admin.git"
},
"bugs": {
"url": "https://github.com/honghuangdc/soybean-admin/issues"
},
"license": "MIT"
} }

10820
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
/** /**
* 初始化加载效果的svg格式logo * 初始化加载效果的svg格式logo
* @param { string }id - 元素id * @param {string} id - 元素id
*/ */
function initSvgLogo(id) { function initSvgLogo(id) {
const svgStr = `<svg width="128px" height="128px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" const svgStr = `<svg width="128px" height="128px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
@@ -22,8 +22,7 @@ function initSvgLogo(id) {
<path style="fill:currentColor" d="M77.9,126.6c0,3.9-2.8,7.2-6.7,7.9c-7.8,1.5-14.8,5.9-19.7,12.2c-2.7,3.5-7.6,4.2-11.2,1.6 <path style="fill:currentColor" d="M77.9,126.6c0,3.9-2.8,7.2-6.7,7.9c-7.8,1.5-14.8,5.9-19.7,12.2c-2.7,3.5-7.6,4.2-11.2,1.6
c-3.6-2.6-4.3-7.6-1.7-11.2c0.1-0.1,0.2-0.3,0.3-0.4c4.1-5.2,9.3-9.6,15.1-12.8c4.4-2.5,9.1-4.2,14-5.1 c-3.6-2.6-4.3-7.6-1.7-11.2c0.1-0.1,0.2-0.3,0.3-0.4c4.1-5.2,9.3-9.6,15.1-12.8c4.4-2.5,9.1-4.2,14-5.1
C73.3,117.7,77.9,121.3,77.9,126.6z" /> C73.3,117.7,77.9,121.3,77.9,126.6z" />
</svg> </svg>`;
`;
const appEl = document.querySelector(id); const appEl = document.querySelector(id);
const div = document.createElement('div'); const div = document.createElement('div');
div.innerHTML = svgStr; div.innerHTML = svgStr;
@@ -34,11 +33,12 @@ function initSvgLogo(id) {
function addThemeColorCssVars() { function addThemeColorCssVars() {
const key = '__THEME_COLOR__'; const key = '__THEME_COLOR__';
const themeColor = window.localStorage.getItem(key) || '#1890ff'; const defaultColor = '#1890ff';
const themeColor = window.localStorage.getItem(key) || defaultColor;
const cssVars = `--primary-color: ${themeColor}`; const cssVars = `--primary-color: ${themeColor}`;
document.documentElement.style.cssText = cssVars; document.documentElement.style.cssText = cssVars;
} }
initSvgLogo('#loadingLogo');
addThemeColorCssVars(); addThemeColorCssVars();
initSvgLogo('#loadingLogo');

View File

@@ -20,4 +20,5 @@ const theme = useThemeStore();
subscribeStore(); subscribeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--mdi" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M19 10c0 1.38-2.12 2.5-3.5 2.5s-2.75-1.12-2.75-2.5h-1.5c0 1.38-1.37 2.5-2.75 2.5S5 11.38 5 10h-.75c-.16.64-.25 1.31-.25 2a8 8 0 0 0 8 8a8 8 0 0 0 8-8c0-.69-.09-1.36-.25-2H19m-7-6C9.04 4 6.45 5.61 5.07 8h13.86C17.55 5.61 14.96 4 12 4m10 8a10 10 0 0 1-10 10A10 10 0 0 1 2 12A10 10 0 0 1 12 2a10 10 0 0 1 10 10m-10 5.23c-1.75 0-3.29-.73-4.19-1.81L9.23 14c.45.72 1.52 1.23 2.77 1.23s2.32-.51 2.77-1.23l1.42 1.42c-.9 1.08-2.44 1.81-4.19 1.81Z"></path></svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@@ -53,7 +53,7 @@ const props = withDefaults(defineProps<Props>(), {
emptyDesc: '暂无数据', emptyDesc: '暂无数据',
iconClass: 'text-320px text-primary', iconClass: 'text-320px text-primary',
descClass: 'text-16px text-[#666]', descClass: 'text-16px text-[#666]',
showNetworkReload: false, showNetworkReload: false
}); });
// 网络状态 // 网络状态
@@ -79,7 +79,7 @@ function handleReload() {
const stopHandle = watch( const stopHandle = watch(
() => props.loading, () => props.loading,
(newValue) => { newValue => {
// 结束加载判断一下网络状态 // 结束加载判断一下网络状态
if (!newValue) { if (!newValue) {
setNetwork(window.navigator.onLine); setNetwork(window.navigator.onLine);
@@ -91,4 +91,5 @@ onUnmounted(() => {
stopHandle(); stopHandle();
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -23,7 +23,7 @@ interface Emits {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
value: true, value: true
}); });
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@@ -34,7 +34,7 @@ const checked = computed({
}, },
set(newValue: boolean) { set(newValue: boolean) {
emit('update:value', newValue); emit('update:value', newValue);
}, }
}); });
function handleClickProtocol() { function handleClickProtocol() {
@@ -44,4 +44,5 @@ function handleClickPolicy() {
emit('click-policy'); emit('click-policy');
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,10 +1,19 @@
<template> <template>
<div <div
class="bg-white text-[#333639] dark:(bg-[#18181c] text-white text-opacity-82) transition-all duration-300 ease-in-out" class="dark:bg-[#18181c] dark:text-white dark:text-opacity-82 transition-all duration-300 ease-in-out"
:class="inverted ? 'bg-[#001428] text-white' : 'bg-white text-[#333639]'"
> >
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
interface Props {
inverted?: boolean;
}
withDefaults(defineProps<Props>(), {
inverted: false
});
</script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex-center text-18px hover:text-primary cursor-pointer" @click="handleSwitch"> <div class="flex-center text-18px cursor-pointer" @click="handleSwitch">
<icon-mdi-moon-waning-crescent v-if="darkMode" /> <icon-mdi-moon-waning-crescent v-if="darkMode" />
<icon-mdi-white-balance-sunny v-else /> <icon-mdi-white-balance-sunny v-else />
</div> </div>
@@ -18,7 +18,7 @@ interface Emits {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
dark: false, dark: false
}); });
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@@ -29,11 +29,12 @@ const darkMode = computed({
}, },
set(newValue: boolean) { set(newValue: boolean) {
emit('update:dark', newValue); emit('update:dark', newValue);
}, }
}); });
function handleSwitch() { function handleSwitch() {
darkMode.value = !darkMode.value; darkMode.value = !darkMode.value;
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -25,4 +25,5 @@ defineProps<Props>();
const routeHomePath = routeName('root'); const routeHomePath = routeName('root');
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -2,36 +2,44 @@
<div v-if="showTooltip"> <div v-if="showTooltip">
<n-tooltip :placement="placement" trigger="hover"> <n-tooltip :placement="placement" trigger="hover">
<template #trigger> <template #trigger>
<div class="flex-center h-full cursor-pointer hover:bg-[#f6f6f6] dark:hover:bg-[#333]" :class="contentClass"> <div class="flex-center h-full cursor-pointer dark:hover:bg-[#333]" :class="contentClassName">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
{{ tooltipContent }} {{ tooltipContent }}
</n-tooltip> </n-tooltip>
</div> </div>
<div v-else class="flex-center cursor-pointer hover:bg-[#f6f6f6] dark:hover:bg-[#333]" :class="contentClass"> <div v-else class="flex-center cursor-pointer dark:hover:bg-[#333]" :class="contentClassName">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import type { FollowerPlacement } from 'vueuc'; import type { PopoverPlacement } from 'naive-ui';
interface Props { interface Props {
/** tooltip显示文本 */ /** tooltip显示文本 */
tooltipContent?: string; tooltipContent?: string;
/** tooltip的位置 */ /** tooltip的位置 */
placement?: FollowerPlacement; placement?: PopoverPlacement;
/** class类 */ /** class类 */
contentClass?: string; contentClass?: string;
/** 反转模式下 */
inverted?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
tooltipContent: '', tooltipContent: '',
placement: 'bottom', placement: 'bottom',
contentClass: '', contentClass: '',
inverted: false
}); });
const showTooltip = computed(() => Boolean(props.tooltipContent)); const showTooltip = computed(() => Boolean(props.tooltipContent));
const contentClassName = computed(
() => `${props.contentClass} ${props.inverted ? 'hover:bg-primary' : 'hover:bg-[#f6f6f6]'}`
);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -29,7 +29,7 @@ const NaiveProviderContent = defineComponent({
}, },
render() { render() {
return h('div'); return h('div');
}, }
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -10,7 +10,8 @@ interface Props {
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
fill: false, fill: false
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -44,4 +44,5 @@ onMounted(() => {
defineExpose({ instance }); defineExpose({ instance });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
separator: ',', separator: ',',
decimal: '.', decimal: '.',
useEasing: true, useEasing: true,
transition: 'linear', transition: 'linear'
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@@ -61,7 +61,7 @@ function run() {
duration: props.duration, duration: props.duration,
onStarted: () => emit('on-started'), onStarted: () => emit('on-started'),
onFinished: () => emit('on-finished'), onFinished: () => emit('on-finished'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}), ...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {})
}); });
} }
@@ -106,3 +106,5 @@ onMounted(() => {
} }
}); });
</script> </script>
<style scoped></style>

View File

@@ -43,7 +43,7 @@ interface Emits {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
emptyIcon: 'mdi:apps', emptyIcon: 'mdi:apps'
}); });
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@@ -51,7 +51,7 @@ const emit = defineEmits<Emits>();
const theme = useThemeStore(); const theme = useThemeStore();
const searchValue = ref(''); const searchValue = ref('');
const iconsList = computed(() => props.icons.filter((v) => v.includes(searchValue.value))); const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
const modelValue = computed({ const modelValue = computed({
get() { get() {
@@ -59,13 +59,14 @@ const modelValue = computed({
}, },
set(val: string) { set(val: string) {
emit('update:value', val); emit('update:value', val);
}, }
}); });
function handleChange(iconItem: string) { function handleChange(iconItem: string) {
modelValue.value = iconItem; modelValue.value = iconItem;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.n-input-wrapper) { :deep(.n-input-wrapper) {
padding-right: 0; padding-right: 0;

View File

@@ -17,7 +17,7 @@ interface Emits {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
code: '', code: ''
}); });
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@@ -26,14 +26,15 @@ const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
watch( watch(
() => props.code, () => props.code,
(newValue) => { newValue => {
setImgCode(newValue); setImgCode(newValue);
} }
); );
watch(imgCode, (newValue) => { watch(imgCode, newValue => {
emit('update:code', newValue); emit('update:code', newValue);
}); });
defineExpose({ getImgCode }); defineExpose({ getImgCode });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -0,0 +1,24 @@
<template>
<svg aria-hidden="true" width="1em" height="1em" class="inline-block">
<use :xlink:href="symbolId" fill="currentColor" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
/** 前缀 */
prefix?: string;
/** 图标名称(图片的文件名) */
icon: string;
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'icon-custom'
});
const symbolId = computed(() => `#${props.prefix}-${props.icon}`);
</script>
<style scoped></style>

View File

@@ -17,4 +17,5 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
</script> </script>
<style scoped></style> <style scoped></style>

163
src/composables/echarts.ts Normal file
View File

@@ -0,0 +1,163 @@
import { ref, watch, nextTick, onUnmounted } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart, ScatterChart, PictorialBarChart, RadarChart, GaugeChart } from 'echarts/charts';
import type {
BarSeriesOption,
LineSeriesOption,
PieSeriesOption,
ScatterSeriesOption,
PictorialBarSeriesOption,
RadarSeriesOption,
GaugeSeriesOption
} from 'echarts/charts';
import {
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent
} from 'echarts/components';
import type {
TitleComponentOption,
LegendComponentOption,
TooltipComponentOption,
GridComponentOption,
ToolboxComponentOption,
DatasetComponentOption
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { useElementSize } from '@vueuse/core';
import { useThemeStore } from '@/store';
export type ECOption = echarts.ComposeOption<
| BarSeriesOption
| LineSeriesOption
| PieSeriesOption
| ScatterSeriesOption
| PictorialBarSeriesOption
| RadarSeriesOption
| GaugeSeriesOption
| TitleComponentOption
| LegendComponentOption
| TooltipComponentOption
| GridComponentOption
| ToolboxComponentOption
| DatasetComponentOption
>;
echarts.use([
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent,
BarChart,
LineChart,
PieChart,
ScatterChart,
PictorialBarChart,
RadarChart,
GaugeChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
/**
* Echarts hooks函数
* @param options - 图表配置
* @param renderFun - 图表渲染函数(例如:图表监听函数)
* @description 按需引入图表组件,没注册的组件需要先引入
*/
export function useEcharts(
options: Ref<ECOption> | ComputedRef<ECOption>,
renderFun?: (chartInstance: echarts.ECharts) => void
) {
const theme = useThemeStore();
const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
let chart: echarts.ECharts | null = null;
function canRender() {
return initialSize.width > 0 && initialSize.height > 0;
}
function isRendered() {
return Boolean(domRef.value && chart);
}
function update(updateOptions: ECOption) {
if (isRendered()) {
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
}
}
async function render() {
if (domRef.value) {
const chartTheme = theme.darkMode ? 'dark' : 'light';
await nextTick();
chart = echarts.init(domRef.value, chartTheme);
if (renderFun) {
renderFun(chart);
}
update(options.value);
}
}
function resize() {
chart?.resize();
}
function destroy() {
chart?.dispose();
}
function updateTheme() {
destroy();
render();
}
const stopSizeWatch = watch([width, height], ([newWidth, newHeight]) => {
initialSize.width = newWidth;
initialSize.height = newHeight;
if (canRender()) {
if (!isRendered()) {
render();
} else {
resize();
}
}
});
const stopOptionWatch = watch(options, newValue => {
update(newValue);
});
const stopDarkModeWatch = watch(
() => theme.darkMode,
() => {
updateTheme();
}
);
onUnmounted(() => {
destroy();
stopSizeWatch();
stopOptionWatch();
stopDarkModeWatch();
});
return {
domRef
};
}

View File

@@ -1,3 +1,4 @@
export * from './system'; export * from './system';
export * from './router'; export * from './router';
export * from './layout'; export * from './layout';
export * from './echarts';

View File

@@ -1,13 +1,13 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useAppStore, useThemeStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
type LayoutMode = 'vertical' | 'horizontal';
type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>; type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>;
export function useBasicLayout() { export function useBasicLayout() {
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
type LayoutMode = 'vertical' | 'horizontal';
const mode = computed(() => { const mode = computed(() => {
const vertical: LayoutMode = 'vertical'; const vertical: LayoutMode = 'vertical';
const horizontal: LayoutMode = 'horizontal'; const horizontal: LayoutMode = 'horizontal';
@@ -18,23 +18,23 @@ export function useBasicLayout() {
vertical: { vertical: {
showLogo: false, showLogo: false,
showHeaderMenu: false, showHeaderMenu: false,
showMenuCollape: true, showMenuCollapse: true
}, },
'vertical-mix': { 'vertical-mix': {
showLogo: false, showLogo: false,
showHeaderMenu: false, showHeaderMenu: false,
showMenuCollape: false, showMenuCollapse: false
}, },
horizontal: { horizontal: {
showLogo: true, showLogo: true,
showHeaderMenu: true, showHeaderMenu: true,
showMenuCollape: false, showMenuCollapse: false
}, },
'horizontal-mix': { 'horizontal-mix': {
showLogo: true, showLogo: true,
showHeaderMenu: false, showHeaderMenu: false,
showMenuCollape: true, showMenuCollapse: true
}, }
}; };
const headerProps = computed(() => layoutHeaderProps[theme.layout.mode]); const headerProps = computed(() => layoutHeaderProps[theme.layout.mode]);
@@ -64,6 +64,6 @@ export function useBasicLayout() {
headerProps, headerProps,
siderVisible, siderVisible,
siderWidth, siderWidth,
siderCollapsedWidth, siderCollapsedWidth
}; };
} }

View File

@@ -46,7 +46,7 @@ export function useRouterPush(inSetup = true) {
const module: EnumType.LoginModuleKey = loginModule || 'pwd-login'; const module: EnumType.LoginModuleKey = loginModule || 'pwd-login';
const routeLocation: RouteLocationRaw = { const routeLocation: RouteLocationRaw = {
name: routeName('login'), name: routeName('login'),
params: { module }, params: { module }
}; };
const redirect = redirectUrl || route.value.fullPath; const redirect = redirectUrl || route.value.fullPath;
Object.assign(routeLocation, { query: { redirect } }); Object.assign(routeLocation, { query: { redirect } });
@@ -80,6 +80,6 @@ export function useRouterPush(inSetup = true) {
toHome, toHome,
toLogin, toLogin,
toLoginModule, toLoginModule,
toLoginRedirect, toLoginRedirect
}; };
} }

View File

@@ -1,4 +1,6 @@
import UAParser from 'ua-parser-js'; import UAParser from 'ua-parser-js';
import { useAuthStore } from '@/store';
import { isArray, isString } from '@/utils';
interface AppInfo { interface AppInfo {
/** 项目名称 */ /** 项目名称 */
@@ -16,7 +18,7 @@ export function useAppInfo(): AppInfo {
return { return {
name, name,
title, title,
desc, desc
}; };
} }
@@ -26,3 +28,27 @@ export function useDeviceInfo() {
const result = parser.getResult(); const result = parser.getResult();
return result; return result;
} }
/** 权限判断 */
export function usePermission() {
const auth = useAuthStore();
function hasPermission(permission: Auth.RoleType | Auth.RoleType[]) {
const { userRole } = auth.userInfo;
let has = userRole === 'super';
if (!has) {
if (isArray(permission)) {
has = (permission as Auth.RoleType[]).includes(userRole);
}
if (isString(permission)) {
has = (permission as Auth.RoleType) === userRole;
}
}
return has;
}
return {
hasPermission
};
}

View File

@@ -1,6 +1,5 @@
/** 百度地图sdk地址 */ /** 百度地图sdk地址 */
export const BAIDU_MAP_SDK_URL = export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
'https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1';
/** 高德地图sdk地址 */ /** 高德地图sdk地址 */
export const GAODE_MAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d'; export const GAODE_MAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';

View File

@@ -5,9 +5,9 @@ export const REGEXP_PHONE =
/** 邮箱正则 */ /** 邮箱正则 */
export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
/** 密码正则(密码为8-18位数字/字符/符号的组合) */ /** 密码正则(密码为6-18位数字/字符/符号的组合) */
export const REGEXP_PWD = export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){6,18}$/;
/** 6位数字验证码正则 */ /** 6位数字验证码正则 */
export const REGEXP_CODE_SIX = /^\d{6}$/; export const REGEXP_CODE_SIX = /^\d{6}$/;

View File

@@ -36,7 +36,7 @@ export const ERROR_STATUS = {
503: '503: 服务不可用~', 503: '503: 服务不可用~',
504: '504: 网关超时~', 504: '504: 网关超时~',
505: '505: http版本不支持该请求~', 505: '505: http版本不支持该请求~',
[DEFAULT_REQUEST_ERROR_CODE]: DEFAULT_REQUEST_ERROR_MSG, [DEFAULT_REQUEST_ERROR_CODE]: DEFAULT_REQUEST_ERROR_MSG
}; };
/** 不弹出错误信息的code */ /** 不弹出错误信息的code */

View File

@@ -1,8 +1,11 @@
import type { App } from 'vue'; import type { App } from 'vue';
import setupNetworkDirective from './network'; import setupNetworkDirective from './network';
import setupLoginDirective from './login'; import setupLoginDirective from './login';
import setupPermissionDirective from './permission';
/** setup custom vue directives. - [安装自定义的vue指令] */
export function setupDirectives(app: App) { export function setupDirectives(app: App) {
setupNetworkDirective(app); setupNetworkDirective(app);
setupLoginDirective(app); setupLoginDirective(app);
setupPermissionDirective(app);
} }

View File

@@ -20,7 +20,7 @@ export default function setupLoginDirective(app: App) {
unmounted(el: HTMLElement, binding) { unmounted(el: HTMLElement, binding) {
if (binding.value === false) return; if (binding.value === false) return;
el.removeEventListener('click', listenerHandler); el.removeEventListener('click', listenerHandler);
}, }
}; };
app.directive('login', loginDirective); app.directive('login', loginDirective);

View File

@@ -18,7 +18,7 @@ export default function setupNetworkDirective(app: App) {
unmounted(el: HTMLElement, binding) { unmounted(el: HTMLElement, binding) {
if (binding.value === false) return; if (binding.value === false) return;
el.removeEventListener('click', listenerHandler); el.removeEventListener('click', listenerHandler);
}, }
}; };
app.directive('network', networkDirective); app.directive('network', networkDirective);

View File

@@ -0,0 +1,26 @@
import type { App, Directive } from 'vue';
import { usePermission } from '@/composables';
export default function setupPermissionDirective(app: App) {
const { hasPermission } = usePermission();
function updateElVisible(el: HTMLElement, permission: Auth.RoleType | Auth.RoleType[]) {
if (!permission) {
throw new Error(`need roles: like v-permission="'admin'", v-permission="['admin', 'test]"`);
}
if (!hasPermission(permission)) {
el.parentElement?.removeChild(el);
}
}
const permissionDirective: Directive<HTMLElement, Auth.RoleType | Auth.RoleType[]> = {
mounted(el, binding) {
updateElVisible(el, binding.value);
},
beforeUpdate(el, binding) {
updateElVisible(el, binding.value);
}
};
app.directive('permission', permissionDirective);
}

View File

@@ -1,8 +1,15 @@
/** 用户角色 */
export enum EnumUserRole {
super = '超级管理员',
admin = '管理员',
user = '普通用户'
}
/** 登录模块 */ /** 登录模块 */
export enum EnumLoginModule { export enum EnumLoginModule {
'pwd-login' = '账密登录', 'pwd-login' = '账密登录',
'code-login' = '手机验证码登录', 'code-login' = '手机验证码登录',
'register' = '注册', 'register' = '注册',
'reset-pwd' = '重置密码', 'reset-pwd' = '重置密码',
'bind-wechat' = '微信绑定', 'bind-wechat' = '微信绑定'
} }

View File

@@ -2,7 +2,7 @@
export enum EnumContentType { export enum EnumContentType {
json = 'application/json', json = 'application/json',
formUrlencoded = 'application/x-www-form-urlencoded', formUrlencoded = 'application/x-www-form-urlencoded',
formData = 'multipart/form-data', formData = 'multipart/form-data'
} }
/** 缓存的key */ /** 缓存的key */
@@ -12,11 +12,11 @@ export enum EnumStorageKey {
/** 用户token */ /** 用户token */
'token' = '__TOKEN__', 'token' = '__TOKEN__',
/** 用户刷新token */ /** 用户刷新token */
'refresh-koken' = '__REFRESH_TOKEN__', 'refresh-token' = '__REFRESH_TOKEN__',
/** 用户信息 */ /** 用户信息 */
'user-info' = '__USER_INFO__', 'user-info' = '__USER_INFO__',
/** 多页签路由信息 */ /** 多页签路由信息 */
'tab-routes' = '__TAB_ROUTES__', 'multi-tab-routes' = '__MULTI_TAB_ROUTES__'
} }
/** 数据类型 */ /** 数据类型 */
@@ -32,4 +32,5 @@ export enum EnumDataType {
regexp = '[object RegExp]', regexp = '[object RegExp]',
set = '[object Set]', set = '[object Set]',
map = '[object Map]', map = '[object Map]',
file = '[object File]'
} }

View File

@@ -1,7 +1,7 @@
/** 布局组件的名称 */ /** 布局组件的名称 */
export enum EnumLayoutComponentName { export enum EnumLayoutComponentName {
basic = 'basic-layout', basic = 'basic-layout',
blank = 'blank-layout', blank = 'blank-layout'
} }
/** 布局模式 */ /** 布局模式 */
@@ -9,20 +9,20 @@ export enum EnumThemeLayoutMode {
'vertical' = '左侧菜单模式', 'vertical' = '左侧菜单模式',
'horizontal' = '顶部菜单模式', 'horizontal' = '顶部菜单模式',
'vertical-mix' = '左侧菜单混合模式', 'vertical-mix' = '左侧菜单混合模式',
'horizontal-mix' = '顶部菜单混合模式', 'horizontal-mix' = '顶部菜单混合模式'
} }
/** 多页签风格 */ /** 多页签风格 */
export enum EnumThemeTabMode { export enum EnumThemeTabMode {
'chrome' = '谷歌风格', 'chrome' = '谷歌风格',
'button' = '按钮风格', 'button' = '按钮风格'
} }
/** 水平模式的菜单位置 */ /** 水平模式的菜单位置 */
export enum EnumThemeHorizontalMenuPosition { export enum EnumThemeHorizontalMenuPosition {
'flex-start' = '居左', 'flex-start' = '居左',
'center' = '居中', 'center' = '居中',
'flex-end' = '居右', 'flex-end' = '居右'
} }
/** 过渡动画类型 */ /** 过渡动画类型 */
@@ -32,5 +32,5 @@ export enum EnumThemeAnimateMode {
'fade-slide' = '滑动', 'fade-slide' = '滑动',
'fade' = '消退', 'fade' = '消退',
'fade-bottom' = '底部消退', 'fade-bottom' = '底部消退',
'fade-scale' = '缩放消退', 'fade-scale' = '缩放消退'
} }

View File

@@ -47,6 +47,6 @@ export default function useCountDown(second: number) {
isCounting, isCounting,
start, start,
stop, stop,
isComplete, isComplete
}; };
} }

View File

@@ -26,7 +26,7 @@ export default function useImageVerify(width = 152, height = 40) {
domRef, domRef,
imgCode, imgCode,
setImgCode, setImgCode,
getImgCode, getImgCode
}; };
} }
@@ -52,6 +52,7 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
ctx.fillStyle = randomColor(180, 230); ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) { for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)]; const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
imgCode += text; imgCode += text;
@@ -81,5 +82,6 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
ctx.fillStyle = randomColor(150, 200); ctx.fillStyle = randomColor(150, 200);
ctx.fill(); ctx.fill();
} }
return imgCode; return imgCode;
} }

View File

@@ -7,6 +7,7 @@ import useCountDown from './useCountDown';
export default function useSmsCode() { export default function useSmsCode() {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const { counts, start, isCounting } = useCountDown(60); const { counts, start, isCounting } = useCountDown(60);
const initLabel = '获取验证码'; const initLabel = '获取验证码';
const countingLabel = (second: number) => `${second}秒后重新获取`; const countingLabel = (second: number) => `${second}秒后重新获取`;
const label = computed(() => { const label = computed(() => {
@@ -40,6 +41,7 @@ export default function useSmsCode() {
async function getSmsCode(phone: string) { async function getSmsCode(phone: string) {
const valid = isPhoneValid(phone); const valid = isPhoneValid(phone);
if (!valid || loading.value) return; if (!valid || loading.value) return;
startLoading(); startLoading();
const { data } = await fetchSmsCode(phone); const { data } = await fetchSmsCode(phone);
if (data) { if (data) {
@@ -54,6 +56,6 @@ export default function useSmsCode() {
start, start,
isCounting, isCounting,
getSmsCode, getSmsCode,
loading, loading
}; };
} }

View File

@@ -3,7 +3,5 @@ import useBoolean from './useBoolean';
import useLoading from './useLoading'; import useLoading from './useLoading';
import useLoadingEmpty from './useLoadingEmpty'; import useLoadingEmpty from './useLoadingEmpty';
import useReload from './useReload'; import useReload from './useReload';
import useBodyScroll from './useBodyScroll';
import useModalVisible from './useModalVisible';
export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload, useBodyScroll, useModalVisible }; export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload };

View File

@@ -1,47 +0,0 @@
interface ScrollBodyStyle {
overflow: string;
paddingRight: string;
}
/**
* body标签滚动
* @param duration - 显示滚动条的延迟时间
*/
export default function useBodyScroll(duration = 300) {
const defaultStyle: ScrollBodyStyle = {
overflow: '',
paddingRight: '',
};
function getInitBodyStyle() {
const { overflow, paddingRight } = document.body.style;
Object.assign(defaultStyle, { overflow, paddingRight });
}
function setScrollBodyStyle() {
document.body.style.paddingRight = `${window.innerWidth - document.body.clientWidth}px`;
document.body.style.overflow = 'hidden';
}
function resetScrollBodyStyle() {
document.body.style.overflow = defaultStyle.overflow;
document.body.style.paddingRight = defaultStyle.paddingRight;
}
/**
* 处理body的滚动条
* @param hideScroll - 禁止滚动
*/
function scrollBodyHandler(hideScroll: boolean) {
if (hideScroll) {
setScrollBodyStyle();
} else {
setTimeout(() => {
resetScrollBodyStyle();
}, duration);
}
}
getInitBodyStyle();
return {
scrollBodyHandler,
};
}

View File

@@ -21,6 +21,6 @@ export default function useBoolean(initValue = false) {
setBool, setBool,
setTrue, setTrue,
setFalse, setFalse,
toggle, toggle
}; };
} }

View File

@@ -15,6 +15,6 @@ export default function useContext<T>(contextName = 'context') {
return { return {
useProvide, useProvide,
useInject, useInject
}; };
} }

View File

@@ -6,6 +6,6 @@ export default function useLoading(initValue = false) {
return { return {
loading, loading,
startLoading, startLoading,
endLoading, endLoading
}; };
} }

View File

@@ -9,6 +9,6 @@ export default function useLoadingEmpty(initLoading = false, initEmpty = false)
startLoading, startLoading,
endLoading, endLoading,
empty, empty,
setEmpty, setEmpty
}; };
} }

View File

@@ -1,33 +0,0 @@
import { watch, onUnmounted } from 'vue';
import useBoolean from './useBoolean';
import useBodyScroll from './useBodyScroll';
/**
* 使用弹窗
* @param hideScroll - 关闭html滚动条
*/
export default function useModalVisible(hideScroll = true) {
const { bool: visible, setTrue: openModal, setFalse: closeModal, toggle: toggleModal } = useBoolean();
const { scrollBodyHandler } = useBodyScroll();
function modalVisibleWatcher() {
const stopHandle = watch(visible, async (newValue) => {
scrollBodyHandler(newValue);
});
onUnmounted(() => {
stopHandle();
});
}
if (hideScroll) {
modalVisibleWatcher();
}
return {
visible,
openModal,
closeModal,
toggleModal,
};
}

View File

@@ -13,17 +13,16 @@ export default function useReload() {
async function handleReload(duration = 0) { async function handleReload(duration = 0) {
setFalse(); setFalse();
await nextTick(); await nextTick();
if (duration) {
if (duration > 0) {
setTimeout(() => { setTimeout(() => {
setTrue(); setTrue();
}, duration); }, duration);
} else {
setTrue();
} }
} }
return { return {
reloadFlag, reloadFlag,
handleReload, handleReload
}; };
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<soybean-admin-layout <admin-layout
:mode="mode" :mode="mode"
:min-width="theme.layout.minWidth" :min-width="theme.layout.minWidth"
:fixed-header-and-tab="theme.fixedHeaderAndTab" :fixed-header-and-tab="theme.fixedHeaderAndTab"
@@ -10,6 +10,7 @@
:sider-width="siderWidth" :sider-width="siderWidth"
:sider-collapsed-width="siderCollapsedWidth" :sider-collapsed-width="siderCollapsedWidth"
:sider-collapse="app.siderCollapse" :sider-collapse="app.siderCollapse"
:add-main-overflow-hidden="addMainOverflowHidden"
:fixed-footer="theme.footer.fixed" :fixed-footer="theme.footer.fixed"
> >
<template #header> <template #header>
@@ -21,23 +22,27 @@
<template #sider> <template #sider>
<global-sider /> <global-sider />
</template> </template>
<global-content /> <global-content @hide-main-overflow="setAddMainOverflowHidden" />
<template #footer> <template #footer>
<global-footer /> <global-footer />
</template> </template>
</soybean-admin-layout> </admin-layout>
<setting-drawer /> <setting-drawer />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SoybeanAdminLayout from 'soybean-admin-layout'; import AdminLayout from '@soybeanjs/vue-admin-layout';
import { useAppStore, useThemeStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
import { useBasicLayout } from '@/composables'; import { useBasicLayout } from '@/composables';
import { useBoolean } from '@/hooks';
import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common'; import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common';
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
const { mode, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout(); const { mode, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout();
const { bool: addMainOverflowHidden, setBool: setAddMainOverflowHidden } = useBoolean();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -5,4 +5,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { GlobalContent } from '../common'; import { GlobalContent } from '../common';
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -4,9 +4,15 @@
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out" class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
> >
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition :name="theme.page.animate ? theme.page.animateMode : undefined" mode="out-in" appear> <transition
:name="theme.pageAnimateMode"
mode="out-in"
:appear="true"
@before-leave="handleBeforeLeave"
@after-enter="handleAfterEnter"
>
<keep-alive :include="routeStore.cacheRoutes"> <keep-alive :include="routeStore.cacheRoutes">
<component :is="Component" v-if="app.reloadFlag" :key="route.path" /> <component :is="Component" v-if="app.reloadFlag" :key="route.fullPath" />
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
@@ -21,12 +27,27 @@ interface Props {
showPadding?: boolean; showPadding?: boolean;
} }
interface Emits {
/** 禁止主体溢出 */
(e: 'hide-main-overflow', hidden: boolean): void;
}
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
showPadding: true, showPadding: true
}); });
const emit = defineEmits<Emits>();
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
function handleBeforeLeave() {
emit('hide-main-overflow', true);
}
function handleAfterEnter() {
emit('hide-main-overflow', false);
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -5,4 +5,5 @@
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts"></script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<hover-container class="w-40px h-full" tooltip-content="全屏" @click="toggle"> <hover-container class="w-40px h-full" tooltip-content="全屏" :inverted="theme.header.inverted" @click="toggle">
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" /> <icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" />
<icon-gridicons-fullscreen v-else class="text-18px" /> <icon-gridicons-fullscreen v-else class="text-18px" />
</hover-container> </hover-container>
@@ -7,7 +7,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFullscreen } from '@vueuse/core'; import { useFullscreen } from '@vueuse/core';
import { useThemeStore } from '@/store';
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,10 +1,21 @@
<template> <template>
<hover-container tooltip-content="github" class="w-40px h-full"> <hover-container
<a href="https://github.com/honghuangdc/soybean-admin" target="_blank" class="flex-center"> tooltip-content="github"
<icon-mdi-github class="text-20px text-[#666]" /> class="w-40px h-full"
</a> :inverted="theme.header.inverted"
@click="handleClickLink"
>
<icon-mdi-github class="text-20px" />
</hover-container> </hover-container>
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup>
import { useThemeStore } from '@/store';
const theme = useThemeStore();
function handleClickLink() {
window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
}
</script>
<style scoped></style> <style scoped></style>

View File

@@ -17,8 +17,9 @@
:is="breadcrumb.icon" :is="breadcrumb.icon"
v-if="theme.header.crumb.showIcon" v-if="theme.header.crumb.showIcon"
class="inline-block align-text-bottom mr-4px text-16px" class="inline-block align-text-bottom mr-4px text-16px"
:class="{ 'text-#BBBBBB': theme.header.inverted }"
/> />
<span>{{ breadcrumb.label }}</span> <span :class="{ 'text-#BBBBBB': theme.header.inverted }">{{ breadcrumb.label }}</span>
</template> </template>
</n-breadcrumb-item> </n-breadcrumb-item>
</template> </template>
@@ -46,4 +47,5 @@ function dropdownSelect(key: string) {
routerPush({ name: key }); routerPush({ name: key });
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,24 +1,42 @@
<template> <template>
<n-menu :value="activeKey" mode="horizontal" :options="menus" @update:value="handleUpdateMenu" /> <div class="flex-1-hidden h-full px-10px">
<n-scrollbar :x-scrollable="true" class="flex-1-hidden h-full" content-class="h-full">
<div class="flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }">
<n-menu
:value="activeKey"
mode="horizontal"
:options="menus"
:inverted="theme.header.inverted"
@update:value="handleUpdateMenu"
/>
</div>
</n-scrollbar>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import type { MenuOption } from 'naive-ui'; import type { MenuOption } from 'naive-ui';
import { useRouteStore } from '@/store'; import { useRouteStore, useThemeStore } from '@/store';
import { useRouterPush } from '@/composables'; import { useRouterPush } from '@/composables';
const route = useRoute(); const route = useRoute();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const theme = useThemeStore();
const { routerPush } = useRouterPush(); const { routerPush } = useRouterPush();
const menus = computed(() => routeStore.menus as GlobalMenuOption[]); const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
const activeKey = computed(() => route.name as string); const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
function handleUpdateMenu(_key: string, item: MenuOption) { function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as GlobalMenuOption; const menuItem = item as GlobalMenuOption;
routerPush(menuItem.routePath); routerPush(menuItem.routePath);
} }
</script> </script>
<style scoped></style>
<style scoped>
:deep(.n-menu-item-content-header) {
overflow: inherit !important;
}
</style>

View File

@@ -1,13 +1,15 @@
<template> <template>
<hover-container class="w-40px h-full" @click="app.toggleSiderCollapse"> <hover-container class="w-40px h-full" :inverted="theme.header.inverted" @click="app.toggleSiderCollapse">
<icon-line-md-menu-unfold-left v-if="app.siderCollapse" class="text-16px" /> <icon-line-md-menu-unfold-left v-if="app.siderCollapse" class="text-16px" />
<icon-line-md-menu-fold-left v-else class="text-16px" /> <icon-line-md-menu-fold-left v-else class="text-16px" />
</hover-container> </hover-container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<hover-container class="w-40px" content-class="hover:text-primary" tooltip-content="主题模式"> <hover-container class="w-40px" :inverted="theme.header.inverted" tooltip-content="主题模式">
<dark-mode-switch :dark="theme.darkMode" class="wh-full" @update:dark="theme.setDarkMode" /> <dark-mode-switch :dark="theme.darkMode" class="wh-full" @update:dark="theme.setDarkMode" />
</hover-container> </hover-container>
</template> </template>
@@ -9,4 +9,5 @@ import { useThemeStore } from '@/store';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<n-dropdown :options="options" @select="handleDropdown"> <n-dropdown :options="options" @select="handleDropdown">
<hover-container class="px-12px"> <hover-container class="px-12px" :inverted="theme.header.inverted">
<icon-custom-avatar class="text-32px" /> <icon-custom-avatar class="text-32px" />
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span> <span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
</hover-container> </hover-container>
@@ -8,28 +8,29 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAuthStore } from '@/store'; import { useAuthStore, useThemeStore } from '@/store';
import { iconifyRender } from '@/utils'; import { iconifyRender } from '@/utils';
type DropdownKey = 'user-center' | 'logout'; type DropdownKey = 'user-center' | 'logout';
const auth = useAuthStore(); const auth = useAuthStore();
const theme = useThemeStore();
const options = [ const options = [
{ {
label: '用户中心', label: '用户中心',
key: 'user-center', key: 'user-center',
icon: iconifyRender('carbon:user-avatar'), icon: iconifyRender('carbon:user-avatar')
}, },
{ {
type: 'divider', type: 'divider',
key: 'divider', key: 'divider'
}, },
{ {
label: '退出登录', label: '退出登录',
key: 'logout', key: 'logout',
icon: iconifyRender('carbon:logout'), icon: iconifyRender('carbon:logout')
}, }
]; ];
function handleDropdown(optionKey: string) { function handleDropdown(optionKey: string) {
@@ -42,9 +43,10 @@ function handleDropdown(optionKey: string) {
negativeText: '取消', negativeText: '取消',
onPositiveClick: () => { onPositiveClick: () => {
auth.resetAuthStore(); auth.resetAuthStore();
}, }
}); });
} }
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,13 +1,11 @@
<template> <template>
<dark-mode-container class="global-header flex-y-center h-full"> <dark-mode-container class="global-header flex-y-center h-full" :inverted="theme.header.inverted">
<global-logo v-if="showLogo" :show-title="true" class="h-full" :style="{ width: theme.sider.width + 'px' }" /> <global-logo v-if="showLogo" :show-title="true" class="h-full" :style="{ width: theme.sider.width + 'px' }" />
<div v-if="!showHeaderMenu" class="flex-1-hidden flex-y-center h-full"> <div v-if="!showHeaderMenu" class="flex-1-hidden flex-y-center h-full">
<menu-collapse v-if="showMenuCollape" /> <menu-collapse v-if="showMenuCollapse" />
<global-breadcrumb v-if="theme.header.crumb.visible" /> <global-breadcrumb v-if="theme.header.crumb.visible" />
</div> </div>
<div v-else class="flex-1-hidden flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }"> <header-menu v-else />
<header-menu />
</div>
<div class="flex justify-end h-full"> <div class="flex justify-end h-full">
<global-search /> <global-search />
<github-site /> <github-site />
@@ -29,7 +27,7 @@ import {
GithubSite, GithubSite,
FullScreen, FullScreen,
ThemeMode, ThemeMode,
UserAvatar, UserAvatar
} from './components'; } from './components';
interface Props { interface Props {
@@ -38,13 +36,14 @@ interface Props {
/** 显示头部菜单 */ /** 显示头部菜单 */
showHeaderMenu: GlobalHeaderProps['showHeaderMenu']; showHeaderMenu: GlobalHeaderProps['showHeaderMenu'];
/** 显示菜单折叠按钮 */ /** 显示菜单折叠按钮 */
showMenuCollape: GlobalHeaderProps['showMenuCollape']; showMenuCollapse: GlobalHeaderProps['showMenuCollapse'];
} }
defineProps<Props>(); defineProps<Props>();
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped> <style scoped>
.global-header { .global-header {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%); box-shadow: 0 1px 2px rgb(0 21 41 / 8%);

View File

@@ -21,4 +21,5 @@ defineProps<Props>();
const { title } = useAppInfo(); const { title } = useAppInfo();
const routeHomePath = routePath('root'); const routeHomePath = routePath('root');
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,6 +17,7 @@
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup></script>
<style lang="scss" scoped> <style lang="scss" scoped>
.icon { .icon {
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66; box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;

View File

@@ -5,7 +5,7 @@
:closable="false" :closable="false"
preset="card" preset="card"
footer-style="padding: 0; margin: 0" footer-style="padding: 0; margin: 0"
class="w-630px fixed top-50px left-1/2 transform -translate-x-1/2" class="w-630px fixed left-0 right-0 top-50px"
@after-leave="handleClose" @after-leave="handleClose"
> >
<n-input ref="inputRef" v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch"> <n-input ref="inputRef" v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch">
@@ -60,10 +60,10 @@ const show = computed({
}, },
set(val: boolean) { set(val: boolean) {
emit('update:value', val); emit('update:value', val);
}, }
}); });
watch(show, async (val) => { watch(show, async val => {
if (val) { if (val) {
/** 自动聚焦 */ /** 自动聚焦 */
await nextTick(); await nextTick();
@@ -74,7 +74,7 @@ watch(show, async (val) => {
/** 查询 */ /** 查询 */
function search() { function search() {
resultOptions.value = routeStore.searchMenus.filter( resultOptions.value = routeStore.searchMenus.filter(
(menu) => keyword.value && menu.meta?.title.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) menu => keyword.value && menu.meta?.title.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim())
); );
if (resultOptions.value?.length > 0) { if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path; activePath.value = resultOptions.value[0].path;
@@ -96,7 +96,7 @@ function handleClose() {
function handleUp() { function handleUp() {
const { length } = resultOptions.value; const { length } = resultOptions.value;
if (length === 0) return; if (length === 0) return;
const index = resultOptions.value.findIndex((item) => item.path === activePath.value); const index = resultOptions.value.findIndex(item => item.path === activePath.value);
if (index === 0) { if (index === 0) {
activePath.value = resultOptions.value[length - 1].path; activePath.value = resultOptions.value[length - 1].path;
} else { } else {
@@ -108,7 +108,7 @@ function handleUp() {
function handleDown() { function handleDown() {
const { length } = resultOptions.value; const { length } = resultOptions.value;
if (length === 0) return; if (length === 0) return;
const index = resultOptions.value.findIndex((item) => item.path === activePath.value); const index = resultOptions.value.findIndex(item => item.path === activePath.value);
if (index + 1 === length) { if (index + 1 === length) {
activePath.value = resultOptions.value[0].path; activePath.value = resultOptions.value[0].path;
} else { } else {
@@ -120,7 +120,7 @@ function handleDown() {
function handleEnter() { function handleEnter() {
const { length } = resultOptions.value; const { length } = resultOptions.value;
if (length === 0 || activePath.value === '') return; if (length === 0 || activePath.value === '') return;
const routeItem = resultOptions.value.find((item) => item.path === activePath.value); const routeItem = resultOptions.value.find(item => item.path === activePath.value);
if (routeItem?.meta?.href) { if (routeItem?.meta?.href) {
window.open(activePath.value, '__blank'); window.open(activePath.value, '__blank');
} else { } else {
@@ -134,4 +134,5 @@ onKeyStroke('Enter', handleEnter);
onKeyStroke('ArrowUp', handleUp); onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown); onKeyStroke('ArrowDown', handleDown);
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -6,7 +6,7 @@
class="bg-[#e5e7eb] dark:bg-dark h-56px mt-8px px-14px rounded-4px cursor-pointer flex-y-center justify-between" class="bg-[#e5e7eb] dark:bg-dark h-56px mt-8px px-14px rounded-4px cursor-pointer flex-y-center justify-between"
:style="{ :style="{
background: item.path === active ? theme.themeColor : '', background: item.path === active ? theme.themeColor : '',
color: item.path === active ? '#fff' : '', color: item.path === active ? '#fff' : ''
}" }"
@click="handleTo" @click="handleTo"
@mouseenter="handleMouse(item)" @mouseenter="handleMouse(item)"
@@ -47,7 +47,7 @@ const active = computed({
}, },
set(val: string) { set(val: string) {
emit('update:value', val); emit('update:value', val);
}, }
}); });
/** 鼠标移入 */ /** 鼠标移入 */
@@ -59,4 +59,5 @@ function handleTo() {
emit('enter'); emit('enter');
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,20 +1,28 @@
<template> <template>
<div> <div>
<hover-container tooltip-content="搜索" class="w-40px h-full" @click="handleSearch"> <hover-container
<icon-uil-search class="text-20px text-[#666]" /> class="w-40px h-full"
tooltip-content="搜索"
:inverted="theme.header.inverted"
@click="handleSearch"
>
<icon-uil-search class="text-20px" />
</hover-container> </hover-container>
<search-modal v-model:value="show" /> <search-modal v-model:value="show" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useThemeStore } from '@/store';
import { useBoolean } from '@/hooks'; import { useBoolean } from '@/hooks';
import { SearchModal } from './components'; import { SearchModal } from './components';
const { bool: show, toggle } = useBoolean(); const { bool: show, toggle } = useBoolean();
const theme = useThemeStore();
function handleSearch() { function handleSearch() {
toggle(); toggle();
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -10,4 +10,5 @@ import { useAppStore } from '@/store';
const app = useAppStore(); const app = useAppStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -2,12 +2,12 @@
<div class="mb-6px px-4px cursor-pointer" @mouseenter="setTrue" @mouseleave="setFalse"> <div class="mb-6px px-4px cursor-pointer" @mouseenter="setTrue" @mouseleave="setFalse">
<div <div
class="flex-center flex-col py-12px rounded-2px bg-transparent transition-colors duration-300 ease-in-out" class="flex-center flex-col py-12px rounded-2px bg-transparent transition-colors duration-300 ease-in-out"
:class="{ 'text-primary !bg-primary-active': isActive, 'text-primary': isHover }" :class="{ 'text-primary !bg-primary_active': isActive, 'text-primary': isHover }"
> >
<component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" /> <component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
<p <p
class="pt-8px text-12px overflow-hidden transition-height duration-300 ease-in-out" class="text-12px overflow-hidden transition-height duration-300 ease-in-out"
:class="[isMini ? 'h-0 pt-0' : 'h-20px pt-8px']" :class="[isMini ? 'h-0 pt-0' : 'h-24px pt-4px']"
> >
{{ label }} {{ label }}
</p> </p>
@@ -35,11 +35,12 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
icon: undefined, icon: undefined,
isMini: false, isMini: false
}); });
const { bool: isHover, setTrue, setFalse } = useBoolean(); const { bool: isHover, setTrue, setFalse } = useBoolean();
const isActive = computed(() => props.routeName === props.activeRouteName); const isActive = computed(() => props.routeName === props.activeRouteName);
</script> </script>
<style scoped></style> <style scoped></style>

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