fix: 自定义标签
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
[*.{js,ts,vue}]
|
||||
charset = utf-8 # 设置文件字符集为 utf-8
|
||||
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||
indent_style = space # 缩进风格(tab | space)
|
||||
indent_size = 2 # 缩进大小
|
||||
max_line_length = 100 # 最大行长度
|
||||
|
||||
[*.md] # 仅 md 文件适用以下规则
|
||||
max_line_length = off # 关闭最大行长度限制
|
||||
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||
37
.env
Normal file
37
.env
Normal file
@@ -0,0 +1,37 @@
|
||||
# 标题
|
||||
VITE_APP_TITLE=衣朵云管理系统
|
||||
|
||||
# 项目本地运行端口号
|
||||
VITE_PORT=80
|
||||
|
||||
# open 运行 npm run dev 时自动打开浏览器
|
||||
VITE_OPEN=true
|
||||
|
||||
# 租户开关
|
||||
VITE_APP_TENANT_ENABLE=true
|
||||
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=true
|
||||
|
||||
# 文档地址的开关
|
||||
VITE_APP_DOCALERT_ENABLE=false
|
||||
|
||||
# 百度统计
|
||||
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
|
||||
|
||||
# 默认账户密码
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT = 衣朵云源码
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME =
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD =
|
||||
|
||||
# API 加解密
|
||||
VITE_APP_API_ENCRYPT_ENABLE = true
|
||||
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
|
||||
VITE_APP_API_ENCRYPT_ALGORITHM = AES
|
||||
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
|
||||
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
|
||||
# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
|
||||
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
|
||||
|
||||
# 百度地图
|
||||
VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD'
|
||||
38
.env.dev
Normal file
38
.env.dev
Normal file
@@ -0,0 +1,38 @@
|
||||
# 开发环境:本地只启动前端项目,依赖开发环境(后端、APP)
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_DEV=true
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://localhost:48080'
|
||||
#VITE_BASE_URL='http://118.253.178.8:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=false
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=false
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=true
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist
|
||||
|
||||
# 商城H5会员端域名
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=true
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://118.253.178.8:3030'
|
||||
35
.env.local
Normal file
35
.env.local
Normal file
@@ -0,0 +1,35 @@
|
||||
# 本地开发环境:本地启动所有项目(前端、后端、APP)时使用,不依赖外部环境
|
||||
NODE_ENV=development
|
||||
|
||||
VITE_DEV=true
|
||||
|
||||
# 请求路径
|
||||
#VITE_BASE_URL='http://118.253.178.8:48080'
|
||||
VITE_BASE_URL='http://localhost:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=false
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=false
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/
|
||||
|
||||
# 商城H5会员端域名
|
||||
VITE_MALL_H5_DOMAIN='http://localhost:3000'
|
||||
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=false
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||
34
.env.prod
Normal file
34
.env.prod
Normal file
@@ -0,0 +1,34 @@
|
||||
# 生产环境:只在打包时使用
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_DEV=true
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://118.253.178.8:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-prod
|
||||
|
||||
# 商城H5会员端域名
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://118.253.178.8:3030'
|
||||
34
.env.stage
Normal file
34
.env.stage
Normal file
@@ -0,0 +1,34 @@
|
||||
# 预发布环境:只在打包时使用
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_DEV=false
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://118.253.178.8:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH='http://static-vue3.yudao.iocoder.cn/'
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-stage
|
||||
|
||||
# 商城H5会员端域名
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||
34
.env.test
Normal file
34
.env.test
Normal file
@@ -0,0 +1,34 @@
|
||||
# 测试环境:只在打包时使用
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_DEV=false
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://localhost:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/admin-ui-vue3/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-test
|
||||
|
||||
# 商城H5会员端域名
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
/test/unit/coverage/
|
||||
/node_modules/*
|
||||
/dist*
|
||||
/src/main.ts
|
||||
259
.eslintrc-auto-import.json
Normal file
259
.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"globals": {
|
||||
"EffectScope": true,
|
||||
"ElMessage": true,
|
||||
"ElMessageBox": true,
|
||||
"ElTag": true,
|
||||
"asyncComputed": true,
|
||||
"autoResetRef": true,
|
||||
"computed": true,
|
||||
"computedAsync": true,
|
||||
"computedEager": true,
|
||||
"computedInject": true,
|
||||
"computedWithControl": true,
|
||||
"controlledComputed": true,
|
||||
"controlledRef": true,
|
||||
"createApp": true,
|
||||
"createEventHook": true,
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createSharedComposable": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
"debouncedWatch": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"eagerComputed": true,
|
||||
"effectScope": true,
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
"isDefined": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onStartTyping": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"pausableWatch": true,
|
||||
"provide": true,
|
||||
"reactify": true,
|
||||
"reactifyObject": true,
|
||||
"reactive": true,
|
||||
"reactiveComputed": true,
|
||||
"reactiveOmit": true,
|
||||
"reactivePick": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"refAutoReset": true,
|
||||
"refDebounced": true,
|
||||
"refDefault": true,
|
||||
"refThrottled": true,
|
||||
"refWithControl": true,
|
||||
"resolveComponent": true,
|
||||
"resolveRef": true,
|
||||
"resolveUnref": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"syncRef": true,
|
||||
"syncRefs": true,
|
||||
"templateRef": true,
|
||||
"throttledRef": true,
|
||||
"throttledWatch": true,
|
||||
"toRaw": true,
|
||||
"toReactive": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
"useDebounce": true,
|
||||
"useDebounceFn": true,
|
||||
"useDebouncedRefHistory": true,
|
||||
"useDeviceMotion": true,
|
||||
"useDeviceOrientation": true,
|
||||
"useDevicePixelRatio": true,
|
||||
"useDevicesList": true,
|
||||
"useDisplayMedia": true,
|
||||
"useDocumentVisibility": true,
|
||||
"useDraggable": true,
|
||||
"useDropZone": true,
|
||||
"useElementBounding": true,
|
||||
"useElementByPoint": true,
|
||||
"useElementHover": true,
|
||||
"useElementSize": true,
|
||||
"useElementVisibility": true,
|
||||
"useEventBus": true,
|
||||
"useEventListener": true,
|
||||
"useEventSource": true,
|
||||
"useEyeDropper": true,
|
||||
"useFavicon": true,
|
||||
"useFetch": true,
|
||||
"useFileDialog": true,
|
||||
"useFileSystemAccess": true,
|
||||
"useFocus": true,
|
||||
"useFocusWithin": true,
|
||||
"useFps": true,
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
"useIntersectionObserver": true,
|
||||
"useInterval": true,
|
||||
"useIntervalFn": true,
|
||||
"useKeyModifier": true,
|
||||
"useLastChanged": true,
|
||||
"useLocalStorage": true,
|
||||
"useMagicKeys": true,
|
||||
"useManualRefHistory": true,
|
||||
"useMediaControls": true,
|
||||
"useMediaQuery": true,
|
||||
"useMemoize": true,
|
||||
"useMemory": true,
|
||||
"useMounted": true,
|
||||
"useMouse": true,
|
||||
"useMouseInElement": true,
|
||||
"useMousePressed": true,
|
||||
"useMutationObserver": true,
|
||||
"useNavigatorLanguage": true,
|
||||
"useNetwork": true,
|
||||
"useNow": true,
|
||||
"useObjectUrl": true,
|
||||
"useOffsetPagination": true,
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
"useUserMedia": true,
|
||||
"useVModel": true,
|
||||
"useVModels": true,
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
"useWebWorker": true,
|
||||
"useWebWorkerFn": true,
|
||||
"useWindowFocus": true,
|
||||
"useWindowScroll": true,
|
||||
"useWindowSize": true,
|
||||
"watch": true,
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
75
.eslintrc.js
Normal file
75
.eslintrc.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// @ts-check
|
||||
const { defineConfig } = require('eslint-define-config')
|
||||
module.exports = defineConfig({
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
jsxPragma: 'React',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
'plugin:prettier/recommended',
|
||||
'@unocss'
|
||||
],
|
||||
rules: {
|
||||
'vue/no-setup-props-destructure': 'off',
|
||||
'vue/script-setup-uses-vars': 'error',
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'space-before-function-paren': 'off',
|
||||
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
'vue/html-closing-bracket-newline': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/require-explicit-emits': 'off',
|
||||
'vue/require-toggle-inside-transition': 'off',
|
||||
'vue/html-self-closing': [
|
||||
'error',
|
||||
{
|
||||
html: {
|
||||
void: 'always',
|
||||
normal: 'never',
|
||||
component: 'always'
|
||||
},
|
||||
svg: 'always',
|
||||
math: 'always'
|
||||
}
|
||||
],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
|
||||
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
||||
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
||||
}
|
||||
})
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
/dist*
|
||||
pnpm-debug
|
||||
auto-*.d.ts
|
||||
.idea
|
||||
.history
|
||||
11
.prettierignore
Normal file
11
.prettierignore
Normal file
@@ -0,0 +1,11 @@
|
||||
/node_modules/**
|
||||
/dist/
|
||||
/dist*
|
||||
/public/*
|
||||
/docs/*
|
||||
/vite.config.ts
|
||||
/src/types/env.d.ts
|
||||
/src/types/auto-components.d.ts
|
||||
/src/types/auto-imports.d.ts
|
||||
/docs/**/*
|
||||
CHANGELOG
|
||||
6
.stylelintignore
Normal file
6
.stylelintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
/dist*
|
||||
/src/types/env.d.ts
|
||||
/docs/**/*
|
||||
18
.vscode/extensions.json
vendored
Normal file
18
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"christian-kohler.path-intellisense",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"mrmlnc.vscode-less",
|
||||
"lokalise.i18n-ally",
|
||||
"redhat.vscode-yaml",
|
||||
"csstools.postcss",
|
||||
"mikestead.dotenv",
|
||||
"eamodio.gitlens",
|
||||
"antfu.iconify",
|
||||
"antfu.unocss",
|
||||
"Vue.volar"
|
||||
]
|
||||
}
|
||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"name": "Launch Edge against localhost",
|
||||
"url": "http://localhost",
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
146
.vscode/settings.json
vendored
Normal file
146
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.tabSize": 2,
|
||||
"prettier.printWidth": 100, // 超过最大值换行
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.eol": "\n",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/*.log": true,
|
||||
"**/*.log*": true,
|
||||
"**/bower_components": true,
|
||||
"**/dist": true,
|
||||
"**/elehukouben": true,
|
||||
"**/.git": true,
|
||||
"**/.gitignore": true,
|
||||
"**/.svn": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/.idea": true,
|
||||
"**/.vscode": false,
|
||||
"**/yarn.lock": true,
|
||||
"**/tmp": true,
|
||||
"out": true,
|
||||
"dist": true,
|
||||
"node_modules": true,
|
||||
"CHANGELOG.md": true,
|
||||
"examples": true,
|
||||
"res": true,
|
||||
"screenshots": true,
|
||||
"yarn-error.log": true,
|
||||
"**/.yarn": true
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.cache": true,
|
||||
"**/.editorconfig": true,
|
||||
"**/.eslintcache": true,
|
||||
"**/bower_components": true,
|
||||
"**/.idea": true,
|
||||
"**/tmp": true,
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/.vscode/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/tmp/**": true,
|
||||
"**/bower_components/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
"stylelint.enable": true,
|
||||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[less]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.namespace": false,
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.displayLanguage": "zh-CN",
|
||||
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||
"cSpell.words": [
|
||||
"brotli",
|
||||
"browserslist",
|
||||
"codemirror",
|
||||
"commitlint",
|
||||
"cropperjs",
|
||||
"echart",
|
||||
"echarts",
|
||||
"esnext",
|
||||
"esno",
|
||||
"iconify",
|
||||
"INTLIFY",
|
||||
"lintstagedrc",
|
||||
"logicflow",
|
||||
"nprogress",
|
||||
"pinia",
|
||||
"pnpm",
|
||||
"qrcode",
|
||||
"sider",
|
||||
"sortablejs",
|
||||
"stylelint",
|
||||
"svgs",
|
||||
"unocss",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"videojs",
|
||||
"VITE",
|
||||
"vitejs",
|
||||
"vueuse",
|
||||
"wangeditor",
|
||||
"xingyu",
|
||||
"yudao",
|
||||
"zxcvbn"
|
||||
],
|
||||
// 控制相关文件嵌套展示
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"*.env": "$(capture).env.*",
|
||||
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
|
||||
},
|
||||
"terminal.integrated.scrollback": 10000,
|
||||
"nuxt.isNuxtApp": false
|
||||
}
|
||||
51
backend-idcard/README.md
Normal file
51
backend-idcard/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 身份证识别(百度 OCR + AES)
|
||||
|
||||
调用百度身份证 OCR 接口,对图片做 AES 加密上传,对返回的 `result` 密文做 Base64 解码 + AES 解密后解析出所需字段。
|
||||
|
||||
## 依赖
|
||||
|
||||
- JDK 8+
|
||||
- [fastjson](https://github.com/alibaba/fastjson)(解析返回 JSON)
|
||||
|
||||
## 配置
|
||||
|
||||
从百度控制台获取:
|
||||
|
||||
- **access_token**:OAuth2 获取(或使用 API Key + Secret Key 换 token)
|
||||
- **aesKey**:16 位 hex 字符串(控制台身份证 OCR 安全设置里)
|
||||
|
||||
## 使用示例
|
||||
|
||||
```java
|
||||
// 构造识别器(accessToken、aesKey 建议从配置文件或环境变量读取)
|
||||
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
|
||||
|
||||
// 方式一:本地文件
|
||||
IdcardRecognizer.IdcardResult result = recognizer.recognize("/path/to/idcard_front.jpg", "front");
|
||||
if (result != null) {
|
||||
System.out.println("姓名: " + result.getName());
|
||||
System.out.println("身份证号: " + result.getIdNumber());
|
||||
System.out.println("出生: " + result.getBirth());
|
||||
System.out.println("住址: " + result.getAddress());
|
||||
// ...
|
||||
}
|
||||
|
||||
// 方式二:上传的图片字节(如 MultipartFile.getBytes())
|
||||
byte[] imgBytes = ...;
|
||||
IdcardRecognizer.IdcardResult back = recognizer.recognize(imgBytes, "back");
|
||||
if (back != null) {
|
||||
System.out.println("签发机关: " + back.getIssueAuthority());
|
||||
System.out.println("有效期限: " + back.getValidDate());
|
||||
}
|
||||
```
|
||||
|
||||
## 返回字段说明
|
||||
|
||||
- **正面 (side=front)**:`name` 姓名、`gender` 性别、`nation` 民族、`birth` 出生、`address` 住址、`idNumber` 公民身份号码
|
||||
- **反面 (side=back)**:`issueAuthority` 签发机关、`validDate` 有效期限
|
||||
|
||||
按百度接口约定,请求时 `id_card_side` 传 `front` 或 `back`。
|
||||
|
||||
## 集成到现有后端
|
||||
|
||||
将 `com.ydoyun.ocr` 包拷贝到你的项目中,并加入 fastjson 依赖即可;若项目已使用 Spring,可把 `IdcardRecognizer` 做成 Bean,`accessToken`、`aesKey` 从 `@Value` 或配置类注入。
|
||||
27
backend-idcard/pom.xml
Normal file
27
backend-idcard/pom.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ydoyun</groupId>
|
||||
<artifactId>idcard-ocr</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>Idcard OCR (Baidu)</name>
|
||||
<description>百度身份证 OCR 调用(AES 加解密)</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>2.0.43</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,316 @@
|
||||
package cn.iocoder.yudao.module.car.baidu;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 获取 token + 身份证识别(百度 OCR,AES 加解密)。
|
||||
*/
|
||||
public class IdcardRecognizer {
|
||||
|
||||
private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
|
||||
|
||||
private final String accessToken;
|
||||
private byte[] originAesKey;
|
||||
|
||||
public IdcardRecognizer(String accessToken, String aesKey) {
|
||||
this.accessToken = accessToken;
|
||||
try {
|
||||
this.originAesKey = AesKeyUtil.parseAesKey(aesKey);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 获取 token(你原来的逻辑) =====================
|
||||
|
||||
/**
|
||||
* 获取权限token
|
||||
* @return 返回示例:
|
||||
* {
|
||||
* "access_token": "24.460da4889caad24cccdb1fea17221975.2592000.1491995545.282335-1234567",
|
||||
* "expires_in": 2592000
|
||||
* }
|
||||
*/
|
||||
public static String getAuth() {
|
||||
String clientId = "mYchXvYyrwXbTscZ0HWfR88s";
|
||||
String clientSecret = "AD4QSVg7OMP0GGkcqxkDSwrn7V7rkShN";
|
||||
return getAuth(clientId, clientSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API访问token
|
||||
* @param ak - 百度云官网获取的 API Key
|
||||
* @param sk - 百度云官网获取的 Securet Key
|
||||
* @return assess_token
|
||||
*/
|
||||
public static String getAuth(String ak, String sk) {
|
||||
String authHost = "https://aip.baidubce.com/oauth/2.0/token?";
|
||||
String getAccessTokenUrl = authHost
|
||||
+ "grant_type=client_credentials"
|
||||
+ "&client_id=" + ak
|
||||
+ "&client_secret=" + sk;
|
||||
try {
|
||||
URL realUrl = new URL(getAccessTokenUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.connect();
|
||||
Map<String, List<String>> map = connection.getHeaderFields();
|
||||
for (String key : map.keySet()) {
|
||||
System.err.println(key + "--->" + map.get(key));
|
||||
}
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String result = "";
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
result += line;
|
||||
}
|
||||
System.err.println("result:" + result);
|
||||
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||
String access_token = jsonObject.getString("access_token");
|
||||
return access_token;
|
||||
} catch (Exception e) {
|
||||
System.err.printf("获取token失败!");
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===================== 身份证识别(AES 加解密) =====================
|
||||
|
||||
/**
|
||||
* 识别身份证图片(本地文件路径)
|
||||
* @param filePath 图片本地路径
|
||||
* @param side 正面 "front" 或反面 "back"
|
||||
* @return 解析后的身份证字段,识别失败返回 null
|
||||
*/
|
||||
public IdcardResult recognize(String filePath, String side) throws Exception {
|
||||
byte[] imgData = Files.readAllBytes(Paths.get(filePath));
|
||||
return recognize(imgData, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别身份证图片(字节数组,适合上传场景)
|
||||
* @param imgData 图片字节
|
||||
* @param side 正面 "front" 或反面 "back"
|
||||
* @return 解析后的身份证字段,识别失败返回 null
|
||||
*/
|
||||
public IdcardResult recognize(byte[] imgData, String side) throws Exception {
|
||||
String plainJson = recognizePlainJson(imgData, side);
|
||||
return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别并返回解密后的明文 JSON(便于调试/落库)。
|
||||
*/
|
||||
public String recognizePlainJson(byte[] imgData, String side) throws Exception {
|
||||
String imgStr = encryptImg(imgData);
|
||||
String imgParam = URLEncoder.encode(imgStr, "UTF-8");
|
||||
String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true";
|
||||
|
||||
String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body);
|
||||
if (rawResponse == null || rawResponse.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return decryptResult(rawResponse);
|
||||
}
|
||||
|
||||
private String encryptImg(byte[] imgData) throws Exception {
|
||||
byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey);
|
||||
return Base64.getEncoder().encodeToString(encImgBytes);
|
||||
}
|
||||
|
||||
private String decryptResult(String encryptResponse) throws Exception {
|
||||
JSONObject obj = JSON.parseObject(encryptResponse);
|
||||
String result = obj.getString("result");
|
||||
if (result == null) return null;
|
||||
byte[] arr = Base64.getDecoder().decode(result);
|
||||
return new String(AesUtil.decrypt(arr, originAesKey), "UTF-8");
|
||||
}
|
||||
|
||||
private IdcardResult parseToResult(String plainJson, String side) {
|
||||
JSONObject root = JSON.parseObject(plainJson);
|
||||
JSONObject wordsResult = root.getJSONObject("words_result");
|
||||
if (wordsResult == null) return null;
|
||||
|
||||
IdcardResult result = new IdcardResult();
|
||||
result.setSide("front".equalsIgnoreCase(side) ? "front" : "back");
|
||||
|
||||
if ("front".equalsIgnoreCase(side)) {
|
||||
result.setName(getWords(wordsResult, "姓名"));
|
||||
result.setGender(getWords(wordsResult, "性别"));
|
||||
result.setNation(getWords(wordsResult, "民族"));
|
||||
result.setBirth(getWords(wordsResult, "出生"));
|
||||
result.setAddress(getWords(wordsResult, "住址"));
|
||||
result.setIdNumber(getWords(wordsResult, "公民身份号码"));
|
||||
} else {
|
||||
result.setIssueAuthority(getWords(wordsResult, "签发机关"));
|
||||
result.setValidDate(getWords(wordsResult, "有效期限"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getWords(JSONObject wordsResult, String key) {
|
||||
if (wordsResult == null) return null;
|
||||
JSONObject item = wordsResult.getJSONObject(key);
|
||||
return item == null ? null : item.getString("words");
|
||||
}
|
||||
|
||||
// --------------- 结果 DTO ---------------
|
||||
|
||||
public static class IdcardResult {
|
||||
private String side;
|
||||
private String name;
|
||||
private String gender;
|
||||
private String nation;
|
||||
private String birth;
|
||||
private String address;
|
||||
private String idNumber;
|
||||
private String issueAuthority;
|
||||
private String validDate;
|
||||
|
||||
public String getSide() { return side; }
|
||||
public void setSide(String side) { this.side = side; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getGender() { return gender; }
|
||||
public void setGender(String gender) { this.gender = gender; }
|
||||
public String getNation() { return nation; }
|
||||
public void setNation(String nation) { this.nation = nation; }
|
||||
public String getBirth() { return birth; }
|
||||
public void setBirth(String birth) { this.birth = birth; }
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
public String getIdNumber() { return idNumber; }
|
||||
public void setIdNumber(String idNumber) { this.idNumber = idNumber; }
|
||||
public String getIssueAuthority() { return issueAuthority; }
|
||||
public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; }
|
||||
public String getValidDate() { return validDate; }
|
||||
public void setValidDate(String validDate) { this.validDate = validDate; }
|
||||
}
|
||||
|
||||
// --------------- 工具类 ---------------
|
||||
|
||||
static class HttpUtil {
|
||||
static String post(String url, String accessToken, String formBody) throws IOException {
|
||||
String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
|
||||
byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(30000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
|
||||
OutputStream os = conn.getOutputStream();
|
||||
try {
|
||||
os.write(bodyBytes);
|
||||
} finally {
|
||||
os.close();
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is == null) return null;
|
||||
try {
|
||||
byte[] bytes = readAllBytes(is);
|
||||
return new String(bytes, "UTF-8");
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream in) throws IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
while ((n = in.read(buf)) >= 0) {
|
||||
if (n > 0) bos.write(buf, 0, n);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
static class AesKeyUtil {
|
||||
private static final String HEX = "0123456789abcdef";
|
||||
|
||||
static byte[] parseAesKey(String hex) throws Exception {
|
||||
if (hex == null || hex.length() != 16) {
|
||||
throw new Exception("aes key 需为 16 位");
|
||||
}
|
||||
char[] data = hex.toCharArray();
|
||||
byte[] out = new byte[data.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
int f = HEX.indexOf(data[i]);
|
||||
if (f < 0) throw new Exception("aes key 需为 hex 字符");
|
||||
out[i] = (byte) f;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
static class AesUtil {
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
|
||||
|
||||
static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception {
|
||||
Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(src);
|
||||
}
|
||||
|
||||
static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception {
|
||||
Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(src);
|
||||
}
|
||||
|
||||
private static Cipher getCipher(byte[] aesKey, int mode) throws Exception {
|
||||
SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
|
||||
cipher.init(mode, spec);
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- 测试 main(改下面三个变量后直接运行) ---------------
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// 1. 用你原来的 getAuth 拿 token
|
||||
String accessToken = getAuth();
|
||||
if (accessToken == null) {
|
||||
System.out.println("获取 access_token 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 从控制台拿 16 位 aesKey(hex 字符串)
|
||||
String aesKey = "填写16位aesKey";
|
||||
String filePath = "填写身份证图片路径";
|
||||
String side = "front"; // front 正面 / back 反面
|
||||
|
||||
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
|
||||
IdcardResult result = recognizer.recognize(filePath, side);
|
||||
|
||||
System.out.println("========== 识别结果 ==========");
|
||||
System.out.println(JSON.toJSONString(result, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.ydoyun.ocr;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 身份证识别器:调用百度 OCR 身份证接口(AES 加密传输),解析并返回所需字段。
|
||||
* 需配置:accessToken、aesKey(16 位 hex),可从百度控制台获取。
|
||||
*/
|
||||
public class IdcardRecognizer {
|
||||
|
||||
private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
|
||||
|
||||
private final String accessToken;
|
||||
private byte[] originAesKey;
|
||||
|
||||
public IdcardRecognizer(String accessToken, String aesKey) {
|
||||
this.accessToken = accessToken;
|
||||
try {
|
||||
this.originAesKey = AesKeyUtil.parseAesKey(aesKey);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别身份证图片(本地文件路径)
|
||||
*
|
||||
* @param filePath 图片本地路径
|
||||
* @param side 正面 "front" 或反面 "back"
|
||||
* @return 解析后的身份证字段,识别失败返回 null
|
||||
*/
|
||||
public IdcardResult recognize(String filePath, String side) throws IOException, Exception {
|
||||
byte[] imgData = Files.readAllBytes(Paths.get(filePath));
|
||||
return recognize(imgData, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别身份证图片(字节数组,适合上传场景)
|
||||
*
|
||||
* @param imgData 图片字节
|
||||
* @param side 正面 "front" 或反面 "back"
|
||||
* @return 解析后的身份证字段,识别失败返回 null
|
||||
*/
|
||||
public IdcardResult recognize(byte[] imgData, String side) throws Exception {
|
||||
String plainJson = recognizePlainJson(imgData, side);
|
||||
return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别并返回解密后的明文 JSON(便于调试/落库/自行解析)。
|
||||
*/
|
||||
public String recognizePlainJson(byte[] imgData, String side) throws Exception {
|
||||
String imgStr = encryptImg(imgData);
|
||||
String imgParam = URLEncoder.encode(imgStr, StandardCharsets.UTF_8.name());
|
||||
String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true";
|
||||
|
||||
String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body);
|
||||
if (rawResponse == null || rawResponse.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return decryptResult(rawResponse);
|
||||
}
|
||||
|
||||
private String encryptImg(byte[] imgData) throws Exception {
|
||||
byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey);
|
||||
return Base64.getEncoder().encodeToString(encImgBytes);
|
||||
}
|
||||
|
||||
private String decryptResult(String encryptResponse) throws Exception {
|
||||
JSONObject obj = JSON.parseObject(encryptResponse);
|
||||
String result = obj.getString("result");
|
||||
if (result == null) return null;
|
||||
byte[] arr = Base64.getDecoder().decode(result);
|
||||
return new String(AesUtil.decrypt(arr, originAesKey), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private IdcardResult parseToResult(String plainJson, String side) {
|
||||
JSONObject root = JSON.parseObject(plainJson);
|
||||
JSONObject wordsResult = root.getJSONObject("words_result");
|
||||
if (wordsResult == null) return null;
|
||||
|
||||
IdcardResult result = new IdcardResult();
|
||||
result.setSide("front".equalsIgnoreCase(side) ? "front" : "back");
|
||||
|
||||
if ("front".equalsIgnoreCase(side)) {
|
||||
result.setName(getWords(wordsResult, "姓名"));
|
||||
result.setGender(getWords(wordsResult, "性别"));
|
||||
result.setNation(getWords(wordsResult, "民族"));
|
||||
result.setBirth(getWords(wordsResult, "出生"));
|
||||
result.setAddress(getWords(wordsResult, "住址"));
|
||||
result.setIdNumber(getWords(wordsResult, "公民身份号码"));
|
||||
} else {
|
||||
result.setIssueAuthority(getWords(wordsResult, "签发机关"));
|
||||
result.setValidDate(getWords(wordsResult, "有效期限"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getWords(JSONObject wordsResult, String key) {
|
||||
if (wordsResult == null) return null;
|
||||
JSONObject item = wordsResult.getJSONObject(key);
|
||||
return item == null ? null : item.getString("words");
|
||||
}
|
||||
|
||||
// --------------- 结果 DTO ---------------
|
||||
|
||||
public static class IdcardResult {
|
||||
private String side;
|
||||
private String name;
|
||||
private String gender;
|
||||
private String nation;
|
||||
private String birth;
|
||||
private String address;
|
||||
private String idNumber;
|
||||
private String issueAuthority;
|
||||
private String validDate;
|
||||
|
||||
public String getSide() { return side; }
|
||||
public void setSide(String side) { this.side = side; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getGender() { return gender; }
|
||||
public void setGender(String gender) { this.gender = gender; }
|
||||
public String getNation() { return nation; }
|
||||
public void setNation(String nation) { this.nation = nation; }
|
||||
public String getBirth() { return birth; }
|
||||
public void setBirth(String birth) { this.birth = birth; }
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
public String getIdNumber() { return idNumber; }
|
||||
public void setIdNumber(String idNumber) { this.idNumber = idNumber; }
|
||||
public String getIssueAuthority() { return issueAuthority; }
|
||||
public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; }
|
||||
public String getValidDate() { return validDate; }
|
||||
public void setValidDate(String validDate) { this.validDate = validDate; }
|
||||
}
|
||||
|
||||
// --------------- 工具类 ---------------
|
||||
|
||||
static class HttpUtil {
|
||||
static String post(String url, String accessToken, String formBody) throws IOException {
|
||||
String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
|
||||
byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(15_000);
|
||||
conn.setReadTimeout(30_000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(bodyBytes);
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is == null) return null;
|
||||
try (InputStream in = is) {
|
||||
byte[] bytes = readAllBytes(in);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream in) throws IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
while ((n = in.read(buf)) >= 0) {
|
||||
if (n > 0) bos.write(buf, 0, n);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
static class AesKeyUtil {
|
||||
private static final String HEX = "0123456789abcdef";
|
||||
|
||||
static byte[] parseAesKey(String hex) throws Exception {
|
||||
if (hex == null || hex.length() != 16) {
|
||||
throw new Exception("aes key 需为 16 位");
|
||||
}
|
||||
char[] data = hex.toCharArray();
|
||||
byte[] out = new byte[data.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
int f = HEX.indexOf(data[i]);
|
||||
if (f < 0) throw new Exception("aes key 需为 hex 字符");
|
||||
out[i] = (byte) f;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
static class AesUtil {
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
|
||||
|
||||
static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception {
|
||||
Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(src);
|
||||
}
|
||||
|
||||
static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception {
|
||||
Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(src);
|
||||
}
|
||||
|
||||
private static Cipher getCipher(byte[] aesKey, int mode) throws Exception {
|
||||
SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
|
||||
cipher.init(mode, spec);
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地快速测试入口(JDK8 可运行,直接改下面四个变量再运行)。
|
||||
*/
|
||||
public static void main(String[] args) throws Exception {
|
||||
// TODO:替换成你自己的配置
|
||||
String accessToken = "填写你的 access_token";
|
||||
String aesKey = "填写 16 位 aesKey(hex 字符串)";
|
||||
String side = "front"; // front 正面 / back 反面
|
||||
String filePath = "填写身份证图片本地路径";
|
||||
|
||||
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
|
||||
IdcardResult result = recognizer.recognize(filePath, side);
|
||||
System.out.println(JSON.toJSONString(result, true));
|
||||
|
||||
// 如果你要看解密后的明文 JSON(排查字段/定位问题),放开下面三行:
|
||||
// byte[] img = Files.readAllBytes(Paths.get(filePath));
|
||||
// String plain = recognizer.recognizePlainJson(img, side);
|
||||
// System.out.println(plain);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend-idcard/target/idcard-ocr-1.0.0.jar
Normal file
BIN
backend-idcard/target/idcard-ocr-1.0.0.jar
Normal file
Binary file not shown.
3
backend-idcard/target/maven-archiver/pom.properties
Normal file
3
backend-idcard/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=idcard-ocr
|
||||
groupId=com.ydoyun
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,5 @@
|
||||
com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class
|
||||
com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class
|
||||
com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class
|
||||
com/ydoyun/ocr/IdcardRecognizer$AesUtil.class
|
||||
com/ydoyun/ocr/IdcardRecognizer.class
|
||||
@@ -0,0 +1 @@
|
||||
/Users/ouhaolan/project/百盛科技/报表/yudao-ui-admin-vue3/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java
|
||||
100
build/vite/index.ts
Normal file
100
build/vite/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { resolve } from 'path'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import progress from 'vite-plugin-progress'
|
||||
import EslintPlugin from 'vite-plugin-eslint'
|
||||
import PurgeIcons from 'vite-plugin-purge-icons'
|
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs'
|
||||
// @ts-ignore
|
||||
import ElementPlus from 'unplugin-element-plus/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
|
||||
export function createVitePlugins() {
|
||||
const root = process.cwd()
|
||||
|
||||
// 路径查找
|
||||
function pathResolve(dir: string) {
|
||||
return resolve(root, '.', dir)
|
||||
}
|
||||
|
||||
return [
|
||||
Vue(),
|
||||
VueJsx(),
|
||||
UnoCSS(),
|
||||
progress(),
|
||||
PurgeIcons(),
|
||||
ElementPlus({}),
|
||||
AutoImport({
|
||||
include: [
|
||||
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
||||
/\.vue$/,
|
||||
/\.vue\?vue/, // .vue
|
||||
/\.md$/ // .md
|
||||
],
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
// 可额外添加需要 autoImport 的组件
|
||||
{
|
||||
'@/hooks/web/useI18n': ['useI18n'],
|
||||
'@/hooks/web/useMessage': ['useMessage'],
|
||||
'@/hooks/web/useTable': ['useTable'],
|
||||
'@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
|
||||
'@/utils/formRules': ['required'],
|
||||
'@/utils/dict': ['DICT_TYPE']
|
||||
}
|
||||
],
|
||||
dts: 'src/types/auto-imports.d.ts',
|
||||
resolvers: [ElementPlusResolver()],
|
||||
eslintrc: {
|
||||
enabled: false, // Default `false`
|
||||
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
|
||||
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
||||
}
|
||||
}),
|
||||
Components({
|
||||
// 生成自定义 `auto-components.d.ts` 全局声明
|
||||
dts: 'src/types/auto-components.d.ts',
|
||||
// 自定义组件的解析器
|
||||
resolvers: [ElementPlusResolver()],
|
||||
globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
|
||||
}),
|
||||
EslintPlugin({
|
||||
cache: false,
|
||||
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
include: [resolve(__dirname, 'src/locales/**')]
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [pathResolve('src/assets/svgs')],
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
svgoOptions: true
|
||||
}),
|
||||
viteCompression({
|
||||
verbose: true, // 是否在控制台输出压缩结果
|
||||
disable: false, // 是否禁用
|
||||
threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b
|
||||
algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
|
||||
ext: '.gz', // 生成的压缩包后缀
|
||||
deleteOriginFile: false //压缩后是否删除源文件
|
||||
}),
|
||||
ViteEjsPlugin(),
|
||||
topLevelAwait({
|
||||
// https://juejin.cn/post/7152191742513512485
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla',
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}`
|
||||
})
|
||||
]
|
||||
}
|
||||
122
build/vite/optimize.ts
Normal file
122
build/vite/optimize.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
const include = [
|
||||
'qs',
|
||||
'url',
|
||||
'vue',
|
||||
'sass',
|
||||
'mitt',
|
||||
'axios',
|
||||
'pinia',
|
||||
'dayjs',
|
||||
'qrcode',
|
||||
'unocss',
|
||||
'vue-router',
|
||||
'vue-types',
|
||||
'vue-i18n',
|
||||
'crypto-js',
|
||||
'cropperjs',
|
||||
'lodash-es',
|
||||
'nprogress',
|
||||
'web-storage-cache',
|
||||
'@iconify/iconify',
|
||||
'@vueuse/core',
|
||||
'@zxcvbn-ts/core',
|
||||
'echarts/core',
|
||||
'echarts/charts',
|
||||
'echarts/components',
|
||||
'echarts/renderers',
|
||||
'echarts-wordcloud',
|
||||
'@wangeditor/editor',
|
||||
'@wangeditor/editor-for-vue',
|
||||
'@microsoft/fetch-event-source',
|
||||
'markdown-it',
|
||||
'markmap-view',
|
||||
'markmap-lib',
|
||||
'markmap-toolbar',
|
||||
'highlight.js',
|
||||
'element-plus',
|
||||
'element-plus/es',
|
||||
'element-plus/es/locale/lang/zh-cn',
|
||||
'element-plus/es/locale/lang/en',
|
||||
'element-plus/es/components/avatar/style/css',
|
||||
'element-plus/es/components/space/style/css',
|
||||
'element-plus/es/components/backtop/style/css',
|
||||
'element-plus/es/components/form/style/css',
|
||||
'element-plus/es/components/radio-group/style/css',
|
||||
'element-plus/es/components/radio/style/css',
|
||||
'element-plus/es/components/checkbox/style/css',
|
||||
'element-plus/es/components/checkbox-group/style/css',
|
||||
'element-plus/es/components/switch/style/css',
|
||||
'element-plus/es/components/time-picker/style/css',
|
||||
'element-plus/es/components/date-picker/style/css',
|
||||
'element-plus/es/components/descriptions/style/css',
|
||||
'element-plus/es/components/descriptions-item/style/css',
|
||||
'element-plus/es/components/link/style/css',
|
||||
'element-plus/es/components/tooltip/style/css',
|
||||
'element-plus/es/components/drawer/style/css',
|
||||
'element-plus/es/components/dialog/style/css',
|
||||
'element-plus/es/components/checkbox-button/style/css',
|
||||
'element-plus/es/components/option-group/style/css',
|
||||
'element-plus/es/components/radio-button/style/css',
|
||||
'element-plus/es/components/cascader/style/css',
|
||||
'element-plus/es/components/color-picker/style/css',
|
||||
'element-plus/es/components/input-number/style/css',
|
||||
'element-plus/es/components/rate/style/css',
|
||||
'element-plus/es/components/select-v2/style/css',
|
||||
'element-plus/es/components/tree-select/style/css',
|
||||
'element-plus/es/components/slider/style/css',
|
||||
'element-plus/es/components/time-select/style/css',
|
||||
'element-plus/es/components/autocomplete/style/css',
|
||||
'element-plus/es/components/image-viewer/style/css',
|
||||
'element-plus/es/components/upload/style/css',
|
||||
'element-plus/es/components/col/style/css',
|
||||
'element-plus/es/components/form-item/style/css',
|
||||
'element-plus/es/components/alert/style/css',
|
||||
'element-plus/es/components/breadcrumb/style/css',
|
||||
'element-plus/es/components/select/style/css',
|
||||
'element-plus/es/components/input/style/css',
|
||||
'element-plus/es/components/breadcrumb-item/style/css',
|
||||
'element-plus/es/components/tag/style/css',
|
||||
'element-plus/es/components/pagination/style/css',
|
||||
'element-plus/es/components/table/style/css',
|
||||
'element-plus/es/components/table-v2/style/css',
|
||||
'element-plus/es/components/table-column/style/css',
|
||||
'element-plus/es/components/card/style/css',
|
||||
'element-plus/es/components/row/style/css',
|
||||
'element-plus/es/components/button/style/css',
|
||||
'element-plus/es/components/menu/style/css',
|
||||
'element-plus/es/components/sub-menu/style/css',
|
||||
'element-plus/es/components/menu-item/style/css',
|
||||
'element-plus/es/components/option/style/css',
|
||||
'element-plus/es/components/dropdown/style/css',
|
||||
'element-plus/es/components/dropdown-menu/style/css',
|
||||
'element-plus/es/components/dropdown-item/style/css',
|
||||
'element-plus/es/components/skeleton/style/css',
|
||||
'element-plus/es/components/skeleton/style/css',
|
||||
'element-plus/es/components/backtop/style/css',
|
||||
'element-plus/es/components/menu/style/css',
|
||||
'element-plus/es/components/sub-menu/style/css',
|
||||
'element-plus/es/components/menu-item/style/css',
|
||||
'element-plus/es/components/dropdown/style/css',
|
||||
'element-plus/es/components/tree/style/css',
|
||||
'element-plus/es/components/dropdown-menu/style/css',
|
||||
'element-plus/es/components/dropdown-item/style/css',
|
||||
'element-plus/es/components/badge/style/css',
|
||||
'element-plus/es/components/breadcrumb/style/css',
|
||||
'element-plus/es/components/breadcrumb-item/style/css',
|
||||
'element-plus/es/components/image/style/css',
|
||||
'element-plus/es/components/collapse-transition/style/css',
|
||||
'element-plus/es/components/timeline/style/css',
|
||||
'element-plus/es/components/timeline-item/style/css',
|
||||
'element-plus/es/components/collapse/style/css',
|
||||
'element-plus/es/components/collapse-item/style/css',
|
||||
'element-plus/es/components/button-group/style/css',
|
||||
'element-plus/es/components/text/style/css',
|
||||
'element-plus/es/components/segmented/style/css',
|
||||
'@element-plus/icons-vue',
|
||||
'element-plus/es/components/footer/style/css',
|
||||
'element-plus/es/components/empty/style/css'
|
||||
]
|
||||
|
||||
const exclude = ['@iconify/json']
|
||||
|
||||
export { include, exclude }
|
||||
311
src/views/ydoyun/customtag/CustomTagFormWithParams.vue
Normal file
311
src/views/ydoyun/customtag/CustomTagFormWithParams.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入标签名称" maxlength="50" show-word-limit />
|
||||
<div class="form-tip">用于报表中展示的标签名称</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
v-model="formData.expression"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入公式描述,如:${region_name}"
|
||||
/>
|
||||
<div class="form-tip">报表中引用的表达式,支持 ${字段名} 格式</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签颜色" prop="color">
|
||||
<div class="color-field">
|
||||
<div class="color-presets">
|
||||
<span
|
||||
v-for="c in presetColors"
|
||||
:key="c"
|
||||
class="color-chip"
|
||||
:class="{ active: formData.color === c }"
|
||||
:style="{ backgroundColor: c }"
|
||||
@click="formData.color = c"
|
||||
></span>
|
||||
</div>
|
||||
<div class="color-picker-wrap">
|
||||
<el-color-picker v-model="formData.color" :predefine="presetColors" />
|
||||
<el-input v-model="formData.color" placeholder="或输入颜色值" class="color-input" maxlength="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip">用于报表展示时的颜色标识</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="代入参数" prop="useParams">
|
||||
<el-radio-group v-model="formData.useParams">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">是否将报表参数代入 SQL 中执行</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.useParams === 1" label="参数列表" prop="params">
|
||||
<el-input v-model="formData.params" placeholder="如:startDate,endDate" />
|
||||
<div class="form-tip">需代入的参数名,多个用逗号分隔</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||
<div class="sql-script-field">
|
||||
<div v-if="formData.useParams === 1" class="param-insert-bar">
|
||||
<span class="param-label">插入参数变量:</span>
|
||||
<el-select
|
||||
v-model="selectedParamIndex"
|
||||
:placeholder="paramList.length > 0 ? '选择参数后点击插入' : '请先填写上方参数列表'"
|
||||
clearable
|
||||
class="param-select"
|
||||
:disabled="paramList.length === 0"
|
||||
>
|
||||
<el-option
|
||||
v-for="(p, idx) in paramList"
|
||||
:key="idx"
|
||||
:label="`参数${idx + 1}: ${p}`"
|
||||
:value="idx"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="paramList.length === 0 || selectedParamIndex === null"
|
||||
@click="selectedParamIndex != null ? insertParamVar(selectedParamIndex) : null"
|
||||
>
|
||||
插入
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
ref="sqlScriptInputRef"
|
||||
v-model="formData.sqlScript"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="formData.useParams === 1 ? '请输入 SQL,参数变量请通过上方下拉选择并插入' : '请输入 SQL 脚本'"
|
||||
@blur="saveSqlCursor"
|
||||
@focus="saveSqlCursor"
|
||||
/>
|
||||
<div v-if="formData.useParams === 1 && formData.sqlScript" class="sql-preview">
|
||||
<span class="preview-label">预览(参数以块显示):</span>
|
||||
<div class="sql-preview-content">
|
||||
<template v-for="(seg, i) in sqlSegments" :key="i">
|
||||
<el-tag v-if="seg.type === 'var'" type="primary" size="small" class="sql-var-tag">
|
||||
{{ seg.text }}
|
||||
</el-tag>
|
||||
<span v-else class="sql-text">{{ seg.text }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip">
|
||||
<template v-if="formData.useParams === 1">参数变量通过下拉选择插入,执行时 ${参数1}、${参数2} 等将替换为对应参数值</template>
|
||||
<template v-else>执行 SQL 获取标签选项,第一列作为显示值</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CustomTagApi, CustomTag, CustomTagType } from '@/api/ydoyun/customtag'
|
||||
|
||||
defineOptions({ name: 'CustomTagFormWithParams' })
|
||||
|
||||
const props = defineProps<{
|
||||
tagType: CustomTagType
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
|
||||
const formData = ref<Partial<CustomTag>>({
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
color: undefined,
|
||||
sqlScript: undefined,
|
||||
useParams: 0,
|
||||
params: undefined
|
||||
})
|
||||
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const sqlScriptInputRef = ref()
|
||||
const sqlCursorPos = ref({ start: 0, end: 0 })
|
||||
const selectedParamIndex = ref<number | null>(null)
|
||||
|
||||
const paramList = computed(() => {
|
||||
const p = formData.value.params
|
||||
if (!p || typeof p !== 'string') return []
|
||||
return p.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
})
|
||||
|
||||
const saveSqlCursor = () => {
|
||||
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
sqlCursorPos.value = { start: textarea.selectionStart, end: textarea.selectionEnd }
|
||||
}
|
||||
}
|
||||
|
||||
const insertParamVar = (index: number) => {
|
||||
const variable = `\${参数${index + 1}}`
|
||||
const current = formData.value.sqlScript || ''
|
||||
const { start, end } = sqlCursorPos.value
|
||||
const before = current.slice(0, start)
|
||||
const after = current.slice(end)
|
||||
formData.value.sqlScript = before + variable + after
|
||||
selectedParamIndex.value = null
|
||||
nextTick(() => {
|
||||
sqlCursorPos.value = { start: start + variable.length, end: start + variable.length }
|
||||
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(start + variable.length, start + variable.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sqlSegments = computed(() => {
|
||||
const sql = formData.value.sqlScript || ''
|
||||
const regex = /\$\{([^}]+)\}/g
|
||||
const segments: { type: 'text' | 'var'; text: string }[] = []
|
||||
let lastIndex = 0
|
||||
let m
|
||||
while ((m = regex.exec(sql)) !== null) {
|
||||
if (m.index > lastIndex) {
|
||||
segments.push({ type: 'text', text: sql.slice(lastIndex, m.index) })
|
||||
}
|
||||
segments.push({ type: 'var', text: m[0] })
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
if (lastIndex < sql.length) {
|
||||
segments.push({ type: 'text', text: sql.slice(lastIndex) })
|
||||
}
|
||||
return segments
|
||||
})
|
||||
|
||||
const validateSqlParams = () => {
|
||||
if (formData.value.useParams !== 1) return true
|
||||
const sql = formData.value.sqlScript || ''
|
||||
const maxParam = paramList.value.length
|
||||
const regex = /\$\{参数(\d+)\}/g
|
||||
let m
|
||||
const invalid: string[] = []
|
||||
while ((m = regex.exec(sql)) !== null) {
|
||||
const n = parseInt(m[1], 10)
|
||||
if (n < 1 || n > maxParam) {
|
||||
invalid.push(m[0])
|
||||
}
|
||||
}
|
||||
if (invalid.length > 0) {
|
||||
message.error(`SQL 中的参数变量 ${invalid.join('、')} 超出参数列表范围(参数1~参数${maxParam})`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const presetColors = [
|
||||
'#1890ff', '#52c41a', '#faad14', '#f5222d',
|
||||
'#722ed1', '#eb2f96', '#13c2c2', '#fa8c16'
|
||||
]
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
if (type === 'create') {
|
||||
formData.value.type = props.tagType
|
||||
}
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = await CustomTagApi.getCustomTag(id)
|
||||
formData.value = {
|
||||
...data,
|
||||
useParams: data.useParams === true || data.useParams === 1 ? 1 : 0
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
if (!validateSqlParams()) return
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as CustomTag
|
||||
if (formType.value === 'create') {
|
||||
await CustomTagApi.createCustomTag(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await CustomTagApi.updateCustomTag(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
type: props.tagType,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
color: undefined,
|
||||
sqlScript: undefined,
|
||||
useParams: 0,
|
||||
params: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.4; margin-top: 4px; }
|
||||
.color-field { width: 100%; }
|
||||
.color-presets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
||||
.color-chip {
|
||||
width: 28px; height: 28px; border-radius: 6px; cursor: pointer; border: 2px solid transparent; transition: all 0.2s;
|
||||
}
|
||||
.color-chip:hover { transform: scale(1.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); }
|
||||
.color-chip.active { border-color: var(--el-color-primary); box-shadow: 0 0 0 2px var(--el-color-primary-light-7); }
|
||||
.color-picker-wrap { display: flex; align-items: center; gap: 12px; }
|
||||
.color-picker-wrap .color-input { flex: 1; min-width: 120px; }
|
||||
.sql-script-field { width: 100%; }
|
||||
.param-insert-bar {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
padding: 8px 12px; background: var(--el-fill-color-light); border-radius: 6px;
|
||||
}
|
||||
.param-label { font-size: 13px; color: var(--el-text-color-secondary); margin-right: 4px; }
|
||||
.param-select { width: 180px; }
|
||||
.sql-preview {
|
||||
margin-top: 10px; padding: 10px 12px; background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px; font-family: monospace; font-size: 13px;
|
||||
}
|
||||
.preview-label { display: block; font-size: 12px; color: var(--el-text-color-secondary); margin-bottom: 8px; }
|
||||
.sql-preview-content { line-height: 1.8; word-break: break-all; }
|
||||
.sql-var-tag { margin: 0 2px; font-family: monospace; }
|
||||
.sql-text { white-space: pre-wrap; }
|
||||
</style>
|
||||
190
src/views/ydoyun/productcustomtag/index.vue
Normal file
190
src/views/ydoyun/productcustomtag/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入标签名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
v-model="queryParams.expression"
|
||||
placeholder="请输入公式描述"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ydoyun:custom-tag:create']">
|
||||
<Icon icon="ep:plus" /> 新增
|
||||
</el-button>
|
||||
<el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['ydoyun:custom-tag:export']">
|
||||
<Icon icon="ep:download" /> 导出
|
||||
</el-button>
|
||||
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数列表" align="center" prop="params" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180" />
|
||||
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['ydoyun:custom-tag:update']">
|
||||
<Icon icon="ep:edit" /> 编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
|
||||
<CustomTagFormWithParams ref="formRef" tag-type="product" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||
import CustomTagFormWithParams from '@/views/ydoyun/customtag/CustomTagFormWithParams.vue'
|
||||
|
||||
defineOptions({ name: 'ProductCustomTag' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: 'product' as const,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
createTime: [] as string[]
|
||||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await CustomTagApi.getCustomTagPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.type = 'product'
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTag(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
const handleRowCheckboxChange = (records: CustomTag[]) => {
|
||||
checkedIds.value = records.map((item) => item.id!).filter(Boolean)
|
||||
}
|
||||
|
||||
const handleDeleteBatch = async () => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTagList(checkedIds.value)
|
||||
checkedIds.value = []
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await CustomTagApi.exportCustomTag(queryParams)
|
||||
download.excel(data, '产品自定义标签.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
111
src/views/ydoyun/reportdatabase/DatabaseList.vue
Normal file
111
src/views/ydoyun/reportdatabase/DatabaseList.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="head-container">
|
||||
<el-input
|
||||
v-model="dbName"
|
||||
class="mb-20px"
|
||||
clearable
|
||||
placeholder="请输入数据源名称"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:search" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch" style="width: 100%">
|
||||
<Icon icon="ep:search" />查询
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="head-container">
|
||||
<el-scrollbar height="calc(100vh - 200px)">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="databaseList"
|
||||
highlight-current-row
|
||||
@current-change="handleDatabaseChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column label="名称" prop="dbName" :show-overflow-tooltip="true" min-width="100" />
|
||||
<el-table-column label="类型" prop="dbType" width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.dbType === 'mysql'" type="success" size="small">MySQL</el-tag>
|
||||
<el-tag v-else-if="scope.row.dbType === 'pgsql'" type="primary" size="small">PG</el-tag>
|
||||
<el-tag v-else-if="scope.row.dbType === 'oracle'" type="warning" size="small">Oracle</el-tag>
|
||||
<el-tag v-else-if="scope.row.dbType === 'sqlserver'" type="danger" size="small">SQL</el-tag>
|
||||
<span v-else>{{ scope.row.dbType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
:small="true"
|
||||
layout="prev, pager, next"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ReportDatabaseApi, ReportDatabase } from '@/api/ydoyun/reportdatabase'
|
||||
|
||||
defineOptions({ name: 'DatabaseList' })
|
||||
|
||||
const dbName = ref('')
|
||||
const databaseList = ref<ReportDatabase[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
dbName: undefined,
|
||||
dbType: undefined
|
||||
})
|
||||
|
||||
const emits = defineEmits(['node-click'])
|
||||
|
||||
/** 查询数据源列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
...queryParams,
|
||||
dbName: dbName.value || undefined
|
||||
}
|
||||
const data = await ReportDatabaseApi.getReportDatabasePage(params)
|
||||
databaseList.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} catch (error) {
|
||||
console.error('查询数据源列表失败:', error)
|
||||
databaseList.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理搜索 */
|
||||
const handleSearch = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 处理数据源选择 */
|
||||
const handleDatabaseChange = (row: ReportDatabase | null) => {
|
||||
if (row) {
|
||||
emits('node-click', row)
|
||||
} else {
|
||||
emits('node-click', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
getList
|
||||
})
|
||||
</script>
|
||||
190
src/views/ydoyun/storecustomtag/index.vue
Normal file
190
src/views/ydoyun/storecustomtag/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入标签名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
v-model="queryParams.expression"
|
||||
placeholder="请输入公式描述"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ydoyun:custom-tag:create']">
|
||||
<Icon icon="ep:plus" /> 新增
|
||||
</el-button>
|
||||
<el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['ydoyun:custom-tag:export']">
|
||||
<Icon icon="ep:download" /> 导出
|
||||
</el-button>
|
||||
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数列表" align="center" prop="params" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180" />
|
||||
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['ydoyun:custom-tag:update']">
|
||||
<Icon icon="ep:edit" /> 编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
|
||||
<CustomTagFormWithParams ref="formRef" tag-type="store" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||
import CustomTagFormWithParams from '@/views/ydoyun/customtag/CustomTagFormWithParams.vue'
|
||||
|
||||
defineOptions({ name: 'StoreCustomTag' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: 'store' as const,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
createTime: [] as string[]
|
||||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await CustomTagApi.getCustomTagPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.type = 'store'
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTag(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
const handleRowCheckboxChange = (records: CustomTag[]) => {
|
||||
checkedIds.value = records.map((item) => item.id!).filter(Boolean)
|
||||
}
|
||||
|
||||
const handleDeleteBatch = async () => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTagList(checkedIds.value)
|
||||
checkedIds.value = []
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await CustomTagApi.exportCustomTag(queryParams)
|
||||
download.excel(data, '店铺自定义标签.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
190
src/views/ydoyun/suppliercustomtag/index.vue
Normal file
190
src/views/ydoyun/suppliercustomtag/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入标签名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
v-model="queryParams.expression"
|
||||
placeholder="请输入公式描述"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ydoyun:custom-tag:create']">
|
||||
<Icon icon="ep:plus" /> 新增
|
||||
</el-button>
|
||||
<el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['ydoyun:custom-tag:export']">
|
||||
<Icon icon="ep:download" /> 导出
|
||||
</el-button>
|
||||
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数列表" align="center" prop="params" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180" />
|
||||
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['ydoyun:custom-tag:update']">
|
||||
<Icon icon="ep:edit" /> 编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['ydoyun:custom-tag:delete']">
|
||||
<Icon icon="ep:delete" /> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
|
||||
<CustomTagFormWithParams ref="formRef" tag-type="supplier" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||
import CustomTagFormWithParams from '@/views/ydoyun/customtag/CustomTagFormWithParams.vue'
|
||||
|
||||
defineOptions({ name: 'SupplierCustomTag' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: 'supplier' as const,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
createTime: [] as string[]
|
||||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await CustomTagApi.getCustomTagPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.type = 'supplier'
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTag(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
const handleRowCheckboxChange = (records: CustomTag[]) => {
|
||||
checkedIds.value = records.map((item) => item.id!).filter(Boolean)
|
||||
}
|
||||
|
||||
const handleDeleteBatch = async () => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await CustomTagApi.deleteCustomTagList(checkedIds.value)
|
||||
checkedIds.value = []
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await CustomTagApi.exportCustomTag(queryParams)
|
||||
download.excel(data, '供货商自定义标签.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
Reference in New Issue
Block a user