feat: axios

pull/1/MERGE
xingyuv 2023-03-18 23:10:57 +08:00
parent d430f6d272
commit 3a39b2afb7
45 changed files with 1539 additions and 1456 deletions

13
.env
View File

@ -1,8 +1,15 @@
# 端口号
VITE_PORT = 3100
VITE_PORT = 80
# 网站标题
VITE_GLOB_APP_TITLE = Vben Admin
VITE_GLOB_APP_TITLE = 芋道管理系统
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
VITE_GLOB_APP_SHORT_NAME = vue_vben_admin
VITE_GLOB_APP_SHORT_NAME = Vben
# 租户开关
VITE_GLOB_APP_TENANT_ENABLE = true
# 验证码的开关
VITE_GLOB_APP_CAPTCHA_ENABLE = true

View File

@ -1,18 +1,21 @@
# 本地开发环境
NODE_ENV=development
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = /
# 本地开发代理,可以解决跨域及多地址代理
# 如果接口地址匹配到则会转发到http://localhost:3000防止本地出现跨域问题
# 可以有多个,注意多个不能换行,否则代理将会失效
VITE_PROXY = [["/basic-api","http://localhost:3000"],["/upload","http://localhost:3300/upload"]]
# VITE_PROXY=[["/api","https://xingyuv.com/test"]]
VITE_PROXY = [["/dev-api","http://localhost:48080"],["/upload","http://localhost:48080/admin-api/infra/file/upload"]]
# VITE_PROXY=[["/api","http://vben.xingyuv.com/test"]]
# 是否删除Console.log
VITE_DROP_CONSOLE = false
# 接口地址
# 如果没有跨域问题,直接在这里配置即可
VITE_GLOB_API_URL = /basic-api
VITE_GLOB_API_URL = "http://localhost:48080/admin-api"
# 文件上传接口 可选
VITE_GLOB_UPLOAD_URL = /upload

24
.env.front Normal file
View File

@ -0,0 +1,24 @@
# 本地开发环境
NODE_ENV=development
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = /
# 本地开发代理,可以解决跨域及多地址代理
# 如果接口地址匹配到则会转发到http://localhost:3000防止本地出现跨域问题
# 可以有多个,注意多个不能换行,否则代理将会失效
VITE_PROXY = [["/dev-api","http://api-dashboard.yudao.iocoder.cn"],["/upload","http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload"]]
# VITE_PROXY=[["/api","http://vben.xingyuv.com/test"]]
# 是否删除Console.log
VITE_DROP_CONSOLE = false
# 接口地址
# 如果没有跨域问题,直接在这里配置即可
VITE_GLOB_API_URL = "http://localhost:48080/admin-api"
# 文件上传接口 可选
VITE_GLOB_UPLOAD_URL = /upload
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
VITE_GLOB_API_URL_PREFIX =

View File

@ -13,13 +13,13 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# 接口地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_API_URL = /basic-api
VITE_GLOB_API_URL = /admin-api
# 文件上传地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_UPLOAD_URL = /upload
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
VITE_GLOB_API_URL_PREFIX =
VITE_GLOB_API_URL_PREFIX =
# 打包是否开启pwa功能
VITE_USE_PWA = false

View File

@ -1,4 +1,5 @@
NODE_ENV=production
# 资源公共路径,需要以 / 开头和结尾
VITE_PUBLIC_PATH = /
@ -14,7 +15,7 @@ VITE_BUILD_COMPRESS = 'none'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# 接口地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_API_URL = /basic-api
VITE_GLOB_API_URL = /admin-api
# 文件上传地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_UPLOAD_URL = /upload

2
CNAME
View File

@ -1 +1 @@
vben.vvbin.cn
vben.xingyuv.com

View File

@ -1,6 +1,6 @@
{
"name": "vben-admin",
"version": "1.0.2",
"name": "ruoyi-ui-admin-vben",
"version": "1.0.0",
"author": {
"name": "xingyu4j",
"email": "xingyu4j@vip.qq.com",
@ -34,8 +34,6 @@
"@ant-design/colors": "^7.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@iconify/iconify": "^3.1.0",
"@logicflow/core": "^1.2.1",
"@logicflow/extension": "^1.2.1",
"@vue/runtime-core": "^3.2.47",
"@vueuse/core": "^9.13.0",
"@zxcvbn-ts/core": "^2.2.1",
@ -55,7 +53,6 @@
"qrcode": "^1.5.1",
"qs": "^6.11.1",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^2.1.0",
"sortablejs": "^1.15.0",
"tinymce": "^5.10.7",
"vditor": "^3.9.1",
@ -82,7 +79,6 @@
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@types/showdown": "^2.0.0",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
@ -91,6 +87,7 @@
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/compiler-sfc": "^3.2.47",
"autoprefixer": "^10.4.14",
"consola": "^2.15.3",
"conventional-changelog-cli": "^2.2.2",
"cross-env": "^7.0.3",
"cz-git": "^1.6.0",
@ -106,7 +103,6 @@
"inquirer": "^9.1.4",
"less": "^4.1.3",
"lint-staged": "^13.2.0",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
@ -122,7 +118,6 @@
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^6.0.2",
"terser": "^5.16.6",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"unplugin-vue-setup-extend-plus": "^0.4.9",
"vite": "^4.2.0",

View File

@ -8,8 +8,6 @@ specifiers:
'@iconify/iconify': ^3.1.0
'@iconify/json': ^2.2.36
'@kirklin/vite-plugin-vben-theme': ^0.1.1
'@logicflow/core': ^1.2.1
'@logicflow/extension': ^1.2.1
'@purge-icons/generated': ^0.9.0
'@types/codemirror': ^5.60.5
'@types/crypto-js': ^4.1.1
@ -21,7 +19,6 @@ specifiers:
'@types/nprogress': ^0.2.0
'@types/qrcode': ^1.5.0
'@types/qs': ^6.9.7
'@types/showdown': ^2.0.0
'@types/sortablejs': ^1.15.1
'@typescript-eslint/eslint-plugin': ^5.55.0
'@typescript-eslint/parser': ^5.55.0
@ -36,6 +33,7 @@ specifiers:
autoprefixer: ^10.4.14
axios: ^1.3.4
codemirror: ^5.65.3
consola: ^2.15.3
conventional-changelog-cli: ^2.2.2
cropperjs: ^1.5.13
cross-env: ^7.0.3
@ -57,7 +55,6 @@ specifiers:
less: ^4.1.3
lint-staged: ^13.2.0
lodash-es: ^4.17.21
npm-run-all: ^4.1.5
nprogress: ^0.2.0
path-to-regexp: ^6.2.1
picocolors: ^1.0.0
@ -73,7 +70,6 @@ specifiers:
rimraf: ^4.4.0
rollup: ^3.19.1
rollup-plugin-visualizer: ^5.9.0
showdown: ^2.1.0
sortablejs: ^1.15.0
stylelint: ^14.16.1
stylelint-config-prettier: ^9.0.5
@ -83,7 +79,6 @@ specifiers:
stylelint-order: ^6.0.2
terser: ^5.16.6
tinymce: ^5.10.7
ts-node: ^10.9.1
typescript: ^5.0.2
unplugin-vue-setup-extend-plus: ^0.4.9
vditor: ^3.9.1
@ -110,8 +105,6 @@ dependencies:
'@ant-design/colors': 7.0.0
'@ant-design/icons-vue': 6.1.0_vue@3.2.47
'@iconify/iconify': 3.1.0
'@logicflow/core': 1.2.1
'@logicflow/extension': 1.2.1
'@vue/runtime-core': 3.2.47
'@vueuse/core': 9.13.0_vue@3.2.47
'@zxcvbn-ts/core': 2.2.1
@ -131,7 +124,6 @@ dependencies:
qrcode: 1.5.1
qs: 6.11.1
resize-observer-polyfill: 1.5.1
showdown: 2.1.0
sortablejs: 1.15.0
tinymce: 5.10.7
vditor: 3.9.1
@ -158,7 +150,6 @@ devDependencies:
'@types/nprogress': 0.2.0
'@types/qrcode': 1.5.0
'@types/qs': 6.9.7
'@types/showdown': 2.0.0
'@types/sortablejs': 1.15.1
'@typescript-eslint/eslint-plugin': 5.55.0_qsnvknysi52qtaxqdyqyohkcku
'@typescript-eslint/parser': 5.55.0_j4766f7ecgqbon3u7zlxn5zszu
@ -167,6 +158,7 @@ devDependencies:
'@vitejs/plugin-vue-jsx': 3.0.1_vite@4.2.0+vue@3.2.47
'@vue/compiler-sfc': 3.2.47
autoprefixer: 10.4.14_postcss@8.4.21
consola: 2.15.3
conventional-changelog-cli: 2.2.2
cross-env: 7.0.3
cz-git: 1.6.0
@ -182,7 +174,6 @@ devDependencies:
inquirer: 9.1.4
less: 4.1.3
lint-staged: 13.2.0
npm-run-all: 4.1.5
picocolors: 1.0.0
postcss: 8.4.21
postcss-html: 1.5.0
@ -198,7 +189,6 @@ devDependencies:
stylelint-config-standard: 29.0.0_stylelint@14.16.1
stylelint-order: 6.0.3_stylelint@14.16.1
terser: 5.16.6
ts-node: 10.9.1_sxidjv3cojnrggmso45tj7hldi
typescript: 5.0.2
unplugin-vue-setup-extend-plus: 0.4.9
vite: 4.2.0_kfn5zdpk76mco3hnivyqwkouli
@ -2062,23 +2052,6 @@ packages:
- supports-color
dev: true
/@logicflow/core/1.2.1:
resolution: {integrity: sha512-m6/K7I85jFfCWcrNEQQpchomUWM4ZUjcOhHjsVhejnfpoEx7jke3tSJlh41LyT9hIUEIy7nxnphp5L4YB+InCA==}
dependencies:
'@types/mousetrap': 1.6.11
mousetrap: 1.6.5
preact: 10.12.1
dev: false
/@logicflow/extension/1.2.1:
resolution: {integrity: sha512-C006Y73sqe0x5ArWzTWc2YglhDNs+Ck/eXJnnaZ89uYqyVDdLgr/6RBjL8JAV41Lg7EQMB1yqmfUW53tCPTRtw==}
dependencies:
'@logicflow/core': 1.2.1
ids: 1.0.0
lodash-es: 4.17.21
preact: 10.12.1
dev: false
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -2423,10 +2396,6 @@ packages:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
/@types/mousetrap/1.6.11:
resolution: {integrity: sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ==}
dev: false
/@types/node/10.17.60:
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
dev: true
@ -2467,10 +2436,6 @@ packages:
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
dev: true
/@types/showdown/2.0.0:
resolution: {integrity: sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==}
dev: true
/@types/sortablejs/1.15.1:
resolution: {integrity: sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ==}
dev: true
@ -3609,11 +3574,6 @@ packages:
engines: {node: '>= 12'}
dev: true
/commander/9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
dev: false
/common-tags/1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'}
@ -3929,17 +3889,6 @@ packages:
- encoding
dev: true
/cross-spawn/6.0.5:
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
engines: {node: '>=4.8'}
dependencies:
nice-try: 1.0.5
path-key: 2.0.1
semver: 5.7.1
shebang-command: 1.2.0
which: 1.3.1
dev: true
/cross-spawn/7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -5561,10 +5510,6 @@ packages:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
dev: true
/ids/1.0.0:
resolution: {integrity: sha512-Zvtq1xUto4LttpstyOlFum8lKx+i1OmRfg+6A9drFS9iSZsDPMHG4Sof/qwNR4kCU7jBeWFPrY2ocHxiz7cCRw==}
dev: false
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
@ -6424,11 +6369,6 @@ packages:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
dev: true
/memorystream/0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
dev: true
/meow/8.1.2:
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
engines: {node: '>=10'}
@ -6600,10 +6540,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/mousetrap/1.6.5:
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
dev: false
/ms/2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: true
@ -6679,10 +6615,6 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
/nice-try/1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
/no-case/3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
@ -6754,22 +6686,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/npm-run-all/4.1.5:
resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==}
engines: {node: '>= 4'}
hasBin: true
dependencies:
ansi-styles: 3.2.1
chalk: 2.4.2
cross-spawn: 6.0.5
memorystream: 0.3.1
minimatch: 3.1.2
pidtree: 0.3.1
read-pkg: 3.0.0
shell-quote: 1.8.0
string.prototype.padend: 3.1.4
dev: true
/npm-run-path/4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@ -7027,11 +6943,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/path-key/2.0.1:
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
engines: {node: '>=4'}
dev: true
/path-key/3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@ -7086,12 +6997,6 @@ packages:
engines: {node: '>=8.6'}
dev: true
/pidtree/0.3.1:
resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==}
engines: {node: '>=0.10'}
hasBin: true
dev: true
/pidtree/0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
@ -7259,10 +7164,6 @@ packages:
posthtml-render: 1.4.0
dev: true
/preact/10.12.1:
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
dev: false
/prelude-ls/1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -7766,13 +7667,6 @@ packages:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
dev: false
/shebang-command/1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
dependencies:
shebang-regex: 1.0.0
dev: true
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -7780,27 +7674,11 @@ packages:
shebang-regex: 3.0.0
dev: true
/shebang-regex/1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'}
dev: true
/shebang-regex/3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
dev: true
/shell-quote/1.8.0:
resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==}
dev: true
/showdown/2.1.0:
resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==}
hasBin: true
dependencies:
commander: 9.5.0
dev: false
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@ -8039,15 +7917,6 @@ packages:
side-channel: 1.0.4
dev: true
/string.prototype.padend/3.1.4:
resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.2.0
es-abstract: 1.21.1
dev: true
/string.prototype.trimend/1.0.6:
resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
dependencies:
@ -8503,37 +8372,6 @@ packages:
yn: 3.1.1
dev: true
/ts-node/10.9.1_sxidjv3cojnrggmso45tj7hldi:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 18.15.3
acorn: 8.8.2
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.0.2
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/tslib/1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true

