feat: axios
parent
d430f6d272
commit
3a39b2afb7
13
.env
13
.env
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
|
# 本地开发环境
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
# 资源公共路径,需要以 /开头和结尾
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# 本地开发代理,可以解决跨域及多地址代理
|
# 本地开发代理,可以解决跨域及多地址代理
|
||||||
# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题
|
# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题
|
||||||
# 可以有多个,注意多个不能换行,否则代理将会失效
|
# 可以有多个,注意多个不能换行,否则代理将会失效
|
||||||
VITE_PROXY = [["/basic-api","http://localhost:3000"],["/upload","http://localhost:3300/upload"]]
|
VITE_PROXY = [["/dev-api","http://localhost:48080"],["/upload","http://localhost:48080/admin-api/infra/file/upload"]]
|
||||||
# VITE_PROXY=[["/api","https://xingyuv.com/test"]]
|
# VITE_PROXY=[["/api","http://vben.xingyuv.com/test"]]
|
||||||
|
|
||||||
# 是否删除Console.log
|
# 是否删除Console.log
|
||||||
VITE_DROP_CONSOLE = false
|
VITE_DROP_CONSOLE = false
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
# 如果没有跨域问题,直接在这里配置即可
|
# 如果没有跨域问题,直接在这里配置即可
|
||||||
VITE_GLOB_API_URL = /basic-api
|
VITE_GLOB_API_URL = "http://localhost:48080/admin-api"
|
||||||
|
|
||||||
# 文件上传接口 可选
|
# 文件上传接口 可选
|
||||||
VITE_GLOB_UPLOAD_URL = /upload
|
VITE_GLOB_UPLOAD_URL = /upload
|
||||||
|
|
|
@ -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 =
|
|
@ -13,13 +13,13 @@ VITE_BUILD_COMPRESS = 'gzip'
|
||||||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||||
|
|
||||||
# 接口地址 可以由nginx做转发或者直接写实际地址
|
# 接口地址 可以由nginx做转发或者直接写实际地址
|
||||||
VITE_GLOB_API_URL = /basic-api
|
VITE_GLOB_API_URL = /admin-api
|
||||||
|
|
||||||
# 文件上传地址 可以由nginx做转发或者直接写实际地址
|
# 文件上传地址 可以由nginx做转发或者直接写实际地址
|
||||||
VITE_GLOB_UPLOAD_URL = /upload
|
VITE_GLOB_UPLOAD_URL = /upload
|
||||||
|
|
||||||
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
|
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
|
||||||
VITE_GLOB_API_URL_PREFIX =
|
VITE_GLOB_API_URL_PREFIX =
|
||||||
|
|
||||||
# 打包是否开启pwa功能
|
# 打包是否开启pwa功能
|
||||||
VITE_USE_PWA = false
|
VITE_USE_PWA = false
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# 资源公共路径,需要以 / 开头和结尾
|
# 资源公共路径,需要以 / 开头和结尾
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ VITE_BUILD_COMPRESS = 'none'
|
||||||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||||
|
|
||||||
# 接口地址 可以由nginx做转发或者直接写实际地址
|
# 接口地址 可以由nginx做转发或者直接写实际地址
|
||||||
VITE_GLOB_API_URL = /basic-api
|
VITE_GLOB_API_URL = /admin-api
|
||||||
|
|
||||||
# 文件上传地址 可以由nginx做转发或者直接写实际地址
|
# 文件上传地址 可以由nginx做转发或者直接写实际地址
|
||||||
VITE_GLOB_UPLOAD_URL = /upload
|
VITE_GLOB_UPLOAD_URL = /upload
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "vben-admin",
|
"name": "ruoyi-ui-admin-vben",
|
||||||
"version": "1.0.2",
|
"version": "1.0.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "xingyu4j",
|
"name": "xingyu4j",
|
||||||
"email": "xingyu4j@vip.qq.com",
|
"email": "xingyu4j@vip.qq.com",
|
||||||
|
@ -34,8 +34,6 @@
|
||||||
"@ant-design/colors": "^7.0.0",
|
"@ant-design/colors": "^7.0.0",
|
||||||
"@ant-design/icons-vue": "^6.1.0",
|
"@ant-design/icons-vue": "^6.1.0",
|
||||||
"@iconify/iconify": "^3.1.0",
|
"@iconify/iconify": "^3.1.0",
|
||||||
"@logicflow/core": "^1.2.1",
|
|
||||||
"@logicflow/extension": "^1.2.1",
|
|
||||||
"@vue/runtime-core": "^3.2.47",
|
"@vue/runtime-core": "^3.2.47",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"@zxcvbn-ts/core": "^2.2.1",
|
"@zxcvbn-ts/core": "^2.2.1",
|
||||||
|
@ -55,7 +53,6 @@
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"qs": "^6.11.1",
|
"qs": "^6.11.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"showdown": "^2.1.0",
|
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"tinymce": "^5.10.7",
|
"tinymce": "^5.10.7",
|
||||||
"vditor": "^3.9.1",
|
"vditor": "^3.9.1",
|
||||||
|
@ -82,7 +79,6 @@
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/showdown": "^2.0.0",
|
|
||||||
"@types/sortablejs": "^1.15.1",
|
"@types/sortablejs": "^1.15.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
"@typescript-eslint/parser": "^5.55.0",
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
|
@ -91,6 +87,7 @@
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
"@vue/compiler-sfc": "^3.2.47",
|
"@vue/compiler-sfc": "^3.2.47",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
"consola": "^2.15.3",
|
||||||
"conventional-changelog-cli": "^2.2.2",
|
"conventional-changelog-cli": "^2.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cz-git": "^1.6.0",
|
"cz-git": "^1.6.0",
|
||||||
|
@ -106,7 +103,6 @@
|
||||||
"inquirer": "^9.1.4",
|
"inquirer": "^9.1.4",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.2.0",
|
"lint-staged": "^13.2.0",
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
|
@ -122,7 +118,6 @@
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-order": "^6.0.2",
|
"stylelint-order": "^6.0.2",
|
||||||
"terser": "^5.16.6",
|
"terser": "^5.16.6",
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"unplugin-vue-setup-extend-plus": "^0.4.9",
|
"unplugin-vue-setup-extend-plus": "^0.4.9",
|
||||||
"vite": "^4.2.0",
|
"vite": "^4.2.0",
|
||||||
|
|
166
pnpm-lock.yaml
166
pnpm-lock.yaml
|
@ -8,8 +8,6 @@ specifiers:
|
||||||
'@iconify/iconify': ^3.1.0
|
'@iconify/iconify': ^3.1.0
|
||||||
'@iconify/json': ^2.2.36
|
'@iconify/json': ^2.2.36
|
||||||
'@kirklin/vite-plugin-vben-theme': ^0.1.1
|
'@kirklin/vite-plugin-vben-theme': ^0.1.1
|
||||||
'@logicflow/core': ^1.2.1
|
|
||||||
'@logicflow/extension': ^1.2.1
|
|
||||||
'@purge-icons/generated': ^0.9.0
|
'@purge-icons/generated': ^0.9.0
|
||||||
'@types/codemirror': ^5.60.5
|
'@types/codemirror': ^5.60.5
|
||||||
'@types/crypto-js': ^4.1.1
|
'@types/crypto-js': ^4.1.1
|
||||||
|
@ -21,7 +19,6 @@ specifiers:
|
||||||
'@types/nprogress': ^0.2.0
|
'@types/nprogress': ^0.2.0
|
||||||
'@types/qrcode': ^1.5.0
|
'@types/qrcode': ^1.5.0
|
||||||
'@types/qs': ^6.9.7
|
'@types/qs': ^6.9.7
|
||||||
'@types/showdown': ^2.0.0
|
|
||||||
'@types/sortablejs': ^1.15.1
|
'@types/sortablejs': ^1.15.1
|
||||||
'@typescript-eslint/eslint-plugin': ^5.55.0
|
'@typescript-eslint/eslint-plugin': ^5.55.0
|
||||||
'@typescript-eslint/parser': ^5.55.0
|
'@typescript-eslint/parser': ^5.55.0
|
||||||
|
@ -36,6 +33,7 @@ specifiers:
|
||||||
autoprefixer: ^10.4.14
|
autoprefixer: ^10.4.14
|
||||||
axios: ^1.3.4
|
axios: ^1.3.4
|
||||||
codemirror: ^5.65.3
|
codemirror: ^5.65.3
|
||||||
|
consola: ^2.15.3
|
||||||
conventional-changelog-cli: ^2.2.2
|
conventional-changelog-cli: ^2.2.2
|
||||||
cropperjs: ^1.5.13
|
cropperjs: ^1.5.13
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
|
@ -57,7 +55,6 @@ specifiers:
|
||||||
less: ^4.1.3
|
less: ^4.1.3
|
||||||
lint-staged: ^13.2.0
|
lint-staged: ^13.2.0
|
||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
npm-run-all: ^4.1.5
|
|
||||||
nprogress: ^0.2.0
|
nprogress: ^0.2.0
|
||||||
path-to-regexp: ^6.2.1
|
path-to-regexp: ^6.2.1
|
||||||
picocolors: ^1.0.0
|
picocolors: ^1.0.0
|
||||||
|
@ -73,7 +70,6 @@ specifiers:
|
||||||
rimraf: ^4.4.0
|
rimraf: ^4.4.0
|
||||||
rollup: ^3.19.1
|
rollup: ^3.19.1
|
||||||
rollup-plugin-visualizer: ^5.9.0
|
rollup-plugin-visualizer: ^5.9.0
|
||||||
showdown: ^2.1.0
|
|
||||||
sortablejs: ^1.15.0
|
sortablejs: ^1.15.0
|
||||||
stylelint: ^14.16.1
|
stylelint: ^14.16.1
|
||||||
stylelint-config-prettier: ^9.0.5
|
stylelint-config-prettier: ^9.0.5
|
||||||
|
@ -83,7 +79,6 @@ specifiers:
|
||||||
stylelint-order: ^6.0.2
|
stylelint-order: ^6.0.2
|
||||||
terser: ^5.16.6
|
terser: ^5.16.6
|
||||||
tinymce: ^5.10.7
|
tinymce: ^5.10.7
|
||||||
ts-node: ^10.9.1
|
|
||||||
typescript: ^5.0.2
|
typescript: ^5.0.2
|
||||||
unplugin-vue-setup-extend-plus: ^0.4.9
|
unplugin-vue-setup-extend-plus: ^0.4.9
|
||||||
vditor: ^3.9.1
|
vditor: ^3.9.1
|
||||||
|
@ -110,8 +105,6 @@ dependencies:
|
||||||
'@ant-design/colors': 7.0.0
|
'@ant-design/colors': 7.0.0
|
||||||
'@ant-design/icons-vue': 6.1.0_vue@3.2.47
|
'@ant-design/icons-vue': 6.1.0_vue@3.2.47
|
||||||
'@iconify/iconify': 3.1.0
|
'@iconify/iconify': 3.1.0
|
||||||
'@logicflow/core': 1.2.1
|
|
||||||
'@logicflow/extension': 1.2.1
|
|
||||||
'@vue/runtime-core': 3.2.47
|
'@vue/runtime-core': 3.2.47
|
||||||
'@vueuse/core': 9.13.0_vue@3.2.47
|
'@vueuse/core': 9.13.0_vue@3.2.47
|
||||||
'@zxcvbn-ts/core': 2.2.1
|
'@zxcvbn-ts/core': 2.2.1
|
||||||
|
@ -131,7 +124,6 @@ dependencies:
|
||||||
qrcode: 1.5.1
|
qrcode: 1.5.1
|
||||||
qs: 6.11.1
|
qs: 6.11.1
|
||||||
resize-observer-polyfill: 1.5.1
|
resize-observer-polyfill: 1.5.1
|
||||||
showdown: 2.1.0
|
|
||||||
sortablejs: 1.15.0
|
sortablejs: 1.15.0
|
||||||
tinymce: 5.10.7
|
tinymce: 5.10.7
|
||||||
vditor: 3.9.1
|
vditor: 3.9.1
|
||||||
|
@ -158,7 +150,6 @@ devDependencies:
|
||||||
'@types/nprogress': 0.2.0
|
'@types/nprogress': 0.2.0
|
||||||
'@types/qrcode': 1.5.0
|
'@types/qrcode': 1.5.0
|
||||||
'@types/qs': 6.9.7
|
'@types/qs': 6.9.7
|
||||||
'@types/showdown': 2.0.0
|
|
||||||
'@types/sortablejs': 1.15.1
|
'@types/sortablejs': 1.15.1
|
||||||
'@typescript-eslint/eslint-plugin': 5.55.0_qsnvknysi52qtaxqdyqyohkcku
|
'@typescript-eslint/eslint-plugin': 5.55.0_qsnvknysi52qtaxqdyqyohkcku
|
||||||
'@typescript-eslint/parser': 5.55.0_j4766f7ecgqbon3u7zlxn5zszu
|
'@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
|
'@vitejs/plugin-vue-jsx': 3.0.1_vite@4.2.0+vue@3.2.47
|
||||||
'@vue/compiler-sfc': 3.2.47
|
'@vue/compiler-sfc': 3.2.47
|
||||||
autoprefixer: 10.4.14_postcss@8.4.21
|
autoprefixer: 10.4.14_postcss@8.4.21
|
||||||
|
consola: 2.15.3
|
||||||
conventional-changelog-cli: 2.2.2
|
conventional-changelog-cli: 2.2.2
|
||||||
cross-env: 7.0.3
|
cross-env: 7.0.3
|
||||||
cz-git: 1.6.0
|
cz-git: 1.6.0
|
||||||
|
@ -182,7 +174,6 @@ devDependencies:
|
||||||
inquirer: 9.1.4
|
inquirer: 9.1.4
|
||||||
less: 4.1.3
|
less: 4.1.3
|
||||||
lint-staged: 13.2.0
|
lint-staged: 13.2.0
|
||||||
npm-run-all: 4.1.5
|
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
postcss: 8.4.21
|
postcss: 8.4.21
|
||||||
postcss-html: 1.5.0
|
postcss-html: 1.5.0
|
||||||
|
@ -198,7 +189,6 @@ devDependencies:
|
||||||
stylelint-config-standard: 29.0.0_stylelint@14.16.1
|
stylelint-config-standard: 29.0.0_stylelint@14.16.1
|
||||||
stylelint-order: 6.0.3_stylelint@14.16.1
|
stylelint-order: 6.0.3_stylelint@14.16.1
|
||||||
terser: 5.16.6
|
terser: 5.16.6
|
||||||
ts-node: 10.9.1_sxidjv3cojnrggmso45tj7hldi
|
|
||||||
typescript: 5.0.2
|
typescript: 5.0.2
|
||||||
unplugin-vue-setup-extend-plus: 0.4.9
|
unplugin-vue-setup-extend-plus: 0.4.9
|
||||||
vite: 4.2.0_kfn5zdpk76mco3hnivyqwkouli
|
vite: 4.2.0_kfn5zdpk76mco3hnivyqwkouli
|
||||||
|
@ -2062,23 +2052,6 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
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:
|
/@nodelib/fs.scandir/2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -2423,10 +2396,6 @@ packages:
|
||||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/mousetrap/1.6.11:
|
|
||||||
resolution: {integrity: sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/node/10.17.60:
|
/@types/node/10.17.60:
|
||||||
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2467,10 +2436,6 @@ packages:
|
||||||
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/showdown/2.0.0:
|
|
||||||
resolution: {integrity: sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/sortablejs/1.15.1:
|
/@types/sortablejs/1.15.1:
|
||||||
resolution: {integrity: sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ==}
|
resolution: {integrity: sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3609,11 +3574,6 @@ packages:
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/commander/9.5.0:
|
|
||||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
|
||||||
engines: {node: ^12.20.0 || >=14}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/common-tags/1.8.2:
|
/common-tags/1.8.2:
|
||||||
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
|
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
@ -3929,17 +3889,6 @@ packages:
|
||||||
- encoding
|
- encoding
|
||||||
dev: true
|
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:
|
/cross-spawn/7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -5561,10 +5510,6 @@ packages:
|
||||||
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ids/1.0.0:
|
|
||||||
resolution: {integrity: sha512-Zvtq1xUto4LttpstyOlFum8lKx+i1OmRfg+6A9drFS9iSZsDPMHG4Sof/qwNR4kCU7jBeWFPrY2ocHxiz7cCRw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ieee754/1.2.1:
|
/ieee754/1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -6424,11 +6369,6 @@ packages:
|
||||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/memorystream/0.3.1:
|
|
||||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
|
||||||
engines: {node: '>= 0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/meow/8.1.2:
|
/meow/8.1.2:
|
||||||
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -6600,10 +6540,6 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/mousetrap/1.6.5:
|
|
||||||
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ms/2.0.0:
|
/ms/2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -6679,10 +6615,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/nice-try/1.0.5:
|
|
||||||
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/no-case/3.0.4:
|
/no-case/3.0.4:
|
||||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6754,22 +6686,6 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
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:
|
/npm-run-path/4.0.1:
|
||||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -7027,11 +6943,6 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
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:
|
/path-key/3.1.1:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -7086,12 +6997,6 @@ packages:
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/pidtree/0.3.1:
|
|
||||||
resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==}
|
|
||||||
engines: {node: '>=0.10'}
|
|
||||||
hasBin: true
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pidtree/0.6.0:
|
/pidtree/0.6.0:
|
||||||
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
@ -7259,10 +7164,6 @@ packages:
|
||||||
posthtml-render: 1.4.0
|
posthtml-render: 1.4.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/preact/10.12.1:
|
|
||||||
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/prelude-ls/1.2.1:
|
/prelude-ls/1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
@ -7766,13 +7667,6 @@ packages:
|
||||||
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
||||||
dev: false
|
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:
|
/shebang-command/2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -7780,27 +7674,11 @@ packages:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
dev: true
|
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:
|
/shebang-regex/3.0.0:
|
||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
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:
|
/side-channel/1.0.4:
|
||||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8039,15 +7917,6 @@ packages:
|
||||||
side-channel: 1.0.4
|
side-channel: 1.0.4
|
||||||
dev: true
|
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:
|
/string.prototype.trimend/1.0.6:
|
||||||
resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
|
resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8503,37 +8372,6 @@ packages:
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
dev: true
|
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:
|
/tslib/1.14.1:
|
||||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { withInstall } from '@/utils'
|
|
||||||
import flowChart from './src/FlowChart.vue'
|
|
||||||
|
|
||||||
export const FlowChart = withInstall(flowChart)
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
@ -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({
|
||||||
|
//弹出式pop,固定fixed
|
||||||
|
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>
|
|
@ -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'
|
||||||
|
},
|
||||||
|
//弹出式pop,固定fixed
|
||||||
|
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>
|
|
@ -0,0 +1,4 @@
|
||||||
|
import VerifySlide from './VerifySlide.vue'
|
||||||
|
import VerifyPoints from './VerifyPoints.vue'
|
||||||
|
|
||||||
|
export { VerifySlide, VerifyPoints }
|
|
@ -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()
|
||||||
|
}
|
|
@ -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']
|
|
@ -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'
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -35,11 +35,11 @@ export interface RetryRequest {
|
||||||
count: number
|
count: number
|
||||||
waitTime: number
|
waitTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Result<T = any> {
|
export interface Result<T = any> {
|
||||||
code: number
|
code: number
|
||||||
type: 'success' | 'error' | 'warning'
|
msg: string
|
||||||
message: string
|
data: T
|
||||||
result: T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// multipart/form-data: upload file
|
// multipart/form-data: upload file
|
||||||
|
|
|
@ -159,6 +159,10 @@ export interface GlobConfig {
|
||||||
urlPrefix?: string
|
urlPrefix?: string
|
||||||
// Project abbreviation
|
// Project abbreviation
|
||||||
shortName: string
|
shortName: string
|
||||||
|
// 租户开关
|
||||||
|
tenantEnable: string
|
||||||
|
// 验证码开关
|
||||||
|
captchaEnable: string
|
||||||
}
|
}
|
||||||
export interface GlobEnvConfig {
|
export interface GlobEnvConfig {
|
||||||
// Site title
|
// Site title
|
||||||
|
@ -171,4 +175,8 @@ export interface GlobEnvConfig {
|
||||||
VITE_GLOB_APP_SHORT_NAME: string
|
VITE_GLOB_APP_SHORT_NAME: string
|
||||||
// Upload url
|
// Upload url
|
||||||
VITE_GLOB_UPLOAD_URL?: string
|
VITE_GLOB_UPLOAD_URL?: string
|
||||||
|
// 租户开关
|
||||||
|
VITE_GLOB_APP_TENANT_ENABLE: string
|
||||||
|
// 验证码开关
|
||||||
|
VITE_GLOB_APP_CAPTCHA_ENABLE: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,16 @@ declare interface PromiseFn<T = any, R = T> {
|
||||||
|
|
||||||
declare type RefType<T> = T | null
|
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 = {
|
declare type LabelValueOptions = {
|
||||||
label: string
|
label: string
|
||||||
value: any
|
value: any
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { ErrorTypeEnum } from '@/enums/exceptionEnum'
|
import { ErrorTypeEnum } from '@/enums/exceptionEnum'
|
||||||
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum'
|
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum'
|
||||||
import { RoleInfo } from '@/api/sys/model/userModel'
|
|
||||||
|
|
||||||
// Lock screen information
|
// Lock screen information
|
||||||
export interface LockInfo {
|
export interface LockInfo {
|
||||||
|
@ -37,7 +36,7 @@ export interface UserInfo {
|
||||||
avatar: string
|
avatar: string
|
||||||
desc?: string
|
desc?: string
|
||||||
homePath?: string
|
homePath?: string
|
||||||
roles: RoleInfo[]
|
roles: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BeforeMiniState {
|
export interface BeforeMiniState {
|
||||||
|
@ -46,3 +45,8 @@ export interface BeforeMiniState {
|
||||||
menuMode?: MenuModeEnum
|
menuMode?: MenuModeEnum
|
||||||
menuType?: MenuTypeEnum
|
menuType?: MenuTypeEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DictState {
|
||||||
|
dictMap: Map<string, any>
|
||||||
|
isSetDict: boolean
|
||||||
|
}
|
||||||
|
|
|
@ -5,43 +5,47 @@ export {}
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta extends Record<string | number | symbol, unknown> {
|
interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||||
orderNo?: number
|
orderNo?: number
|
||||||
// title
|
// 路由title 一般必填
|
||||||
title: string
|
title: string
|
||||||
// dynamic router level.
|
// 动态路由可打开Tab页数
|
||||||
dynamicLevel?: number
|
dynamicLevel?: number
|
||||||
// dynamic router real route path (For performance).
|
// 动态路由的实际Path, 即去除路由的动态部分;
|
||||||
realPath?: string
|
realPath?: string
|
||||||
// Whether to ignore permissions
|
// 是否忽略权限,只在权限模式为Role的时候有效
|
||||||
ignoreAuth?: boolean
|
ignoreAuth?: boolean
|
||||||
// role info
|
// 可以访问的角色,只在权限模式为Role的时候有效
|
||||||
roles?: RoleEnum[]
|
roles?: RoleEnum[]
|
||||||
// Whether not to cache
|
// 是否忽略KeepAlive缓存
|
||||||
ignoreKeepAlive?: boolean
|
ignoreKeepAlive?: boolean
|
||||||
// Is it fixed on tab
|
// 是否固定标签
|
||||||
affix?: boolean
|
affix?: boolean
|
||||||
// icon on tab
|
// 图标,也是菜单图标
|
||||||
icon?: string
|
icon?: string
|
||||||
|
// 内嵌iframe的地址
|
||||||
frameSrc?: string
|
frameSrc?: string
|
||||||
// current page transition
|
// 指定该路由切换的动画名
|
||||||
transitionName?: string
|
transitionName?: string
|
||||||
// Whether the route has been dynamically added
|
// 隐藏该路由在面包屑上面的显示
|
||||||
hideBreadcrumb?: boolean
|
hideBreadcrumb?: boolean
|
||||||
// Hide submenu
|
// 如果该路由会携带参数,且需要在tab页上面显示。则需要设置为true
|
||||||
|
carryParam?: boolean
|
||||||
|
// 隐藏所有子菜单
|
||||||
hideChildrenInMenu?: boolean
|
hideChildrenInMenu?: boolean
|
||||||
// Carrying parameters
|
// Carrying parameters
|
||||||
carryParam?: boolean
|
carryParam?: boolean
|
||||||
// Used internally to mark single-level menus
|
// Used internally to mark single-level menus
|
||||||
single?: boolean
|
single?: boolean
|
||||||
// Currently active menu
|
// 当前激活的菜单。用于配置详情页时左侧激活的菜单路径
|
||||||
currentActiveMenu?: string
|
currentActiveMenu?: string
|
||||||
// Never show in tab
|
// 当前路由不再标签页显示
|
||||||
hideTab?: boolean
|
hideTab?: boolean
|
||||||
// Never show in menu
|
// 当前路由不再菜单显示
|
||||||
hideMenu?: boolean
|
hideMenu?: boolean
|
||||||
isLink?: boolean
|
// 菜单排序,只对第一级有效
|
||||||
// only build for Menu
|
orderNo?: number
|
||||||
|
// 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下,生成对应的菜单而忽略路由。2.5.3以上版本有效
|
||||||
ignoreRoute?: boolean
|
ignoreRoute?: boolean
|
||||||
// Hide path for children
|
// 是否在子级菜单的完整path中忽略本级path。2.5.3以上版本有效
|
||||||
hidePathForChildren?: boolean
|
hidePathForChildren?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,19 @@ import { isFunction } from '@/utils/is'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { ContentTypeEnum } from '@/enums/httpEnum'
|
import { ContentTypeEnum } from '@/enums/httpEnum'
|
||||||
import { RequestEnum } 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'
|
export * from './axiosTransform'
|
||||||
|
const globSetting = useGlobSetting()
|
||||||
|
// 请求队列
|
||||||
|
let requestList: any[] = []
|
||||||
|
// 是否正在刷新中
|
||||||
|
let isRefreshToken = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: axios module
|
* @description: axios 模块
|
||||||
*/
|
*/
|
||||||
export class VAxios {
|
export class VAxios {
|
||||||
private axiosInstance: AxiosInstance
|
private axiosInstance: AxiosInstance
|
||||||
|
@ -25,7 +33,7 @@ export class VAxios {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Create axios instance
|
* @description: 创建 axios 实例
|
||||||
*/
|
*/
|
||||||
private createAxios(config: CreateAxiosOptions): void {
|
private createAxios(config: CreateAxiosOptions): void {
|
||||||
this.axiosInstance = axios.create(config)
|
this.axiosInstance = axios.create(config)
|
||||||
|
@ -39,9 +47,13 @@ export class VAxios {
|
||||||
getAxios(): AxiosInstance {
|
getAxios(): AxiosInstance {
|
||||||
return this.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) {
|
configAxios(config: CreateAxiosOptions) {
|
||||||
if (!this.axiosInstance) {
|
if (!this.axiosInstance) {
|
||||||
|
@ -51,7 +63,7 @@ export class VAxios {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Set general header
|
* @description: 设置通用标题
|
||||||
*/
|
*/
|
||||||
setHeader(headers: any): void {
|
setHeader(headers: any): void {
|
||||||
if (!this.axiosInstance) {
|
if (!this.axiosInstance) {
|
||||||
|
@ -86,13 +98,54 @@ export class VAxios {
|
||||||
return config
|
return config
|
||||||
}, undefined)
|
}, undefined)
|
||||||
|
|
||||||
// Request interceptor error capture
|
// 请求拦截器错误捕获
|
||||||
requestInterceptorsCatch &&
|
requestInterceptorsCatch &&
|
||||||
isFunction(requestInterceptorsCatch) &&
|
isFunction(requestInterceptorsCatch) &&
|
||||||
this.axiosInstance.interceptors.request.use(undefined, 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)
|
res && axiosCanceler.removePending(res.config)
|
||||||
if (responseInterceptors && isFunction(responseInterceptors)) {
|
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||||
res = responseInterceptors(res)
|
res = responseInterceptors(res)
|
||||||
|
@ -100,7 +153,7 @@ export class VAxios {
|
||||||
return res
|
return res
|
||||||
}, undefined)
|
}, undefined)
|
||||||
|
|
||||||
// Response result interceptor error capture
|
// 响应结果拦截器错误捕获
|
||||||
responseInterceptorsCatch &&
|
responseInterceptorsCatch &&
|
||||||
isFunction(responseInterceptorsCatch) &&
|
isFunction(responseInterceptorsCatch) &&
|
||||||
this.axiosInstance.interceptors.response.use(undefined, (error) => {
|
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) {
|
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
|
||||||
const formData = new window.FormData()
|
const formData = new window.FormData()
|
||||||
|
@ -142,12 +195,13 @@ export class VAxios {
|
||||||
data: formData,
|
data: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-type': ContentTypeEnum.FORM_DATA,
|
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||||
|
// @ts-ignore
|
||||||
ignoreCancelToken: true
|
ignoreCancelToken: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// support form-data
|
// 支持表单数据
|
||||||
supportFormData(config: AxiosRequestConfig) {
|
supportFormData(config: AxiosRequestConfig) {
|
||||||
const headers = config.headers || this.options.headers
|
const headers = config.headers || this.options.headers
|
||||||
const contentType = headers?.['Content-Type'] || headers?.['content-type']
|
const contentType = headers?.['Content-Type'] || headers?.['content-type']
|
||||||
|
@ -182,6 +236,94 @@ export class VAxios {
|
||||||
return this.request({ ...config, method: 'DELETE' }, options)
|
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> {
|
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
let conf: CreateAxiosOptions = cloneDeep(config)
|
let conf: CreateAxiosOptions = cloneDeep(config)
|
||||||
// cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
|
// cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
|
||||||
|
@ -223,7 +365,7 @@ export class VAxios {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
// rewrite error message from axios in here
|
// 在此处重写来自 axios 的错误消息
|
||||||
}
|
}
|
||||||
reject(e)
|
reject(e)
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const getPendingUrl = (config: InternalAxiosRequestConfig) => [config.met
|
||||||
|
|
||||||
export class AxiosCanceler {
|
export class AxiosCanceler {
|
||||||
/**
|
/**
|
||||||
* Add request
|
* 添加请求
|
||||||
* @param {Object} config
|
* @param {Object} config
|
||||||
*/
|
*/
|
||||||
addPending(config: InternalAxiosRequestConfig) {
|
addPending(config: InternalAxiosRequestConfig) {
|
||||||
|
@ -19,14 +19,14 @@ export class AxiosCanceler {
|
||||||
config.cancelToken ||
|
config.cancelToken ||
|
||||||
new axios.CancelToken((cancel) => {
|
new axios.CancelToken((cancel) => {
|
||||||
if (!pendingMap.has(url)) {
|
if (!pendingMap.has(url)) {
|
||||||
// If there is no current request in pending, add it
|
// 如果当前没有待处理的请求,添加它
|
||||||
pendingMap.set(url, cancel)
|
pendingMap.set(url, cancel)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Clear all pending
|
* @description: 清除所有待处理
|
||||||
*/
|
*/
|
||||||
removeAllPending() {
|
removeAllPending() {
|
||||||
pendingMap.forEach((cancel) => {
|
pendingMap.forEach((cancel) => {
|
||||||
|
@ -36,15 +36,14 @@ export class AxiosCanceler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removal request
|
* 删除请求
|
||||||
* @param {Object} config
|
* @param {Object} config
|
||||||
*/
|
*/
|
||||||
removePending(config: InternalAxiosRequestConfig) {
|
removePending(config: InternalAxiosRequestConfig) {
|
||||||
const url = getPendingUrl(config)
|
const url = getPendingUrl(config)
|
||||||
|
|
||||||
if (pendingMap.has(url)) {
|
if (pendingMap.has(url)) {
|
||||||
// If there is a current request identifier in pending,
|
// 如果有当前请求标识符处于pending状态,则需要取消当前请求并移除
|
||||||
// the current request needs to be cancelled and removed
|
|
||||||
const cancel = pendingMap.get(url)
|
const cancel = pendingMap.get(url)
|
||||||
cancel && cancel(url)
|
cancel && cancel(url)
|
||||||
pendingMap.delete(url)
|
pendingMap.delete(url)
|
||||||
|
|
|
@ -12,8 +12,7 @@ export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||||
|
|
||||||
export abstract class AxiosTransform {
|
export abstract class AxiosTransform {
|
||||||
/**
|
/**
|
||||||
* @description: Process configuration before request
|
* @description: 请求前的流程配置
|
||||||
* @description: Process configuration before request
|
|
||||||
*/
|
*/
|
||||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig
|
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig
|
||||||
|
|
||||||
|
|
|
@ -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
|
// 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.
|
// Return to the current page after successful login. This step needs to be operated on the login page.
|
||||||
case 401:
|
case 401:
|
||||||
userStore.setToken(undefined)
|
userStore.setAccessToken(undefined)
|
||||||
errMessage = msg || t('sys.api.errMsg401')
|
errMessage = msg || t('sys.api.errMsg401')
|
||||||
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
|
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
|
||||||
userStore.setSessionTimeout(true)
|
userStore.setSessionTimeout(true)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Format request parameter time
|
* @description: 请求参数时间格式
|
||||||
*/
|
*/
|
||||||
export function formatRequestDate(params: Recordable) {
|
export function formatRequestDate(params: Recordable) {
|
||||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { checkStatus } from './checkStatus'
|
||||||
import { useGlobSetting } from '@/hooks/setting'
|
import { useGlobSetting } from '@/hooks/setting'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum'
|
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum'
|
||||||
import { isString, isUnDef, isNull, isEmpty } from '@/utils/is'
|
import { isEmpty, isNull, isString, isUnDef } from '@/utils/is'
|
||||||
import { getToken } from '@/utils/auth'
|
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||||
import { setObjToUrlParams, deepMerge } from '@/utils'
|
import { setObjToUrlParams, deepMerge } from '@/utils'
|
||||||
import { useErrorLogStoreWithOut } from '@/store/modules/errorLog'
|
import { useErrorLogStoreWithOut } from '@/store/modules/errorLog'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
@ -22,8 +22,12 @@ import axios from 'axios'
|
||||||
|
|
||||||
const globSetting = useGlobSetting()
|
const globSetting = useGlobSetting()
|
||||||
const urlPrefix = globSetting.urlPrefix
|
const urlPrefix = globSetting.urlPrefix
|
||||||
|
const tenantEnable = globSetting.tenantEnable
|
||||||
const { createMessage, createErrorModal, createSuccessModal } = useMessage()
|
const { createMessage, createErrorModal, createSuccessModal } = useMessage()
|
||||||
|
|
||||||
|
// 请求白名单,无须token的接口
|
||||||
|
const whiteList: string[] = ['/login', '/refresh-token']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 数据处理,方便区分多种处理方式
|
* @description: 数据处理,方便区分多种处理方式
|
||||||
*/
|
*/
|
||||||
|
@ -34,6 +38,10 @@ const transform: AxiosTransform = {
|
||||||
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { isTransformResponse, isReturnNativeResponse } = options
|
const { isTransformResponse, isReturnNativeResponse } = options
|
||||||
|
// 二进制数据则直接返回
|
||||||
|
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||||
if (isReturnNativeResponse) {
|
if (isReturnNativeResponse) {
|
||||||
return res
|
return res
|
||||||
|
@ -50,18 +58,20 @@ const transform: AxiosTransform = {
|
||||||
// return '[HTTP] Request has no return value';
|
// return '[HTTP] Request has no return value';
|
||||||
throw new Error(t('sys.api.apiRequestFailed'))
|
throw new Error(t('sys.api.apiRequestFailed'))
|
||||||
}
|
}
|
||||||
|
console.info(data)
|
||||||
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
|
// 这里 code,result,message为 后台统一的字段,需要在 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
|
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
|
||||||
if (hasSuccess) {
|
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)) {
|
if (isNull(successMsg) || isUnDef(successMsg) || isEmpty(successMsg)) {
|
||||||
successMsg = t(`sys.api.operationSuccess`)
|
successMsg = t(`sys.api.operationSuccess`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.successMessageMode === 'modal') {
|
if (options.successMessageMode === 'modal') {
|
||||||
createSuccessModal({ title: t('sys.api.successTip'), content: successMsg })
|
createSuccessModal({ title: t('sys.api.successTip'), content: successMsg })
|
||||||
} else if (options.successMessageMode === 'message') {
|
} else if (options.successMessageMode === 'message') {
|
||||||
|
@ -74,19 +84,19 @@ const transform: AxiosTransform = {
|
||||||
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
||||||
let timeoutMsg = ''
|
let timeoutMsg = ''
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case ResultEnum.TIMEOUT:
|
case ResultEnum.UNAUTHORIZED:
|
||||||
timeoutMsg = t('sys.api.timeoutMessage')
|
timeoutMsg = t('sys.api.timeoutMessage')
|
||||||
const userStore = useUserStoreWithOut()
|
const userStore = useUserStoreWithOut()
|
||||||
userStore.setToken(undefined)
|
userStore.setAccessToken(undefined)
|
||||||
userStore.logout(true)
|
userStore.logout(true)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (message) {
|
if (msg) {
|
||||||
timeoutMsg = message
|
timeoutMsg = msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorMessageMode='modal'的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
// errorMessageMode='modal' 的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||||
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
||||||
if (options.errorMessageMode === 'modal') {
|
if (options.errorMessageMode === 'modal') {
|
||||||
createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg })
|
createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg })
|
||||||
|
@ -147,12 +157,25 @@ const transform: AxiosTransform = {
|
||||||
* @description: 请求拦截器处理
|
* @description: 请求拦截器处理
|
||||||
*/
|
*/
|
||||||
requestInterceptors: (config, options) => {
|
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
|
// 请求之前处理config
|
||||||
const token = getToken()
|
const token = getAccessToken()
|
||||||
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
|
if (token && !isToken) {
|
||||||
// jwt token
|
// jwt token
|
||||||
;(config as Recordable).headers.Authorization = options.authenticationScheme ? `${options.authenticationScheme} ${token}` : 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
|
return config
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -221,7 +244,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||||
// authentication schemes,e.g: Bearer
|
// authentication schemes,e.g: Bearer
|
||||||
// authenticationScheme: 'Bearer',
|
// authenticationScheme: 'Bearer',
|
||||||
authenticationScheme: '',
|
authenticationScheme: 'Bearer',
|
||||||
timeout: 10 * 1000,
|
timeout: 10 * 1000,
|
||||||
// 基础接口地址
|
// 基础接口地址
|
||||||
// baseURL: globSetting.apiUrl,
|
// baseURL: globSetting.apiUrl,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
|
<div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
|
||||||
<AppLogo class="-enter-x" />
|
<AppLogo class="-enter-x" />
|
||||||
<div class="my-auto">
|
<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">
|
<div class="mt-10 font-medium text-white -enter-x">
|
||||||
<span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
|
<span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<LoginFormTitle v-show="getShow" class="enter-x" />
|
<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">
|
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef" v-show="getShow" @keypress.enter="handleLogin">
|
||||||
<FormItem name="account" class="enter-x">
|
<FormItem name="tenantName" class="enter-x">
|
||||||
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" class="fix-auto-fill" />
|
<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>
|
||||||
<FormItem name="password" class="enter-x">
|
<FormItem name="password" class="enter-x">
|
||||||
<InputPassword size="large" visibilityToggle v-model:value="formData.password" :placeholder="t('sys.login.password')" />
|
<InputPassword size="large" visibilityToggle v-model:value="formData.password" :placeholder="t('sys.login.password')" />
|
||||||
|
@ -28,7 +37,7 @@
|
||||||
</ARow>
|
</ARow>
|
||||||
|
|
||||||
<FormItem class="enter-x">
|
<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') }}
|
{{ t('sys.login.loginButton') }}
|
||||||
</Button>
|
</Button>
|
||||||
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
|
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
|
||||||
|
@ -47,9 +56,9 @@
|
||||||
</Button>
|
</Button>
|
||||||
</ACol>
|
</ACol>
|
||||||
<ACol :md="6" :xs="24">
|
<ACol :md="6" :xs="24">
|
||||||
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
|
<a-button block @click="setLoginState(LoginStateEnum.REGISTER)">
|
||||||
{{ t('sys.login.registerButton') }}
|
{{ t('sys.login.registerButton') }}
|
||||||
</Button>
|
</a-button>
|
||||||
</ACol>
|
</ACol>
|
||||||
</ARow>
|
</ARow>
|
||||||
|
|
||||||
|
@ -63,6 +72,7 @@
|
||||||
<TwitterCircleFilled />
|
<TwitterCircleFilled />
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Verify ref="verify" mode="pop" :captchaType="captchaType" :imgSize="{ width: '400px', height: '200px' }" @success="handleLogin" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, unref, computed } from 'vue'
|
import { reactive, ref, unref, computed } from 'vue'
|
||||||
|
@ -75,18 +85,29 @@ import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
|
|
||||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'
|
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'
|
||||||
|
import { useGlobSetting } from '@/hooks/setting'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
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 ACol = Col
|
||||||
const ARow = Row
|
const ARow = Row
|
||||||
const FormItem = Form.Item
|
const FormItem = Form.Item
|
||||||
const InputPassword = Input.Password
|
const InputPassword = Input.Password
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { notification, createErrorModal } = useMessage()
|
const { notification, createErrorModal } = useMessage()
|
||||||
const { prefixCls } = useDesign('login')
|
const { prefixCls } = useDesign('login')
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
|
||||||
|
const { tenantEnable, captchaEnable } = useGlobSetting()
|
||||||
|
|
||||||
const { setLoginState, getLoginState } = useLoginState()
|
const { setLoginState, getLoginState } = useLoginState()
|
||||||
const { getFormRules } = useFormRules()
|
const { getFormRules } = useFormRules()
|
||||||
|
@ -95,9 +116,14 @@ const formRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rememberMe = ref(false)
|
const rememberMe = ref(false)
|
||||||
|
|
||||||
|
const verify = ref()
|
||||||
|
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
account: 'vben',
|
tenantName: '芋道源码',
|
||||||
password: '123456'
|
username: 'admin',
|
||||||
|
password: 'admin123',
|
||||||
|
captchaVerification: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const { validForm } = useFormValid(formRef)
|
const { validForm } = useFormValid(formRef)
|
||||||
|
@ -106,20 +132,43 @@ const { validForm } = useFormValid(formRef)
|
||||||
|
|
||||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
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()
|
const data = await validForm()
|
||||||
if (!data) return
|
if (!data) return
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const userInfo = await userStore.login({
|
const userInfo = await userStore.login({
|
||||||
password: data.password,
|
password: data.password,
|
||||||
username: data.account,
|
username: data.username,
|
||||||
|
captchaVerification: params.captchaVerification,
|
||||||
mode: 'none' //不要默认的错误提示
|
mode: 'none' //不要默认的错误提示
|
||||||
})
|
})
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
|
await permissionStore.changePermissionCode(userInfo.permissions)
|
||||||
notification.success({
|
notification.success({
|
||||||
message: t('sys.login.loginSuccessTitle'),
|
message: t('sys.login.loginSuccessTitle'),
|
||||||
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
|
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.nickname}`,
|
||||||
duration: 3
|
duration: 3
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,5 @@ const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
const data = await validForm()
|
const data = await validForm()
|
||||||
if (!data) return
|
if (!data) return
|
||||||
console.log(data)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { QrCode } from '@/components/Qrcode'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { useLoginState, LoginStateEnum } from './useLogin'
|
import { useLoginState, LoginStateEnum } from './useLogin'
|
||||||
|
|
||||||
const qrCodeUrl = 'https://vben.vvbin.cn/login'
|
const qrCodeUrl = 'https://vben.xingyuv.com/login'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { handleBackLogin, getLoginState } = useLoginState()
|
const { handleBackLogin, getLoginState } = useLoginState()
|
||||||
|
|
|
@ -73,6 +73,5 @@ const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||||
async function handleRegister() {
|
async function handleRegister() {
|
||||||
const data = await validForm()
|
const data = await validForm()
|
||||||
if (!data) return
|
if (!data) return
|
||||||
console.log(data)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,12 +26,11 @@ const isBackMode = () => {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 记录当前的UserId
|
// 记录当前的UserId
|
||||||
userId.value = userStore.getUserInfo?.userId
|
userId.value = userStore.getUserInfo?.user.id
|
||||||
console.log('Mounted', userStore.getUserInfo)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (userId.value && userId.value !== userStore.getUserInfo.userId) {
|
if (userId.value && userId.value !== userStore.getUserInfo.user.id) {
|
||||||
// 登录的不是同一个用户,刷新整个页面以便丢弃之前用户的页面状态
|
// 登录的不是同一个用户,刷新整个页面以便丢弃之前用户的页面状态
|
||||||
document.location.reload()
|
document.location.reload()
|
||||||
} else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {
|
} else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {
|
||||||
|
|
Loading…
Reference in New Issue