View File

@ -1,4 +0,0 @@
import { withInstall } from '@/utils'
import flowChart from './src/FlowChart.vue'
export const FlowChart = withInstall(flowChart)

View File

@ -1,142 +0,0 @@
<template>
<div class="h-full" :class="prefixCls">
<FlowChartToolbar :prefixCls="prefixCls" v-if="toolbar" @view-data="handlePreview" />
<div ref="lfElRef" class="h-full"></div>
<BasicModal @register="register" title="流程数据" width="50%">
<JsonPreview :data="graphData" />
</BasicModal>
</div>
</template>
<script lang="ts" setup name="FlowChart">
import type { Ref } from 'vue'
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
import type { Definition } from '@logicflow/core'
import LogicFlow from '@logicflow/core'
import FlowChartToolbar from './FlowChartToolbar.vue'
import { BpmnElement, DndPanel, Menu, SelectionSelect, Snapshot } from '@logicflow/extension'
import { useDesign } from '@/hooks/web/useDesign'
import { useAppStore } from '@/store/modules/app'
import { createFlowChartContext } from './useFlowContext'
import { toLogicFlowData } from './adpterForTurbo'
import { BasicModal, useModal } from '@/components/Modal'
import { JsonPreview } from '@/components/CodeEditor'
import { configDefaultDndPanel } from './config'
import '@logicflow/core/dist/style/index.css'
import '@logicflow/extension/lib/style/index.css'
const props = defineProps({
flowOptions: {
type: Object as PropType<Definition>,
default: () => ({})
},
data: {
type: Object as PropType<any>,
default: () => ({})
},
toolbar: {
type: Boolean,
default: true
},
patternItems: {
type: Array
}
})
const lfElRef = ref(null)
const graphData = ref({})
const lfInstance = ref(null) as Ref<LogicFlow | null>
const { prefixCls } = useDesign('flow-chart')
const appStore = useAppStore()
const [register, { openModal }] = useModal()
createFlowChartContext({
logicFlow: lfInstance as unknown as LogicFlow
})
const getFlowOptions = computed(() => {
const { flowOptions } = props
const defaultOptions: Partial<Definition> = {
grid: true,
background: {
color: appStore.getDarkMode === 'light' ? '#f7f9ff' : '#151515'
},
keyboard: {
enabled: true
},
...flowOptions
}
return defaultOptions as Definition
})
watch(
() => props.data,
() => {
onRender()
}
)
// TODO
// watch(
// () => appStore.getDarkMode,
// () => {
// init();
// }
// );
watch(
() => unref(getFlowOptions),
(options) => {
unref(lfInstance)?.updateEditConfig(options)
}
)
// init logicFlow
async function init() {
await nextTick()
const lfEl = unref(lfElRef)
if (!lfEl) {
return
}
LogicFlow.use(DndPanel)
// Canvas configuration
LogicFlow.use(Snapshot)
// Use the bpmn plug-in to introduce bpmn elements, which can be used after conversion in turbo
LogicFlow.use(BpmnElement)
// Start the right-click menu
LogicFlow.use(Menu)
LogicFlow.use(SelectionSelect)
lfInstance.value = new LogicFlow({
...unref(getFlowOptions),
container: lfEl
})
const lf = unref(lfInstance)!
lf?.setDefaultEdgeType('line')
await onRender()
lf?.setPatternItems(props.patternItems || configDefaultDndPanel(lf))
}
async function onRender() {
await nextTick()
const lf = unref(lfInstance)
if (!lf) {
return
}
const lFData = toLogicFlowData(props.data)
lf.render(lFData)
}
function handlePreview() {
const lf = unref(lfInstance)
if (!lf) {
return
}
graphData.value = unref(lf).getGraphData()
openModal()
}
onMounted(init)
</script>

View File

@ -1,153 +0,0 @@
<template>
<div :class="`${prefixCls}-toolbar`" class="flex items-center px-2 py-1">
<template v-for="item in toolbarItemList" :key="item.type">
<Tooltip placement="bottom" v-bind="item.disabled ? { visible: false } : {}">
<template #title>{{ item.tooltip }}</template>
<span :class="`${prefixCls}-toolbar__icon`" v-if="item.icon" @click="onControl(item)">
<Icon :icon="item.icon" :class="item.disabled ? 'cursor-not-allowed disabeld' : 'cursor-pointer'" />
</span>
</Tooltip>
<Divider v-if="item.separate" type="vertical" />
</template>
</div>
</template>
<script lang="ts" setup name="FlowChartToolbar">
import type { ToolbarConfig } from './types'
import { nextTick, onUnmounted, ref, unref, watchEffect } from 'vue'
import { Divider, Tooltip } from 'ant-design-vue'
import { Icon } from '@/components/Icon'
import { useFlowChartContext } from './useFlowContext'
import { ToolbarTypeEnum } from './enum'
defineProps({
prefixCls: String
})
const emit = defineEmits(['view-data'])
const toolbarItemList = ref<ToolbarConfig[]>([
{
type: ToolbarTypeEnum.ZOOM_IN,
icon: 'codicon:zoom-out',
tooltip: '缩小'
},
{
type: ToolbarTypeEnum.ZOOM_OUT,
icon: 'codicon:zoom-in',
tooltip: '放大'
},
{
type: ToolbarTypeEnum.RESET_ZOOM,
icon: 'codicon:screen-normal',
tooltip: '重置比例'
},
{ separate: true },
{
type: ToolbarTypeEnum.UNDO,
icon: 'ion:arrow-undo-outline',
tooltip: '后退',
disabled: true
},
{
type: ToolbarTypeEnum.REDO,
icon: 'ion:arrow-redo-outline',
tooltip: '前进',
disabled: true
},
{ separate: true },
{
type: ToolbarTypeEnum.SNAPSHOT,
icon: 'ion:download-outline',
tooltip: '下载'
},
{
type: ToolbarTypeEnum.VIEW_DATA,
icon: 'carbon:document-view',
tooltip: '查看数据'
}
])
const { logicFlow } = useFlowChartContext()
function onHistoryChange({ data: { undoAble, redoAble } }) {
const itemsList = unref(toolbarItemList)
const undoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.UNDO)
const redoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.REDO)
if (undoIndex !== -1) {
unref(toolbarItemList)[undoIndex].disabled = !undoAble
}
if (redoIndex !== -1) {
unref(toolbarItemList)[redoIndex].disabled = !redoAble
}
}
const onControl = (item) => {
const lf = unref(logicFlow)
if (!lf) {
return
}
switch (item.type) {
case ToolbarTypeEnum.ZOOM_IN:
lf.zoom()
break
case ToolbarTypeEnum.ZOOM_OUT:
lf.zoom(true)
break
case ToolbarTypeEnum.RESET_ZOOM:
lf.resetZoom()
break
case ToolbarTypeEnum.UNDO:
lf.undo()
break
case ToolbarTypeEnum.REDO:
lf.redo()
break
case ToolbarTypeEnum.SNAPSHOT:
lf.getSnapshot()
break
case ToolbarTypeEnum.VIEW_DATA:
emit('view-data')
break
}
}
watchEffect(async () => {
if (unref(logicFlow)) {
await nextTick()
unref(logicFlow)?.on('history:change', onHistoryChange)
}
})
onUnmounted(() => {
unref(logicFlow)?.off('history:change', onHistoryChange)
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-flow-chart-toolbar';
html[data-theme='dark'] {
.lf-dnd {
background: #080808;
}
}
.@{prefix-cls} {
height: 36px;
background-color: @app-content-background;
border-bottom: 1px solid @border-color-base;
.disabeld {
color: @disabled-color;
}
&__icon {
display: inline-block;
padding: 2px 4px;
margin-right: 10px;
&:hover {
color: @primary-color;
}
}
}
</style>

View File

@ -1,75 +0,0 @@
const TurboType = {
SEQUENCE_FLOW: 1,
START_EVENT: 2,
END_EVENT: 3,
USER_TASK: 4,
SERVICE_TASK: 5,
EXCLUSIVE_GATEWAY: 6
}
function convertFlowElementToEdge(element) {
const { incoming, outgoing, properties, key } = element
const { text, startPoint, endPoint, pointsList, logicFlowType } = properties
const edge = {
id: key,
type: logicFlowType,
sourceNodeId: incoming[0],
targetNodeId: outgoing[0],
text,
startPoint,
endPoint,
pointsList,
properties: {}
}
const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType']
Object.keys(element.properties).forEach((property) => {
if (excludeProperties.indexOf(property) === -1) {
edge.properties[property] = element.properties[property]
}
})
return edge
}
function convertFlowElementToNode(element) {
const { properties, key } = element
const { x, y, text, logicFlowType } = properties
const node = {
id: key,
type: logicFlowType,
x,
y,
text,
properties: {}
}
const excludeProperties = ['x', 'y', 'text', 'logicFlowType']
Object.keys(element.properties).forEach((property) => {
if (excludeProperties.indexOf(property) === -1) {
node.properties[property] = element.properties[property]
}
})
return node
}
export function toLogicFlowData(data) {
const lfData: {
// TODO type
nodes: any[]
edges: any[]
} = {
nodes: [],
edges: []
}
const list = data.flowElementList
list &&
list.length > 0 &&
list.forEach((element) => {
if (element.type === TurboType.SEQUENCE_FLOW) {
const edge = convertFlowElementToEdge(element)
lfData.edges.push(edge)
} else {
const node = convertFlowElementToNode(element)
lfData.nodes.push(node)
}
})
return lfData
}

View File

@ -1,96 +0,0 @@
export const nodeList = [
{
text: '开始',
type: 'start',
class: 'node-start'
},
{
text: '矩形',
type: 'rect',
class: 'node-rect'
},
{
type: 'user',
text: '用户',
class: 'node-user'
},
{
type: 'push',
text: '推送',
class: 'node-push'
},
{
type: 'download',
text: '位置',
class: 'node-download'
},
{
type: 'end',
text: '结束',
class: 'node-end'
}
]
export const BpmnNode = [
{
type: 'bpmn:startEvent',
text: '开始',
class: 'bpmn-start'
},
{
type: 'bpmn:endEvent',
text: '结束',
class: 'bpmn-end'
},
{
type: 'bpmn:exclusiveGateway',
text: '网关',
class: 'bpmn-exclusiveGateway'
},
{
type: 'bpmn:userTask',
text: '用户',
class: 'bpmn-user'
}
]
export function configDefaultDndPanel(lf) {
return [
{
text: '选区',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAOVJREFUOBGtVMENwzAIjKP++2026ETdpv10iy7WFbqFyyW6GBywLCv5gI+Dw2Bluj1znuSjhb99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCUHj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkMQVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=',
callback: () => {
lf.updateEditConfig({
stopMoveGraph: true
})
}
},
{
type: 'circle',
text: '开始',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg=='
},
{
type: 'rect',
text: '用户任务',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
cls: 'important-node'
},
{
type: 'rect',
text: '系统任务',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
cls: 'import_icon'
},
{
type: 'diamond',
text: '条件判断',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII='
},
{
type: 'circle',
text: '结束',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC'
}
]
}

View File

@ -1,11 +0,0 @@
export enum ToolbarTypeEnum {
ZOOM_IN = 'zoomIn',
ZOOM_OUT = 'zoomOut',
RESET_ZOOM = 'resetZoom',
UNDO = 'undo',
REDO = 'redo',
SNAPSHOT = 'snapshot',
VIEW_DATA = 'viewData'
}

View File

@ -1,14 +0,0 @@
import { NodeConfig } from '@logicflow/core'
import { ToolbarTypeEnum } from './enum'
export interface NodeItem extends NodeConfig {
icon: string
}
export interface ToolbarConfig {
type?: string | ToolbarTypeEnum
tooltip?: string | boolean
icon?: string
disabled?: boolean
separate?: boolean
}

View File

@ -1,17 +0,0 @@
import type LogicFlow from '@logicflow/core'
import { provide, inject } from 'vue'
const key = Symbol('flow-chart')
type Instance = {
logicFlow: LogicFlow
}
export function createFlowChartContext(instance: Instance) {
provide(key, instance)
}
export function useFlowChartContext(): Instance {
return inject(key) as Instance
}

View File

@ -0,0 +1,4 @@
import { withInstall } from '@/utils/index'
import verify from './src/Verify.vue'
export const Verify = withInstall(verify)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,250 @@
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'
}"
>
<div class="verify-refresh" style="z-index: 3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<img
:src="'data:image/png;base64,' + pointBackImgBase"
ref="canvas"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px'
}"
>
{{ index + 1 }}
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<div
class="verify-bar-area"
:style="{
width: setSize.imgWidth,
color: barAreaColor,
'border-color': barAreaBorderColor,
'line-height': barSize.height
}"
>
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel" setup>
/**
* VerifyPoints
* @description 点选
* */
import { resetSize } from './../utils/util'
import { aesEncrypt } from './../utils/ase'
import { getCaptcha, checkCaptcha } from '@/api/base/login'
import { onMounted, reactive, ref, nextTick, toRefs, getCurrentInstance } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
const props = defineProps({
//popfixed
mode: {
type: String,
default: 'fixed'
},
captchaType: {
type: String
},
//
vSpace: {
type: Number,
default: 5
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
})
const { t } = useI18n()
const { mode, captchaType } = toRefs(props)
const { proxy } = getCurrentInstance()
let secretKey = ref(''), //ase
checkNum = ref(3), //
fontPos = reactive([]), //
checkPosArr = reactive([]), //
num = ref(1), //
pointBackImgBase = ref(''), //
poinTextList = reactive([]), //
backToken = ref(''), //token
setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
}),
tempPoints = reactive([]),
text = ref(''),
barAreaColor = ref(undefined),
barAreaBorderColor = ref(undefined),
showRefresh = ref(true),
bindingClick = ref(true)
const init = () => {
//
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
const canvasClick = (e) => {
checkPosArr.push(getMousePos(canvas, e))
if (num.value == checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
//
let arr = pointTransfrom(checkPosArr, setSize)
checkPosArr.length = 0
checkPosArr.push(...arr)
//
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
//
var captchaVerification = secretKey.value
? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value)
: backToken.value + '---' + JSON.stringify(checkPosArr)
let data = {
captchaType: captchaType.value,
pointJson: secretKey.value ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value) : JSON.stringify(checkPosArr),
token: backToken.value
}
checkCaptcha(data).then((response) => {
const res = response.data
if (res.repCode == '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = t('component.captcha.success')
bindingClick.value = false
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
proxy.$parent.$emit('success', { captchaVerification })
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = t('component.captcha.fail')
setTimeout(() => {
refresh()
}, 700)
}
})
}, 400)
}
if (num.value < checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
}
}
//
const getMousePos = function (obj, e) {
var x = e.offsetX
var y = e.offsetY
return { x, y }
}
//
const createPoint = function (pos) {
tempPoints.push(Object.assign({}, pos))
return num.value + 1
}
const refresh = async function () {
tempPoints.splice(0, tempPoints.length)
barAreaColor.value = '#000'
barAreaBorderColor.value = '#ddd'
bindingClick.value = true
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
await getPictrue()
showRefresh.value = true
}
//
const getPictrue = async () => {
let data = {
captchaType: captchaType.value
}
const res = await getCaptcha(data)
if (res.data.repCode == '0000') {
pointBackImgBase.value = res.data.repData.originalImageBase64
backToken.value = res.data.repData.token
secretKey.value = res.data.repData.secretKey
poinTextList.value = res.data.repData.wordList
text.value = t('component.captcha.point') + '【' + poinTextList.value.join(',') + '】'
} else {
text.value = res.data.repMsg
}
}
//
const pointTransfrom = function (pointArr, imgSize) {
var newPointArr = pointArr.map((p) => {
let x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth))
let y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight))
return { x, y }
})
return newPointArr
}
</script>

View File

@ -0,0 +1,357 @@
<template>
<div style="position: relative">
<div v-if="type === '2'" class="verify-img-out" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }">
<div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
<img :src="'data:image/png;base64,' + backImgBase" alt="" style="width: 100%; height: 100%; display: block" />
<div class="verify-refresh" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<transition name="tips">
<span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'">
{{ tipWords }}
</span>
</transition>
</div>
</div>
<!-- 公共部分 -->
<div class="verify-bar-area" :style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }">
<span class="verify-msg" v-text="text"></span>
<div
class="verify-left-bar"
:style="{
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
height: barSize.height,
'border-color': leftBarBorderColor,
transaction: transitionWidth
}"
>
<span class="verify-msg" v-text="finishText"></span>
<div
class="verify-move-block"
@touchstart="start"
@mousedown="start"
:style="{
width: barSize.height,
height: barSize.height,
'background-color': moveBlockBackgroundColor,
left: moveBlockLeft,
transition: transitionLeft
}"
>
<i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
height: setSize.imgHeight,
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight
}"
>
<img :src="blockBackImgBase" alt="" style="width: 100%; height: 100%; display: block; -webkit-user-drag: none" />
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/babel" setup>
/**
* VerifySlide
* @description 滑块
* */
import { aesEncrypt } from './../utils/ase'
import { resetSize } from './../utils/util'
import { getCaptcha, checkCaptcha } from '@/api/base/login'
import { useI18n } from '@/hooks/web/useI18n'
import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, getCurrentInstance } from 'vue'
const props = defineProps({
captchaType: {
type: String
},
type: {
type: String,
default: '1'
},
//popfixed
mode: {
type: String,
default: 'fixed'
},
vSpace: {
type: Number,
default: 5
},
explain: {
type: String,
default: ''
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
blockSize: {
type: Object,
default() {
return {
width: '50px',
height: '50px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '30px'
}
}
}
})
const { t } = useI18n()
const { mode, captchaType, type, blockSize, explain } = toRefs(props)
const { proxy } = getCurrentInstance()
const secretKey = ref('') //ase
const passFlag = ref('') //
const backImgBase = ref('') //
const blockBackImgBase = ref('') //
const backToken = ref('') //token
const startMoveTime = ref('') //
const endMovetime = ref('') //
const tipWords = ref('')
const text = ref('')
const finishText = ref('')
const setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
})
const moveBlockLeft = ref(undefined)
const leftBarWidth = ref(undefined)
//
const moveBlockBackgroundColor = ref(undefined)
const leftBarBorderColor = ref('#ddd')
const iconColor = ref(undefined)
const iconClass = ref('icon-right')
const status = ref(false) //
const isEnd = ref(false) //
const showRefresh = ref(true)
const transitionLeft = ref('')
const transitionWidth = ref('')
const startLeft = ref(0)
const barArea = computed(() => {
return proxy.$el.querySelector('.verify-bar-area')
})
const init = () => {
if (explain.value === '') {
text.value = t('component.captcha.slide')
} else {
text.value = explain.value
}
getPictrue()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
window.removeEventListener('touchmove', function (e) {
move(e)
})
window.removeEventListener('mousemove', function (e) {
move(e)
})
//
window.removeEventListener('touchend', function () {
end()
})
window.removeEventListener('mouseup', function () {
end()
})
window.addEventListener('touchmove', function (e) {
move(e)
})
window.addEventListener('mousemove', function (e) {
move(e)
})
//
window.addEventListener('touchend', function () {
end()
})
window.addEventListener('mouseup', function () {
end()
})
}
watch(type, () => {
init()
})
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
//
const start = (e) => {
e = e || window.event
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left)
startMoveTime.value = +new Date() //
if (isEnd.value == false) {
text.value = ''
moveBlockBackgroundColor.value = '#337ab7'
leftBarBorderColor.value = '#337AB7'
iconColor.value = '#fff'
e.stopPropagation()
status.value = true
}
}
//
const move = (e) => {
e = e || window.event
if (status.value && isEnd.value == false) {
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
var bar_area_left = barArea.value.getBoundingClientRect().left
var move_block_left = x - bar_area_left //left
if (move_block_left >= barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2) {
move_block_left = barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
}
if (move_block_left <= 0) {
move_block_left = parseInt(parseInt(blockSize.value.width) / 2)
}
//left
moveBlockLeft.value = move_block_left - startLeft.value + 'px'
leftBarWidth.value = move_block_left - startLeft.value + 'px'
}
}
//
const end = () => {
endMovetime.value = +new Date()
//
if (status.value && isEnd.value == false) {
var moveLeftDistance = parseInt((moveBlockLeft.value || '').replace('px', ''))
moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth)
let data = {
captchaType: captchaType.value,
pointJson: secretKey.value
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value
}
checkCaptcha(data).then((response) => {
const res = response.data
if (res.repCode == '0000') {
moveBlockBackgroundColor.value = '#5cb85c'
leftBarBorderColor.value = '#5cb85c'
iconColor.value = '#fff'
iconClass.value = 'icon-check'
showRefresh.value = false
isEnd.value = true
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
passFlag.value = true
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s
${t('component.captcha.success')}`
const captchaVerification = secretKey.value
? aesEncrypt(backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
: backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 })
setTimeout(() => {
tipWords.value = ''
proxy.$parent.closeBox()
proxy.$parent.$emit('success', { captchaVerification })
}, 1000)
} else {
moveBlockBackgroundColor.value = '#d9534f'
leftBarBorderColor.value = '#d9534f'
iconColor.value = '#fff'
iconClass.value = 'icon-close'
passFlag.value = false
setTimeout(function () {
refresh()
}, 1000)
proxy.$parent.$emit('error', proxy)
tipWords.value = t('component.captcha.fail')
setTimeout(() => {
tipWords.value = ''
}, 1000)
}
})
status.value = false
}
}
const refresh = async () => {
showRefresh.value = true
finishText.value = ''
transitionLeft.value = 'left .3s'
moveBlockLeft.value = 0
leftBarWidth.value = undefined
transitionWidth.value = 'width .3s'
leftBarBorderColor.value = '#ddd'
moveBlockBackgroundColor.value = '#fff'
iconColor.value = '#000'
iconClass.value = 'icon-right'
isEnd.value = false
await getPictrue()
setTimeout(() => {
transitionWidth.value = ''
transitionLeft.value = ''
text.value = explain.value
}, 300)
}
//
const getPictrue = async () => {
let data = {
captchaType: captchaType.value
}
const res = await getCaptcha(data)
if (res.data.repCode == '0000') {
backImgBase.value = res.data.repData.originalImageBase64
blockBackImgBase.value = 'data:image/png;base64,' + res.data.repData.jigsawImageBase64
backToken.value = res.data.repData.token
secretKey.value = res.data.repData.secretKey
} else {
tipWords.value = res.data.repMsg
}
}
</script>

View File

@ -0,0 +1,4 @@
import VerifySlide from './VerifySlide.vue'
import VerifyPoints from './VerifyPoints.vue'
export { VerifySlide, VerifyPoints }

View File

@ -0,0 +1,14 @@
import CryptoJS from 'crypto-js'
/**
* @word
* @keyWord String
* */
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

View File

@ -0,0 +1,97 @@
export function resetSize(vm) {
let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度
const EmployeeWindow = window as any
const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth
const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px'
} else {
img_width = vm.imgSize.width
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px'
} else {
img_height = vm.imgSize.height
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px'
} else {
bar_width = vm.barSize.width
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px'
} else {
bar_height = vm.barSize.height
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
export const _code_chars = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
]
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']

View File

@ -1,7 +0,0 @@
import { withInstall } from '@/utils'
import basicDragVerify from './src/DragVerify.vue'
import rotateDragVerify from './src/ImgRotate.vue'
export const BasicDragVerify = withInstall(basicDragVerify)
export const RotateDragVerify = withInstall(rotateDragVerify)
export * from './src/typing'

View File

@ -1,365 +0,0 @@
<script lang="tsx">
import type { Ref } from 'vue'
import { defineComponent, ref, computed, unref, reactive, watch, watchEffect } from 'vue'
import { useTimeoutFn } from '@/hooks/core/useTimeout'
import { useEventListener } from '@/hooks/event/useEventListener'
import { basicProps } from './props'
import { getSlot } from '@/utils/helper/tsxHelper'
import { CheckOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'BaseDargVerify',
props: basicProps,
emits: ['success', 'update:value', 'change', 'start', 'move', 'end'],
setup(props, { emit, slots, expose }) {
const state = reactive({
isMoving: false,
isPassing: false,
moveDistance: 0,
toLeft: false,
startTime: 0,
endTime: 0
})
const wrapElRef = ref<HTMLDivElement | null>(null)
const barElRef = ref<HTMLDivElement | null>(null)
const contentElRef = ref<HTMLDivElement | null>(null)
const actionElRef = ref(null) as Ref<HTMLDivElement | null>
useEventListener({
el: document,
name: 'mouseup',
listener: () => {
if (state.isMoving) {
resume()
}
}
})
const getActionStyleRef = computed(() => {
const { height, actionStyle } = props
const h = `${parseInt(height as string)}px`
return {
left: 0,
width: h,
height: h,
...actionStyle
}
})
const getWrapStyleRef = computed(() => {
const { height, width, circle, wrapStyle } = props
const h = parseInt(height as string)
const w = `${parseInt(width as string)}px`
return {
width: w,
height: `${h}px`,
lineHeight: `${h}px`,
borderRadius: circle ? h / 2 + 'px' : 0,
...wrapStyle
}
})
const getBarStyleRef = computed(() => {
const { height, circle, barStyle } = props
const h = parseInt(height as string)
return {
height: `${h}px`,
borderRadius: circle ? h / 2 + 'px 0 0 ' + h / 2 + 'px' : 0,
...barStyle
}
})
const getContentStyleRef = computed(() => {
const { height, width, contentStyle } = props
const h = `${parseInt(height as string)}px`
const w = `${parseInt(width as string)}px`
return {
height: h,
width: w,
...contentStyle
}
})
watch(
() => state.isPassing,
(isPassing) => {
if (isPassing) {
const { startTime, endTime } = state
const time = (endTime - startTime) / 1000
emit('success', { isPassing, time: time.toFixed(1) })
emit('update:value', isPassing)
emit('change', isPassing)
}
}
)
watchEffect(() => {
state.isPassing = !!props.value
})
function getEventPageX(e: MouseEvent | TouchEvent) {
return (e as MouseEvent).pageX || (e as TouchEvent).touches[0].pageX
}
function handleDragStart(e: MouseEvent | TouchEvent) {
if (state.isPassing) {
return
}
const actionEl = unref(actionElRef)
if (!actionEl) return
emit('start', e)
state.moveDistance = getEventPageX(e) - parseInt(actionEl.style.left.replace('px', ''), 10)
state.startTime = new Date().getTime()
state.isMoving = true
}
function getOffset(el: HTMLDivElement) {
const actionWidth = parseInt(el.style.width)
const { width } = props
const widthNum = parseInt(width as string)
const offset = widthNum - actionWidth - 6
return { offset, widthNum, actionWidth }
}
function handleDragMoving(e: MouseEvent | TouchEvent) {
const { isMoving, moveDistance } = state
if (isMoving) {
const actionEl = unref(actionElRef)
const barEl = unref(barElRef)
if (!actionEl || !barEl) return
const { offset, widthNum, actionWidth } = getOffset(actionEl)
const moveX = getEventPageX(e) - moveDistance
emit('move', {
event: e,
moveDistance,
moveX
})
if (moveX > 0 && moveX <= offset) {
actionEl.style.left = `${moveX}px`
barEl.style.width = `${moveX + actionWidth / 2}px`
} else if (moveX > offset) {
actionEl.style.left = `${widthNum - actionWidth}px`
barEl.style.width = `${widthNum - actionWidth / 2}px`
if (!props.isSlot) {
checkPass()
}
}
}
}
function handleDragOver(e: MouseEvent | TouchEvent) {
const { isMoving, isPassing, moveDistance } = state
if (isMoving && !isPassing) {
emit('end', e)
const actionEl = unref(actionElRef)
const barEl = unref(barElRef)
if (!actionEl || !barEl) return
const moveX = getEventPageX(e) - moveDistance
const { offset, widthNum, actionWidth } = getOffset(actionEl)
if (moveX < offset) {
if (!props.isSlot) {
resume()
} else {
setTimeout(() => {
if (!props.value) {
resume()
} else {
const contentEl = unref(contentElRef)
if (contentEl) {
contentEl.style.width = `${parseInt(barEl.style.width)}px`
}
}
}, 0)
}
} else {
actionEl.style.left = `${widthNum - actionWidth}px`
barEl.style.width = `${widthNum - actionWidth / 2}px`
checkPass()
}
state.isMoving = false
}
}
function checkPass() {
if (props.isSlot) {
resume()
return
}
state.endTime = new Date().getTime()
state.isPassing = true
state.isMoving = false
}
function resume() {
state.isMoving = false
state.isPassing = false
state.moveDistance = 0
state.toLeft = false
state.startTime = 0
state.endTime = 0
const actionEl = unref(actionElRef)
const barEl = unref(barElRef)
const contentEl = unref(contentElRef)
if (!actionEl || !barEl || !contentEl) return
state.toLeft = true
useTimeoutFn(() => {
state.toLeft = false
actionEl.style.left = '0'
barEl.style.width = '0'
// The time is consistent with the animation time
}, 300)
contentEl.style.width = unref(getContentStyleRef).width
}
expose({
resume
})
return () => {
const renderBar = () => {
const cls = [`darg-verify-bar`]
if (state.toLeft) {
cls.push('to-left')
}
return <div class={cls} ref={barElRef} style={unref(getBarStyleRef)} />
}
const renderContent = () => {
const cls = [`darg-verify-content`]
const { isPassing } = state
const { text, successText } = props
isPassing && cls.push('success')
return (
<div class={cls} ref={contentElRef} style={unref(getContentStyleRef)}>
{getSlot(slots, 'text', isPassing) || (isPassing ? successText : text)}
</div>
)
}
const renderAction = () => {
const cls = [`darg-verify-action`]
const { toLeft, isPassing } = state
if (toLeft) {
cls.push('to-left')
}
return (
<div class={cls} onMousedown={handleDragStart} onTouchstart={handleDragStart} style={unref(getActionStyleRef)} ref={actionElRef}>
{getSlot(slots, 'actionIcon', isPassing) ||
(isPassing ? (
<CheckOutlined class={`darg-verify-action__icon`} />
) : (
<DoubleRightOutlined class={`darg-verify-action__icon`} />
))}
</div>
)
}
return (
<div
class="darg-verify"
ref={wrapElRef}
style={unref(getWrapStyleRef)}
onMousemove={handleDragMoving}
onTouchmove={handleDragMoving}
onMouseleave={handleDragOver}
onMouseup={handleDragOver}
onTouchend={handleDragOver}
>
{renderBar()}
{renderContent()}
{renderAction()}
</div>
)
}
}
})
</script>
<style lang="less">
@radius: 4px;
.darg-verify {
position: relative;
overflow: hidden;
text-align: center;
background-color: rgb(238 238 238);
border: 1px solid #ddd;
border-radius: @radius;
&-bar {
position: absolute;
width: 0;
height: 36px;
background-color: @success-color;
border-radius: @radius;
&.to-left {
width: 0 !important;
transition: width 0.3s;
}
}
&-content {
position: absolute;
top: 0;
font-size: 12px;
text-size-adjust: none;
background-color: -webkit-gradient(
linear,
left top,
right top,
color-stop(0, #333),
color-stop(0.4, #333),
color-stop(0.5, #fff),
color-stop(0.6, #333),
color-stop(1, #333)
);
animation: slidetounlock 3s infinite;
background-clip: text;
user-select: none;
&.success {
-webkit-text-fill-color: @white;
}
& > * {
-webkit-text-fill-color: #333;
}
}
&-action {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: move;
background-color: @white;
border-radius: @radius;
justify-content: center;
align-items: center;
&__icon {
cursor: inherit;
}
&.to-left {
left: 0 !important;
transition: left 0.3s;
}
}
}
@keyframes slidetounlock {
0% {
background-position: -120px 0;
}
100% {
background-position: 120px 0;
}
}
</style>

View File

@ -1,214 +0,0 @@
<script lang="tsx">
import type { MoveData, DragVerifyActionType } from './typing'
import { defineComponent, computed, unref, reactive, watch, ref } from 'vue'
import { useTimeoutFn } from '@/hooks/core/useTimeout'
import BasicDragVerify from './DragVerify.vue'
import { hackCss } from '@/utils/domUtils'
import { rotateProps } from './props'
import { useI18n } from '@/hooks/web/useI18n'
export default defineComponent({
name: 'ImgRotateDragVerify',
inheritAttrs: false,
props: rotateProps,
emits: ['success', 'change', 'update:value'],
setup(props, { emit, attrs, expose }) {
const basicRef = ref<Nullable<DragVerifyActionType>>(null)
const state = reactive({
showTip: false,
isPassing: false,
imgStyle: {},
randomRotate: 0,
currentRotate: 0,
toOrigin: false,
startTime: 0,
endTime: 0,
draged: false
})
const { t } = useI18n()
watch(
() => state.isPassing,
(isPassing) => {
if (isPassing) {
const { startTime, endTime } = state
const time = (endTime - startTime) / 1000
emit('success', { isPassing, time: time.toFixed(1) })
emit('change', isPassing)
emit('update:value', isPassing)
}
}
)
const getImgWrapStyleRef = computed(() => {
const { imgWrapStyle, imgWidth } = props
return {
width: `${imgWidth}px`,
height: `${imgWidth}px`,
...imgWrapStyle
}
})
const getFactorRef = computed(() => {
const { minDegree, maxDegree } = props
if (minDegree === maxDegree) {
return Math.floor(1 + Math.random() * 1) / 10 + 1
}
return 1
})
function handleStart() {
state.startTime = new Date().getTime()
}
function handleDragBarMove(data: MoveData) {
state.draged = true
const { imgWidth, height, maxDegree } = props
const { moveX } = data
const currentRotate = Math.ceil((moveX / (imgWidth! - parseInt(height as string))) * maxDegree! * unref(getFactorRef))
state.currentRotate = currentRotate
state.imgStyle = hackCss('transform', `rotateZ(${state.randomRotate - currentRotate}deg)`)
}
function handleImgOnLoad() {
const { minDegree, maxDegree } = props
const ranRotate = Math.floor(minDegree! + Math.random() * (maxDegree! - minDegree!)) //
state.randomRotate = ranRotate
state.imgStyle = hackCss('transform', `rotateZ(${ranRotate}deg)`)
}
function handleDragEnd() {
const { randomRotate, currentRotate } = state
const { diffDegree } = props
if (Math.abs(randomRotate - currentRotate) >= (diffDegree || 20)) {
state.imgStyle = hackCss('transform', `rotateZ(${randomRotate}deg)`)
state.toOrigin = true
useTimeoutFn(() => {
state.toOrigin = false
state.showTip = true
//
}, 300)
} else {
checkPass()
}
state.showTip = true
}
function checkPass() {
state.isPassing = true
state.endTime = new Date().getTime()
}
function resume() {
state.showTip = false
const basicEl = unref(basicRef)
if (!basicEl) {
return
}
state.isPassing = false
basicEl.resume()
handleImgOnLoad()
}
expose({ resume })
// handleImgOnLoad();
return () => {
const { src } = props
const { toOrigin, isPassing, startTime, endTime } = state
const imgCls: string[] = []
if (toOrigin) {
imgCls.push('to-origin')
}
const time = (endTime - startTime) / 1000
return (
<div class="ir-dv">
<div class={`ir-dv-img__wrap`} style={unref(getImgWrapStyleRef)}>
<img
src={src}
onLoad={handleImgOnLoad}
width={parseInt(props.width as string)}
class={imgCls}
style={state.imgStyle}
onClick={() => {
resume()
}}
alt="verify"
/>
{state.showTip && (
<span class={[`ir-dv-img__tip`, state.isPassing ? 'success' : 'error']}>
{state.isPassing ? t('component.verify.time', { time: time.toFixed(1) }) : t('component.verify.error')}
</span>
)}
{!state.showTip && !state.draged && <span class={[`ir-dv-img__tip`, 'normal']}>{t('component.verify.redoTip')}</span>}
</div>
<BasicDragVerify
class={`ir-dv-drag__bar`}
onMove={handleDragBarMove}
onEnd={handleDragEnd}
onStart={handleStart}
ref={basicRef}
{...{ ...attrs, ...props }}
value={isPassing}
isSlot={true}
/>
</div>
)
}
}
})
</script>
<style lang="less">
.ir-dv {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
&-img__wrap {
position: relative;
overflow: hidden;
border-radius: 50%;
img {
width: 100%;
border-radius: 50%;
&.to-origin {
transition: transform 0.3s;
}
}
}
&-img__tip {
position: absolute;
bottom: 10px;
left: 0;
z-index: 1;
display: block;
width: 100%;
height: 30px;
font-size: 12px;
line-height: 30px;
color: @white;
text-align: center;
&.success {
background-color: fade(@success-color, 60%);
}
&.error {
background-color: fade(@error-color, 60%);
}
&.normal {
background-color: rgb(0 0 0 / 30%);
}
}
&-drag__bar {
margin-top: 20px;
}
}
</style>

View File

@ -1,86 +0,0 @@
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const basicProps = {
value: {
type: Boolean as PropType<boolean>,
default: false
},
isSlot: {
type: Boolean as PropType<boolean>,
default: false
},
text: {
type: [String] as PropType<string>,
default: t('component.verify.dragText')
},
successText: {
type: [String] as PropType<string>,
default: t('component.verify.successText')
},
height: {
type: [Number, String] as PropType<number | string>,
default: 40
},
width: {
type: [Number, String] as PropType<number | string>,
default: 220
},
circle: {
type: Boolean as PropType<boolean>,
default: false
},
wrapStyle: {
type: Object as PropType<any>,
default: () => ({})
},
contentStyle: {
type: Object as PropType<any>,
default: () => ({})
},
barStyle: {
type: Object as PropType<any>,
default: () => ({})
},
actionStyle: {
type: Object as PropType<any>,
default: () => ({})
}
}
export const rotateProps = {
...basicProps,
src: {
type: String as PropType<string>
},
imgWidth: {
type: Number as PropType<number>,
default: 260
},
imgWrapStyle: {
type: Object as PropType<any>,
default: () => ({})
},
minDegree: {
type: Number as PropType<number>,
default: 90
},
maxDegree: {
type: Number as PropType<number>,
default: 270
},
diffDegree: {
type: Number as PropType<number>,
default: 20
}
}

View File

@ -1,14 +0,0 @@
export interface DragVerifyActionType {
resume: () => void
}
export interface PassingData {
isPassing: boolean
time: number
}
export interface MoveData {
event: MouseEvent | TouchEvent
moveDistance: number
moveX: number
}

View File

@ -35,11 +35,11 @@ export interface RetryRequest {
count: number
waitTime: number
}
export interface Result<T = any> {
code: number
type: 'success' | 'error' | 'warning'
message: string
result: T
msg: string
data: T
}
// multipart/form-data: upload file

View File

@ -159,6 +159,10 @@ export interface GlobConfig {
urlPrefix?: string
// Project abbreviation
shortName: string
// 租户开关
tenantEnable: string
// 验证码开关
captchaEnable: string
}
export interface GlobEnvConfig {
// Site title
@ -171,4 +175,8 @@ export interface GlobEnvConfig {
VITE_GLOB_APP_SHORT_NAME: string
// Upload url
VITE_GLOB_UPLOAD_URL?: string
// 租户开关
VITE_GLOB_APP_TENANT_ENABLE: string
// 验证码开关
VITE_GLOB_APP_CAPTCHA_ENABLE: string
}

10
src/types/index.d.ts vendored
View File

@ -8,6 +8,16 @@ declare interface PromiseFn<T = any, R = T> {
declare type RefType<T> = T | null
declare interface PageParam {
pageSize?: number
pageNo?: number
}
declare interface PageResult<T = any> {
list: T[]
total: number
}
declare type LabelValueOptions = {
label: string
value: any

View File

@ -1,6 +1,5 @@
import { ErrorTypeEnum } from '@/enums/exceptionEnum'
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum'
import { RoleInfo } from '@/api/sys/model/userModel'
// Lock screen information
export interface LockInfo {
@ -37,7 +36,7 @@ export interface UserInfo {
avatar: string
desc?: string
homePath?: string
roles: RoleInfo[]
roles: string[]
}
export interface BeforeMiniState {
@ -46,3 +45,8 @@ export interface BeforeMiniState {
menuMode?: MenuModeEnum
menuType?: MenuTypeEnum
}
export interface DictState {
dictMap: Map<string, any>
isSetDict: boolean
}

View File

@ -5,43 +5,47 @@ export {}
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> {
orderNo?: number
// title
// 路由title 一般必填
title: string
// dynamic router level.
// 动态路由可打开Tab页数
dynamicLevel?: number
// dynamic router real route path (For performance).
// 动态路由的实际Path, 即去除路由的动态部分;
realPath?: string
// Whether to ignore permissions
// 是否忽略权限只在权限模式为Role的时候有效
ignoreAuth?: boolean
// role info
// 可以访问的角色只在权限模式为Role的时候有效
roles?: RoleEnum[]
// Whether not to cache
// 是否忽略KeepAlive缓存
ignoreKeepAlive?: boolean
// Is it fixed on tab
// 是否固定标签
affix?: boolean
// icon on tab
// 图标,也是菜单图标
icon?: string
// 内嵌iframe的地址
frameSrc?: string
// current page transition
// 指定该路由切换的动画名
transitionName?: string
// Whether the route has been dynamically added
// 隐藏该路由在面包屑上面的显示
hideBreadcrumb?: boolean
// Hide submenu
// 如果该路由会携带参数且需要在tab页上面显示。则需要设置为true
carryParam?: boolean
// 隐藏所有子菜单
hideChildrenInMenu?: boolean
// Carrying parameters
carryParam?: boolean
// Used internally to mark single-level menus
single?: boolean
// Currently active menu
// 当前激活的菜单。用于配置详情页时左侧激活的菜单路径
currentActiveMenu?: string
// Never show in tab
// 当前路由不再标签页显示
hideTab?: boolean
// Never show in menu
// 当前路由不再菜单显示
hideMenu?: boolean
isLink?: boolean
// only build for Menu
// 菜单排序,只对第一级有效
orderNo?: number
// 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下生成对应的菜单而忽略路由。2.5.3以上版本有效
ignoreRoute?: boolean
// Hide path for children
// 是否在子级菜单的完整path中忽略本级path。2.5.3以上版本有效
hidePathForChildren?: boolean
}
}

View File

@ -8,11 +8,19 @@ import { isFunction } from '@/utils/is'
import { cloneDeep } from 'lodash-es'
import { ContentTypeEnum } from '@/enums/httpEnum'
import { RequestEnum } from '@/enums/httpEnum'
import { downloadByData } from '@/utils/file/download'
import { useGlobSetting } from '@/hooks/setting'
import { setAccessToken, getRefreshToken, getTenantId, getAccessToken } from '@/utils/auth'
export * from './axiosTransform'
const globSetting = useGlobSetting()
// 请求队列
let requestList: any[] = []
// 是否正在刷新中
let isRefreshToken = false
/**
* @description: axios module
* @description: axios
*/
export class VAxios {
private axiosInstance: AxiosInstance
@ -25,7 +33,7 @@ export class VAxios {
}
/**
* @description: Create axios instance
* @description: axios
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config)
@ -39,9 +47,13 @@ export class VAxios {
getAxios(): AxiosInstance {
return this.axiosInstance
}
refreshToken() {
axios.defaults.headers.common['tenant-id'] = getTenantId() as number
return axios.post(globSetting.apiUrl + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
}
/**
* @description: Reconfigure axios
* @description: axios
*/
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
@ -51,7 +63,7 @@ export class VAxios {
}
/**
* @description: Set general header
* @description:
*/
setHeader(headers: any): void {
if (!this.axiosInstance) {
@ -86,13 +98,54 @@ export class VAxios {
return config
}, undefined)
// Request interceptor error capture
// 请求拦截器错误捕获
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch)
// Response result interceptor processing
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
// 响应结果拦截器处理
this.axiosInstance.interceptors.response.use(async (res: AxiosResponse<any>) => {
const config = res.config
if (res.data.code === 401) {
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true
// 1. 获取到刷新token
if (getRefreshToken()) {
// 2. 进行刷新访问令牌
console.info('进行刷新访问令牌')
try {
const refreshTokenRes = await this.refreshToken()
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setAccessToken(refreshTokenRes.data.data)
;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb: any) => {
cb()
})
requestList = []
// TODO
// res = await Promise.all([this.axiosInstance(config)])[0]
console.info('刷新令牌end', res)
} catch (e) {
console.info(e)
requestList.forEach((cb: any) => {
cb()
})
} finally {
requestList = []
isRefreshToken = false
}
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(this.axiosInstance(config))
})
})
}
}
res && axiosCanceler.removePending(res.config)
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res)
@ -100,7 +153,7 @@ export class VAxios {
return res
}, undefined)
// Response result interceptor error capture
// 响应结果拦截器错误捕获
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, (error) => {
@ -110,7 +163,7 @@ export class VAxios {
}
/**
* @description: File Upload
* @description:
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData()
@ -142,12 +195,13 @@ export class VAxios {
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
// @ts-ignore
ignoreCancelToken: true
}
})
}
// support form-data
// 支持表单数据
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers
const contentType = headers?.['Content-Type'] || headers?.['content-type']
@ -182,6 +236,94 @@ export class VAxios {
return this.request({ ...config, method: 'DELETE' }, options)
}
download<T = any>(config: AxiosRequestConfig, title: string, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep({
...config,
method: 'GET',
responseType: 'blob'
})
const transform = this.getTransform()
const { requestOptions } = this.options
const opt: RequestOptions = Object.assign({}, requestOptions, options)
const { beforeRequestHook, requestCatchHook } = transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt)
}
conf.requestOptions = opt
conf = this.supportFormData(conf)
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
resolve(res as unknown as Promise<T>)
// download file
if (typeof res != undefined) {
downloadByData(res?.data as unknown as BlobPart, title)
}
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt))
return
}
if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
}
reject(e)
})
})
}
export<T = any>(config: AxiosRequestConfig, title: string, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep({
...config,
method: 'POST',
responseType: 'blob'
})
const transform = this.getTransform()
const { requestOptions } = this.options
const opt: RequestOptions = Object.assign({}, requestOptions, options)
const { beforeRequestHook, requestCatchHook } = transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt)
}
conf.requestOptions = opt
conf = this.supportFormData(conf)
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
resolve(res as unknown as Promise<T>)
// download file
if (typeof res != undefined) {
downloadByData(res?.data as unknown as BlobPart, title)
}
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt))
return
}
if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
}
reject(e)
})
})
}
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config)
// cancelToken 如果被深拷贝会导致最外层无法使用cancel方法来取消请求
@ -223,7 +365,7 @@ export class VAxios {
return
}
if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
// 在此处重写来自 axios 的错误消息
}
reject(e)
})

View File

@ -9,7 +9,7 @@ export const getPendingUrl = (config: InternalAxiosRequestConfig) => [config.met
export class AxiosCanceler {
/**
* Add request
*
* @param {Object} config
*/
addPending(config: InternalAxiosRequestConfig) {
@ -19,14 +19,14 @@ export class AxiosCanceler {
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
// If there is no current request in pending, add it
// 如果当前没有待处理的请求,添加它
pendingMap.set(url, cancel)
}
})
}
/**
* @description: Clear all pending
* @description:
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
@ -36,15 +36,14 @@ export class AxiosCanceler {
}
/**
* Removal request
*
* @param {Object} config
*/
removePending(config: InternalAxiosRequestConfig) {
const url = getPendingUrl(config)
if (pendingMap.has(url)) {
// If there is a current request identifier in pending,
// the current request needs to be cancelled and removed
// 如果有当前请求标识符处于pending状态则需要取消当前请求并移除
const cancel = pendingMap.get(url)
cancel && cancel(url)
pendingMap.delete(url)

View File

@ -12,8 +12,7 @@ export interface CreateAxiosOptions extends AxiosRequestConfig {
export abstract class AxiosTransform {
/**
* @description: Process configuration before request
* @description: Process configuration before request
* @description:
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig

View File

@ -24,7 +24,7 @@ export function checkStatus(status: number, msg: string, errorMessageMode: Error
// Jump to the login page if not logged in, and carry the path of the current page
// Return to the current page after successful login. This step needs to be operated on the login page.
case 401:
userStore.setToken(undefined)
userStore.setAccessToken(undefined)
errMessage = msg || t('sys.api.errMsg401')
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
userStore.setSessionTimeout(true)

View File

@ -16,7 +16,7 @@ export function joinTimestamp(join: boolean, restful = false): string | object {
}
/**
* @description: Format request parameter time
* @description:
*/
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {

View File

@ -10,8 +10,8 @@ import { checkStatus } from './checkStatus'
import { useGlobSetting } from '@/hooks/setting'
import { useMessage } from '@/hooks/web/useMessage'
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum'
import { isString, isUnDef, isNull, isEmpty } from '@/utils/is'
import { getToken } from '@/utils/auth'
import { isEmpty, isNull, isString, isUnDef } from '@/utils/is'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { setObjToUrlParams, deepMerge } from '@/utils'
import { useErrorLogStoreWithOut } from '@/store/modules/errorLog'
import { useI18n } from '@/hooks/web/useI18n'
@ -22,8 +22,12 @@ import axios from 'axios'
const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix
const tenantEnable = globSetting.tenantEnable
const { createMessage, createErrorModal, createSuccessModal } = useMessage()
// 请求白名单无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
/**
* @description: 便
*/
@ -34,6 +38,10 @@ const transform: AxiosTransform = {
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { t } = useI18n()
const { isTransformResponse, isReturnNativeResponse } = options
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res
@ -50,18 +58,20 @@ const transform: AxiosTransform = {
// return '[HTTP] Request has no return value';
throw new Error(t('sys.api.apiRequestFailed'))
}
console.info(data)
// 这里 coderesultmessage为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, result, message } = data
const { code, data: result, msg } = data
console.info(result)
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
if (hasSuccess) {
let successMsg = message
let successMsg = msg
if (successMsg === null || successMsg === undefined || successMsg === '') {
successMsg = t('sys.api.operationSuccess')
}
if (isNull(successMsg) || isUnDef(successMsg) || isEmpty(successMsg)) {
successMsg = t(`sys.api.operationSuccess`)
}
if (options.successMessageMode === 'modal') {
createSuccessModal({ title: t('sys.api.successTip'), content: successMsg })
} else if (options.successMessageMode === 'message') {
@ -74,19 +84,19 @@ const transform: AxiosTransform = {
// 如果不希望中断当前请求请return数据否则直接抛出异常即可
let timeoutMsg = ''
switch (code) {
case ResultEnum.TIMEOUT:
case ResultEnum.UNAUTHORIZED:
timeoutMsg = t('sys.api.timeoutMessage')
const userStore = useUserStoreWithOut()
userStore.setToken(undefined)
userStore.setAccessToken(undefined)
userStore.logout(true)
break
default:
if (message) {
timeoutMsg = message
if (msg) {
timeoutMsg = msg
}
}
// errorMessageMode='modal'的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
// errorMessageMode='modal' 的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
if (options.errorMessageMode === 'modal') {
createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg })
@ -147,12 +157,25 @@ const transform: AxiosTransform = {
* @description:
*/
requestInterceptors: (config, options) => {
// 是否需要设置 token
let isToken = (config as Recordable)?.requestOptions?.withToken == false
whiteList.some((v) => {
if (config.url) {
config.url.indexOf(v) > -1
return (isToken = false)
}
})
// 请求之前处理config
const token = getToken()
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
const token = getAccessToken()
if (token && !isToken) {
// jwt token
;(config as Recordable).headers.Authorization = options.authenticationScheme ? `${options.authenticationScheme} ${token}` : token
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId
}
return config
},
@ -221,7 +244,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// authentication schemese.g: Bearer
// authenticationScheme: 'Bearer',
authenticationScheme: '',
authenticationScheme: 'Bearer',
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,

View File

@ -14,7 +14,7 @@
<div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
<AppLogo class="-enter-x" />
<div class="my-auto">
<img :alt="title" src="../../../assets/svg/login-box-bg.svg" class="w-1/2 -mt-16 -enter-x" />
<img :alt="title" src="@/assets/svg/login-box-bg.svg" class="w-1/2 -mt-16 -enter-x" />
<div class="mt-10 font-medium text-white -enter-x">
<span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
</div>

View File

@ -1,8 +1,17 @@
<template>
<LoginFormTitle v-show="getShow" class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef" v-show="getShow" @keypress.enter="handleLogin">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" class="fix-auto-fill" />
<FormItem name="tenantName" class="enter-x">
<Input
v-if="tenantEnable === 'true'"
size="large"
v-model:value="formData.tenantName"
:placeholder="t('sys.login.tenantName')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="username" class="enter-x">
<Input size="large" v-model:value="formData.username" :placeholder="t('sys.login.userName')" class="fix-auto-fill" />
</FormItem>
<FormItem name="password" class="enter-x">
<InputPassword size="large" visibilityToggle v-model:value="formData.password" :placeholder="t('sys.login.password')" />
@ -28,7 +37,7 @@
</ARow>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
<Button type="primary" size="large" block @click="getCode" :loading="loading">
{{ t('sys.login.loginButton') }}
</Button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
@ -47,9 +56,9 @@
</Button>
</ACol>
<ACol :md="6" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
<a-button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</Button>
</a-button>
</ACol>
</ARow>
@ -63,6 +72,7 @@
<TwitterCircleFilled />
</div>
</Form>
<Verify ref="verify" mode="pop" :captchaType="captchaType" :imgSize="{ width: '400px', height: '200px' }" @success="handleLogin" />
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed } from 'vue'
@ -75,18 +85,29 @@ import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'
import { useGlobSetting } from '@/hooks/setting'
import { useDesign } from '@/hooks/web/useDesign'
//import { onKeyStroke } from '@vueuse/core';
import * as authUtil from '@/utils/auth'
import { Verify } from '@/components/Verifition'
import { getTenantIdByName } from '@/api/base/login'
const ACol = Col
const ARow = Row
const FormItem = Form.Item
const InputPassword = Input.Password
const { t } = useI18n()
const { notification, createErrorModal } = useMessage()
const { prefixCls } = useDesign('login')
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const { tenantEnable, captchaEnable } = useGlobSetting()
const { setLoginState, getLoginState } = useLoginState()
const { getFormRules } = useFormRules()
@ -95,9 +116,14 @@ const formRef = ref()
const loading = ref(false)
const rememberMe = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const formData = reactive({
account: 'vben',
password: '123456'
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
captchaVerification: ''
})
const { validForm } = useFormValid(formRef)
@ -106,20 +132,43 @@ const { validForm } = useFormValid(formRef)
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
async function handleLogin() {
//
async function getCode() {
//
if (captchaEnable === 'false') {
await handleLogin({})
} else {
//
//
verify.value.show()
}
}
//ID
async function getTenantId() {
if (tenantEnable === 'true') {
const res = await getTenantIdByName(formData.tenantName)
authUtil.setTenantId(res)
}
}
async function handleLogin(params) {
await getTenantId()
const data = await validForm()
if (!data) return
try {
loading.value = true
const userInfo = await userStore.login({
password: data.password,
username: data.account,
username: data.username,
captchaVerification: params.captchaVerification,
mode: 'none' //
})
if (userInfo) {
await permissionStore.changePermissionCode(userInfo.permissions)
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.nickname}`,
duration: 3
})
}

View File

@ -48,6 +48,5 @@ const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
async function handleLogin() {
const data = await validForm()
if (!data) return
console.log(data)
}
</script>

View File

@ -18,7 +18,7 @@ import { QrCode } from '@/components/Qrcode'
import { useI18n } from '@/hooks/web/useI18n'
import { useLoginState, LoginStateEnum } from './useLogin'
const qrCodeUrl = 'https://vben.vvbin.cn/login'
const qrCodeUrl = 'https://vben.xingyuv.com/login'
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()

View File

@ -73,6 +73,5 @@ const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
async function handleRegister() {
const data = await validForm()
if (!data) return
console.log(data)
}
</script>

View File

@ -26,12 +26,11 @@ const isBackMode = () => {
onMounted(() => {
// UserId
userId.value = userStore.getUserInfo?.userId
console.log('Mounted', userStore.getUserInfo)
userId.value = userStore.getUserInfo?.user.id
})
onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.userId) {
if (userId.value && userId.value !== userStore.getUserInfo.user.id) {
// 便
document.location.reload()
} else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {