Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm

This commit is contained in:
jason 2024-05-20 21:34:19 +08:00
commit 03433278be
214 changed files with 16344 additions and 2560 deletions

View File

@ -1,20 +1,16 @@
# 开发环境本地只启动前端项目依赖开发环境后端、APP # 开发环境本地只启动前端项目依赖开发环境后端、APP
NODE_ENV=development NODE_ENV=production
VITE_DEV=true VITE_DEV=true
# 请求路径 # 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务 # 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api
@ -37,4 +33,4 @@ VITE_OUT_DIR=dist
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# 验证码的开关 # 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false VITE_APP_CAPTCHA_ENABLE=true

View File

@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api

View File

@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api

View File

@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api

View File

@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api

View File

@ -68,6 +68,8 @@ module.exports = defineConfig({
], ],
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off', 'vue/no-v-html': 'off',
'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示因为暂时不需要这么严格警告也有点繁琐
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
} }
}) })

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ dist
dist-ssr dist-ssr
*.local *.local
/dist* /dist*
*-lock.*
pnpm-debug pnpm-debug
auto-*.d.ts auto-*.d.ts
.idea .idea

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -3,7 +3,6 @@
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mrmlnc.vscode-less", "mrmlnc.vscode-less",

View File

@ -117,6 +117,8 @@
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | | 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | | 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
![功能图](/.image/common/system-feature.png)
### 工作流程 ### 工作流程
| | 功能 | 描述 | | | 功能 | 描述 |
@ -129,6 +131,8 @@
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | | 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 |
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | | 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
![功能图](/.image/common/bpm-feature.png)
### 支付系统 ### 支付系统
| | 功能 | 描述 | | | 功能 | 描述 |
@ -143,7 +147,7 @@ ps核心功能已经实现正在对接微信小程序中...
### 基础设施 ### 基础设施
| | 功能 | 描述 | | | 功能 | 描述 |
|-----|----------|----------------------------------------------| |----|----------|----------------------------------------------|
| 🚀 | 代码生成 | 前后端代码的生成Java、Vue、SQL、单元测试支持 CRUD 下载 | | 🚀 | 代码生成 | 前后端代码的生成Java、Vue、SQL、单元测试支持 CRUD 下载 |
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 | | 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 | | 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
@ -158,12 +162,12 @@ ps核心功能已经实现正在对接微信小程序中...
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 | | 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 | | 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 | | 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 | | 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | | 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | | 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
![功能图](/.image/common/infra-feature.png)
### 数据报表 ### 数据报表
| | 功能 | 描述 | | | 功能 | 描述 |

View File

@ -1,22 +1,22 @@
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.0.0-snapshot", "version": "2.1.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,
"scripts": { "scripts": {
"i": "pnpm install", "i": "pnpm install",
"dev": "vite --mode local-dev", "dev": "vite",
"dev-server": "vite --mode dev", "dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit", "ts:check": "vue-tsc --noEmit",
"build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev", "serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod", "serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local-dev && vite preview", "preview": "pnpm build:local && vite preview",
"clean": "npx rimraf node_modules", "clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache", "clean:cache": "npx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
@ -30,12 +30,12 @@
"@form-create/element-ui": "^3.1.24", "@form-create/element-ui": "^3.1.24",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@videojs-player/vue": "^1.0.0", "@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.6.1", "@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.6.1", "axios": "^1.6.8",
"benz-amr-recorder": "^1.1.5", "benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0", "bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1", "camunda-bpmn-moddle": "^7.0.1",
@ -44,9 +44,9 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"diagram-js": "^12.8.0", "diagram-js": "^12.8.0",
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.4.3", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.4.2", "element-plus": "2.6.1",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
@ -55,76 +55,78 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.2", "qs": "^6.12.0",
"steady-xml": "^0.1.0", "steady-xml": "^0.1.0",
"url": "^0.11.3", "url": "^0.11.3",
"video.js": "^7.21.5", "video.js": "^7.21.5",
"vue": "^3.3.8", "vue": "3.4.21",
"vue-dompurify-html": "^4.1.4", "vue-dompurify-html": "^4.1.4",
"vue-i18n": "^9.6.5", "vue-i18n": "9.10.2",
"vue-router": "^4.2.5", "vue-router": "^4.3.0",
"vue-types": "^5.1.1", "vue-types": "^5.1.1",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1", "web-storage-cache": "^1.1.1",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.4.1", "@commitlint/cli": "^19.0.1",
"@commitlint/config-conventional": "^18.4.0", "@commitlint/config-conventional": "^19.0.0",
"@iconify/json": "^2.2.142", "@iconify/json": "^2.2.187",
"@intlify/unplugin-vue-i18n": "^1.5.0", "@intlify/unplugin-vue-i18n": "^2.0.0",
"@purge-icons/generated": "^0.9.0", "@purge-icons/generated": "^0.9.0",
"@types/lodash-es": "^4.17.11", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.9.0", "@types/node": "^20.11.21",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.10", "@types/qs": "^6.9.12",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/parser": "^7.1.0",
"@unocss/transformer-variant-group": "^0.57.4", "@unocss/transformer-variant-group": "^0.58.5",
"@unocss/eslint-config": "^0.57.4", "@unocss/eslint-config": "^0.57.4",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^4.4.1", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.0.2", "@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.17",
"bpmn-js": "8.9.0", "bpmn-js": "8.9.0",
"bpmn-js-properties-panel": "0.46.0", "bpmn-js-properties-panel": "0.46.0",
"consola": "^3.2.3", "consola": "^3.2.3",
"eslint": "^8.53.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^1.24.1", "eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.18.1", "eslint-plugin-vue": "^9.22.0",
"lint-staged": "^15.1.0", "lint-staged": "^15.2.2",
"postcss": "^8.4.31", "postcss": "^8.4.35",
"postcss-html": "^1.5.0", "postcss-html": "^1.6.0",
"postcss-scss": "^4.0.9", "postcss-scss": "^4.0.9",
"prettier": "^3.1.0", "prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.4.1", "rollup": "^4.12.0",
"sass": "^1.69.5", "sass": "^1.69.5",
"stylelint": "^15.11.0", "stylelint": "^16.2.1",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-recommended": "^14.0.0",
"stylelint-config-standard": "^34.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-order": "^6.0.3", "stylelint-order": "^6.0.4",
"terser": "^5.24.0", "terser": "^5.28.1",
"typescript": "5.2.2", "typescript": "5.3.3",
"unocss": "^0.57.4", "unocss": "^0.58.5",
"unplugin-auto-import": "^0.16.7", "unplugin-auto-import": "^0.16.7",
"unplugin-element-plus": "^0.8.0", "unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
"vite": "4.5.0", "vite": "5.1.4",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4", "vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-progress": "^0.0.7", "vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2", "vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.1", "vite-plugin-top-level-await": "^1.3.1",
"vue-eslint-parser": "^9.3.2", "vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.22" "vue-tsc": "^1.8.27"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@ -135,7 +137,6 @@
"url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues" "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
}, },
"homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3", "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
"packageManager": "pnpm@8.6.0",
"engines": { "engines": {
"node": ">= 16.0.0", "node": ">= 16.0.0",
"pnpm": ">=8.6.0" "pnpm": ">=8.6.0"

9614
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,9 @@ export interface PermissionVO {
} }
export interface TransferReqVO { export interface TransferReqVO {
bizId: number // 模块编号 id: number // 模块编号
newOwnerUserId: number // 新负责人的用户编号 newOwnerUserId: number // 新负责人的用户编号
oldOwnerPermissionLevel: number // 老负责人加入团队后的权限级别 oldOwnerPermissionLevel?: number // 老负责人加入团队后的权限级别
toBizTypes?: number[] // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择 toBizTypes?: number[] // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
} }

View File

@ -14,21 +14,21 @@ export interface CrmStatisticsCustomerSummaryByUserRespVO {
receivablePrice: number receivablePrice: number
} }
export interface CrmStatisticsFollowupSummaryByDateRespVO { export interface CrmStatisticsFollowUpSummaryByDateRespVO {
time: string time: string
followupRecordCount: number followUpRecordCount: number
followupCustomerCount: number followUpCustomerCount: number
} }
export interface CrmStatisticsFollowupSummaryByUserRespVO { export interface CrmStatisticsFollowUpSummaryByUserRespVO {
ownerUserName: string ownerUserName: string
followupRecordCount: number followupRecordCount: number
followupCustomerCount: number followupCustomerCount: number
} }
export interface CrmStatisticsFollowupSummaryByTypeRespVO { export interface CrmStatisticsFollowUpSummaryByTypeRespVO {
followupType: string followUpType: string
followupRecordCount: number followUpRecordCount: number
} }
export interface CrmStatisticsCustomerContractSummaryRespVO { export interface CrmStatisticsCustomerContractSummaryRespVO {
@ -44,6 +44,18 @@ export interface CrmStatisticsCustomerContractSummaryRespVO {
orderDate: Date orderDate: Date
} }
export interface CrmStatisticsPoolSummaryByDateRespVO {
time: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsPoolSummaryByUserRespVO {
ownerUserName: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsCustomerDealCycleByDateRespVO { export interface CrmStatisticsCustomerDealCycleByDateRespVO {
time: string time: string
customerDealCycle: number customerDealCycle: number
@ -55,6 +67,18 @@ export interface CrmStatisticsCustomerDealCycleByUserRespVO {
customerDealCount: number customerDealCount: number
} }
export interface CrmStatisticsCustomerDealCycleByAreaRespVO {
areaName: string
customerDealCycle: number
customerDealCount: number
}
export interface CrmStatisticsCustomerDealCycleByProductRespVO {
productName: string
customerDealCycle: number
customerDealCount: number
}
// 客户分析 API // 客户分析 API
export const StatisticsCustomerApi = { export const StatisticsCustomerApi = {
// 1.1 客户总量分析(按日期) // 1.1 客户总量分析(按日期)
@ -72,23 +96,23 @@ export const StatisticsCustomerApi = {
}) })
}, },
// 2.1 客户跟进次数分析(按日期) // 2.1 客户跟进次数分析(按日期)
getFollowupSummaryByDate: (params: any) => { getFollowUpSummaryByDate: (params: any) => {
return request.get({ return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-date', url: '/crm/statistics-customer/get-follow-up-summary-by-date',
params params
}) })
}, },
// 2.2 客户跟进次数分析(按用户) // 2.2 客户跟进次数分析(按用户)
getFollowupSummaryByUser: (params: any) => { getFollowUpSummaryByUser: (params: any) => {
return request.get({ return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-user', url: '/crm/statistics-customer/get-follow-up-summary-by-user',
params params
}) })
}, },
// 3.1 获取客户跟进方式统计数 // 3.1 获取客户跟进方式统计数
getFollowupSummaryByType: (params: any) => { getFollowUpSummaryByType: (params: any) => {
return request.get({ return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-type', url: '/crm/statistics-customer/get-follow-up-summary-by-type',
params params
}) })
}, },
@ -99,18 +123,46 @@ export const StatisticsCustomerApi = {
params params
}) })
}, },
// 5.1 获取客户成交周期(按日期) // 5.1 获取客户公海分析(按日期)
getPoolSummaryByDate: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-date',
params: param
})
},
// 5.2 获取客户公海分析(按用户)
getPoolSummaryByUser: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-user',
params: param
})
},
// 6.1 获取客户成交周期(按日期)
getCustomerDealCycleByDate: (params: any) => { getCustomerDealCycleByDate: (params: any) => {
return request.get({ return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-date', url: '/crm/statistics-customer/get-customer-deal-cycle-by-date',
params params
}) })
}, },
// 5.2 获取客户成交周期(按用户) // 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByUser: (params: any) => { getCustomerDealCycleByUser: (params: any) => {
return request.get({ return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-user', url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
params params
}) })
},
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByArea: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-area',
params
})
},
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByProduct: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-product',
params
})
} }
} }

View File

@ -0,0 +1,58 @@
import request from '@/config/axios'
export interface CrmStatisticFunnelRespVO {
customerCount: number // 客户数
businessCount: number // 商机数
businessWinCount: number // 赢单数
}
export interface CrmStatisticsBusinessSummaryByDateRespVO {
time: string // 时间
businessCreateCount: number // 商机数
totalPrice: number | string // 商机金额
}
export interface CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
time: string // 时间
businessCount: number // 商机数量
businessWinCount: number // 赢单商机数
}
// 客户分析 API
export const StatisticFunnelApi = {
// 1. 获取销售漏斗统计数据
getFunnelSummary: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-funnel-summary',
params
})
},
// 2. 获取商机结束状态统计
getBusinessSummaryByEndStatus: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-summary-by-end-status',
params
})
},
// 3. 获取新增商机分析(按日期)
getBusinessSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-summary-by-date',
params
})
},
// 4. 获取商机转化率分析(按日期)
getBusinessInversionRateSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-inversion-rate-summary-by-date',
params
})
},
// 5. 获取商机列表(按日期)
getBusinessPageByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-page-by-date',
params
})
}
}

View File

@ -0,0 +1,33 @@
import request from '@/config/axios'
export interface StatisticsPerformanceRespVO {
time: string
currentMonthCount: number
lastMonthCount: number
lastYearCount: number
}
// 排行 API
export const StatisticsPerformanceApi = {
// 员工获得合同金额统计
getContractPricePerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-contract-price-performance',
params
})
},
// 员工获得回款统计
getReceivablePricePerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-receivable-price-performance',
params
})
},
//员工获得签约合同数量统计
getContractCountPerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-contract-count-performance',
params
})
}
}

View File

@ -0,0 +1,60 @@
import request from '@/config/axios'
export interface CrmStatisticCustomerBaseRespVO {
customerCount: number
dealCount: number
dealPortion: string | number
}
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
industryId: number
industryPortion: string | number
}
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
source: number
sourcePortion: string | number
}
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
level: number
levelPortion: string | number
}
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
areaId: number
areaName: string
areaPortion: string | number
}
// 客户分析 API
export const StatisticsPortraitApi = {
// 1. 获取客户行业统计数据
getCustomerIndustry: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-industry-summary',
params
})
},
// 2. 获取客户来源统计数据
getCustomerSource: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-source-summary',
params
})
},
// 3. 获取客户级别统计数据
getCustomerLevel: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-level-summary',
params
})
},
// 4. 获取客户地区统计数据
getCustomerArea: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-area-summary',
params
})
}
}

View File

@ -13,8 +13,8 @@ export interface ProductCategoryVO {
// ERP 产品分类 API // ERP 产品分类 API
export const ProductCategoryApi = { export const ProductCategoryApi = {
// 查询产品分类列表 // 查询产品分类列表
getProductCategoryList: async (params) => { getProductCategoryList: async () => {
return await request.get({ url: `/erp/product-category/list`, params }) return await request.get({ url: `/erp/product-category/list` })
}, },
// 查询产品分类精简列表 // 查询产品分类精简列表

View File

@ -8,11 +8,15 @@ export interface ApiAccessLogVO {
applicationName: string applicationName: string
requestMethod: string requestMethod: string
requestParams: string requestParams: string
responseBody: string
requestUrl: string requestUrl: string
userIp: string userIp: string
userAgent: string userAgent: string
operateModule: string
operateName: string
operateType: number
beginTime: Date beginTime: Date
endTIme: Date endTime: Date
duration: number duration: number
resultCode: number resultCode: number
resultMsg: string resultMsg: string

View File

@ -28,7 +28,6 @@ export type CodegenColumnVO = {
columnComment: string columnComment: string
nullable: number nullable: number
primaryKey: number primaryKey: number
autoIncrement: boolean
ordinalPosition: number ordinalPosition: number
javaType: string javaType: string
javaField: string javaField: string

View File

@ -1,16 +0,0 @@
import request from '@/config/axios'
// 导出Html
export const exportHtml = () => {
return request.download({ url: '/infra/db-doc/export-html' })
}
// 导出Word
export const exportWord = () => {
return request.download({ url: '/infra/db-doc/export-word' })
}
// 导出Markdown
export const exportMarkdown = () => {
return request.download({ url: '/infra/db-doc/export-markdown' })
}

View File

@ -7,8 +7,8 @@ export interface Demo02CategoryVO {
} }
// 查询示例分类列表 // 查询示例分类列表
export const getDemo02CategoryList = async (params) => { export const getDemo02CategoryList = async () => {
return await request.get({ url: `/infra/demo02-category/list`, params }) return await request.get({ url: `/infra/demo02-category/list` })
} }
// 查询示例分类详情 // 查询示例分类详情

View File

@ -1,49 +1,53 @@
import request from '@/config/axios' import request from '@/config/axios'
// 秒杀时段 VO
export interface SeckillConfigVO { export interface SeckillConfigVO {
id: number id: number // 编号
name: string name: string // 秒杀时段名称
startTime: string startTime: string // 开始时间点
endTime: string endTime: string // 结束时间点
sliderPicUrls: string[] sliderPicUrls: string[] // 秒杀轮播图
status: number status: number // 活动状态
} }
// 查询秒杀时段配置列表 // 秒杀时段 API
export const getSeckillConfigPage = async (params) => { export const SeckillConfigApi = {
return await request.get({ url: '/promotion/seckill-config/page', params }) // 查询秒杀时段分页
} getSeckillConfigPage: async (params: any) => {
return await request.get({ url: `/promotion/seckill-config/page`, params })
},
// 查询秒杀时段配置详情 // 查询秒杀时段列表
export const getSeckillConfig = async (id: number) => { getSimpleSeckillConfigList: async () => {
return await request.get({ url: '/promotion/seckill-config/get?id=' + id }) return await request.get({ url: `/promotion/seckill-config/simple-list` })
} },
// 获得所有开启状态的秒杀时段精简列表 // 查询秒杀时段详情
export const getSimpleSeckillConfigList = async () => { getSeckillConfig: async (id: number) => {
return await request.get({ url: '/promotion/seckill-config/list-all-simple' }) return await request.get({ url: `/promotion/seckill-config/get?id=` + id })
} },
// 新增秒杀时段配置 // 新增秒杀时段
export const createSeckillConfig = async (data: SeckillConfigVO) => { createSeckillConfig: async (data: SeckillConfigVO) => {
return await request.post({ url: '/promotion/seckill-config/create', data }) return await request.post({ url: `/promotion/seckill-config/create`, data })
} },
// 修改秒杀时段配置 // 修改秒杀时段
export const updateSeckillConfig = async (data: SeckillConfigVO) => { updateSeckillConfig: async (data: SeckillConfigVO) => {
return await request.put({ url: '/promotion/seckill-config/update', data }) return await request.put({ url: `/promotion/seckill-config/update`, data })
} },
// 删除秒杀时段
deleteSeckillConfig: async (id: number) => {
return await request.delete({ url: `/promotion/seckill-config/delete?id=` + id })
},
// 修改时段配置状态 // 修改时段配置状态
export const updateSeckillConfigStatus = (id: number, status: number) => { updateSeckillConfigStatus: async (id: number, status: number) => {
const data = { const data = {
id, id,
status status
} }
return request.put({ url: '/promotion/seckill-config/update-status', data: data }) return request.put({ url: '/promotion/seckill-config/update-status', data: data })
} }
// 删除秒杀时段配置
export const deleteSeckillConfig = async (id: number) => {
return await request.delete({ url: '/promotion/seckill-config/delete?id=' + id })
} }

View File

@ -5,7 +5,7 @@ import { formatDate } from '@/utils/formatTime'
/** 会员分析 Request VO */ /** 会员分析 Request VO */
export interface MemberAnalyseReqVO { export interface MemberAnalyseReqVO {
times: [dayjs.ConfigType, dayjs.ConfigType] times: dayjs.ConfigType[]
} }
/** 会员分析 Response VO */ /** 会员分析 Response VO */

View File

@ -141,7 +141,7 @@ export const getExpressTrackList = async (id: number | null) => {
} }
export interface DeliveryVO { export interface DeliveryVO {
id: number // 订单编号 id?: number // 订单编号
logisticsId: number | null // 物流公司编号 logisticsId: number | null // 物流公司编号
logisticsNo: string // 物流编号 logisticsNo: string // 物流编号
} }

View File

@ -1,39 +0,0 @@
import request from '@/config/axios'
export interface UReportDataVO {
id: number
name: string
status: number
content: string
remark: string
}
// 查询Ureport2报表分页
export const getUReportDataPage = async (params) => {
return await request.get({ url: `/report/ureport-data/page`, params })
}
// 查询Ureport2报表详情
export const getUReportData = async (id: number) => {
return await request.get({ url: `/report/ureport-data/get?id=` + id })
}
// 新增Ureport2报表
export const createUReportData = async (data: UReportDataVO) => {
return await request.post({ url: `/report/ureport-data/create`, data })
}
// 修改Ureport2报表
export const updateUReportData = async (data: UReportDataVO) => {
return await request.put({ url: `/report/ureport-data/update`, data })
}
// 删除Ureport2报表
export const deleteUReportData = async (id: number) => {
return await request.delete({ url: `/report/ureport-data/delete?id=` + id })
}
// 导出Ureport2报表 Excel
export const exportUReportData = async (params) => {
return await request.download({ url: `/report/ureport-data/export-excel`, params })
}

View File

@ -1,40 +0,0 @@
import request from '@/config/axios'
export interface ErrorCodeVO {
id: number | undefined
type: number
applicationName: string
code: number | undefined
message: string
memo: string
createTime: Date
}
// 查询错误码列表
export const getErrorCodePage = (params: PageParam) => {
return request.get({ url: '/system/error-code/page', params })
}
// 查询错误码详情
export const getErrorCode = (id: number) => {
return request.get({ url: '/system/error-code/get?id=' + id })
}
// 新增错误码
export const createErrorCode = (data: ErrorCodeVO) => {
return request.post({ url: '/system/error-code/create', data })
}
// 修改错误码
export const updateErrorCode = (data: ErrorCodeVO) => {
return request.put({ url: '/system/error-code/update', data })
}
// 删除错误码
export const deleteErrorCode = (id: number) => {
return request.delete({ url: '/system/error-code/delete?id=' + id })
}
// 导出错误码
export const excelErrorCode = (params) => {
return request.download({ url: '/system/error-code/export-excel', params })
}

View File

@ -8,6 +8,7 @@ export interface MailAccountVO {
host: string host: string
port: number port: number
sslEnable: boolean sslEnable: boolean
starttlsEnable: boolean
} }
// 查询邮箱账号列表 // 查询邮箱账号列表

View File

@ -2,30 +2,6 @@ import request from '@/config/axios'
export type OperateLogVO = { export type OperateLogVO = {
id: number id: number
userNickname: string
traceId: string
userId: number
module: string
name: string
type: number
content: string
exts: Map<String, Object>
requestMethod: string
requestUrl: string
userIp: string
userAgent: string
javaMethod: string
javaMethodArgs: string
startTime: Date
duration: number
resultCode: number
resultMsg: string
resultData: string
}
export type OperateLogV2VO = {
id: number
userNickname: string
traceId: string traceId: string
userType: number userType: number
userId: number userId: number
@ -42,11 +18,6 @@ export type OperateLogV2VO = {
creator: string creator: string
creatorName: string creatorName: string
createTime: Date createTime: Date
// 数据扩展,渲染时使用
title: string // 操作标题(如果为空则取 name 值)
colSize: number // 变更记录行数
contentStrList: string[]
tagsContentList: string[]
} }
// 查询操作日志列表 // 查询操作日志列表
@ -54,6 +25,6 @@ export const getOperateLogPage = (params: PageParam) => {
return request.get({ url: '/system/operate-log/page', params }) return request.get({ url: '/system/operate-log/page', params })
} }
// 导出操作日志 // 导出操作日志
export const exportOperateLog = (params) => { export const exportOperateLog = (params: any) => {
return request.download({ url: '/system/operate-log/export', params }) return request.download({ url: '/system/operate-log/export', params })
} }

View File

@ -1,58 +0,0 @@
import request from '@/config/axios'
import qs from 'qs'
export interface SensitiveWordVO {
id: number
name: string
status: number
description: string
tags: string[]
createTime: Date
}
export interface SensitiveWordTestReqVO {
text: string
tag: string[]
}
// 查询敏感词列表
export const getSensitiveWordPage = (params: PageParam) => {
return request.get({ url: '/system/sensitive-word/page', params })
}
// 查询敏感词详情
export const getSensitiveWord = (id: number) => {
return request.get({ url: '/system/sensitive-word/get?id=' + id })
}
// 新增敏感词
export const createSensitiveWord = (data: SensitiveWordVO) => {
return request.post({ url: '/system/sensitive-word/create', data })
}
// 修改敏感词
export const updateSensitiveWord = (data: SensitiveWordVO) => {
return request.put({ url: '/system/sensitive-word/update', data })
}
// 删除敏感词
export const deleteSensitiveWord = (id: number) => {
return request.delete({ url: '/system/sensitive-word/delete?id=' + id })
}
// 导出敏感词
export const exportSensitiveWord = (params) => {
return request.download({ url: '/system/sensitive-word/export-excel', params })
}
// 获取所有敏感词的标签数组
export const getSensitiveWordTagList = () => {
return request.get({ url: '/system/sensitive-word/get-tags' })
}
// 获得文本所包含的不合法的敏感词数组
export const validateText = (query: SensitiveWordTestReqVO) => {
return request.get({
url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' })
})
}

BIN
src/assets/imgs/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -25,6 +25,9 @@ defineProps({
</template> </template>
<Icon :size="14" class="ml-5px" icon="ep:question-filled" /> <Icon :size="14" class="ml-5px" icon="ep:question-filled" />
</ElTooltip> </ElTooltip>
<div class="flex flex-grow pl-20px">
<slot name="header"></slot>
</div>
</div> </div>
</template> </template>
<div> <div>

View File

@ -503,9 +503,13 @@ const submit = () => {
emit('update:modelValue', defaultValue.value) emit('update:modelValue', defaultValue.value)
dialogVisible.value = false dialogVisible.value = false
} }
const inputChange = () => {
emit('update:modelValue', defaultValue.value)
}
</script> </script>
<template> <template>
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs"> <el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
<template #append> <template #append>
<el-select v-model="select" placeholder="生成器" style="width: 115px"> <el-select v-model="select" placeholder="生成器" style="width: 115px">
<el-option label="每分钟" value="0 * * * * ?" /> <el-option label="每分钟" value="0 * * * * ?" />

View File

@ -0,0 +1,90 @@
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="cellList"
class="m-b-16px"
:rows="1"
:cols="cellCount"
:cube-size="38"
@hot-area-selected="handleHotAreaSelected"
/>
<img src="@/assets/imgs/diy/app-nav-bar-mp.png" alt="" class="h-30px w-76px" v-if="isMp" />
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio>
<el-radio label="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<el-form-item label="内容" :prop="`cell[${cellIndex}].text`">
<el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="颜色" :prop="`cell[${cellIndex}].text`">
<ColorInput v-model="cell!.textColor" />
</el-form-item>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<el-form-item label="图片" :prop="`cell[${cellIndex}].imgUrl`">
<UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" :prop="`cell[${cellIndex}].url`">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<el-form-item label="提示文字" :prop="`cell[${cellIndex}].placeholder`">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="圆角" :prop="`cell[${cellIndex}].borderRadius`">
<el-slider
v-model="cell.borderRadius"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
</template>
</template>
</template>
<script setup lang="ts">
import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util'
//
defineOptions({ name: 'NavigationBarCellProperty' })
const props = defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const { formData: cellList } = usePropertyForm(props.modelValue, emit)
if (!cellList.value) cellList.value = []
// 628
const cellCount = computed(() => (props.isMp ? 6 : 8))
//
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
selectedHotAreaIndex.value = index
if (!cellValue.type) {
cellValue.type = 'text'
cellValue.textColor = '#111111'
}
}
</script>
<style scoped lang="scss"></style>

View File

@ -2,22 +2,53 @@ import { DiyComponent } from '@/components/DiyEditor/util'
/** 顶部导航栏属性 */ /** 顶部导航栏属性 */
export interface NavigationBarProperty { export interface NavigationBarProperty {
// 页面标题 // 背景类型
title: string bgType: 'color' | 'img'
// 页面描述 // 背景颜色
description: string bgColor: string
// 顶部导航高度 // 图片链接
navBarHeight: number bgImg: string
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
// 样式类型:默认 | 沉浸式 // 样式类型:默认 | 沉浸式
styleType: 'default' | 'immersion' styleType: 'normal' | 'inner'
// 常驻显示 // 常驻显示
alwaysShow: boolean alwaysShow: boolean
// 是否显示返回按钮 // 小程序单元格列表
showGoBack: boolean mpCells: NavigationBarCellProperty[]
// 其它平台单元格列表
otherCells: NavigationBarCellProperty[]
// 本地变量
_local: {
// 预览顶部导航(小程序)
previewMp: boolean
// 预览顶部导航(非小程序)
previewOther: boolean
}
}
/** 顶部导航栏 - 单元格 属性 */
export interface NavigationBarCellProperty {
// 类型:文字 | 图片 | 搜索框
type: 'text' | 'image' | 'search'
// 宽度
width: number
// 高度
height: number
// 顶部位置
top: number
// 左侧位置
left: number
// 文字内容
text: string
// 文字颜色
textColor: string
// 图片地址
imgUrl: string
// 图片链接
url: string
// 搜索框:提示文字
placeholder: string
// 搜索框:边框圆角半径
borderRadius: number
} }
// 定义组件 // 定义组件
@ -26,13 +57,26 @@ export const component = {
name: '顶部导航栏', name: '顶部导航栏',
icon: 'tabler:layout-navbar', icon: 'tabler:layout-navbar',
property: { property: {
title: '页面标题', bgType: 'color',
description: '', bgColor: '#fff',
navBarHeight: 35, bgImg: '',
backgroundColor: '#fff', styleType: 'normal',
backgroundImage: '',
styleType: 'default',
alwaysShow: true, alwaysShow: true,
showGoBack: true mpCells: [
{
type: 'text',
textColor: '#111111'
}
],
otherCells: [
{
type: 'text',
textColor: '#111111'
}
],
_local: {
previewMp: true,
previewOther: false
}
} }
} as DiyComponent<NavigationBarProperty> } as DiyComponent<NavigationBarProperty>

View File

@ -1,45 +1,73 @@
<template> <template>
<div <div class="navigation-bar" :style="bgStyle">
class="navigation-bar" <div class="h-full w-full flex items-center">
:style="{ <div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
height: `${property.navBarHeight}px`, <span v-if="cell.type === 'text'">{{ cell.text }}</span>
backgroundColor: property.backgroundColor, <img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
backgroundImage: `url(${property.backgroundImage})` <SearchBar v-else :property="getSearchProp" />
}"
>
<!-- 左侧 -->
<div class="left">
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
</div> </div>
<!-- 中间 -->
<div
class="center"
:style="{
height: `${property.navBarHeight}px`,
lineHeight: `${property.navBarHeight}px`
}"
>
{{ property.title }}
</div> </div>
<!-- 右侧 --> <img
<div class="right"></div> v-if="property._local?.previewMp"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
alt=""
class="h-30px w-86px"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NavigationBarProperty } from './config' import { NavigationBarCellProperty, NavigationBarProperty } from './config'
import SearchBar from '@/components/DiyEditor/components/mobile/SearchBar/index.vue'
import { StyleValue } from 'vue'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 页面顶部导航栏 */ /** 页面顶部导航栏 */
defineOptions({ name: 'NavigationBar' }) defineOptions({ name: 'NavigationBar' })
defineProps<{ property: NavigationBarProperty }>() const props = defineProps<{ property: NavigationBarProperty }>()
//
const bgStyle = computed(() => {
const background =
props.property.bgType === 'img' && props.property.bgImg
? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
: props.property.bgColor
return { background }
})
//
const cellList = computed(() =>
props.property._local?.previewMp ? props.property.mpCells : props.property.otherCells
)
//
const cellWidth = computed(() => {
return props.property._local?.previewMp ? (375 - 80 - 86) / 6 : (375 - 90) / 8
})
//
const getCellStyle = (cell: NavigationBarCellProperty) => {
return {
width: cell.width * cellWidth.value + (cell.width - 1) * 10 + 'px',
left: cell.left * cellWidth.value + (cell.left + 1) * 10 + 'px',
position: 'absolute'
} as StyleValue
}
//
const getSearchProp = (cell: NavigationBarCellProperty) => {
return {
height: 30,
showScan: false,
placeholder: cell.placeholder,
borderRadius: cell.borderRadius
} as SearchProperty
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.navigation-bar { .navigation-bar {
display: flex; display: flex;
height: 35px; height: 50px;
background: #fff; background: #fff;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 6px;
/* 左边 */ /* 左边 */
.left { .left {

View File

@ -1,53 +1,73 @@
<template> <template>
<el-form label-width="80px" :model="formData" :rules="rules"> <el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面标题" prop="title">
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
</el-form-item>
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="样式" prop="styleType"> <el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType"> <el-radio-group v-model="formData!.styleType">
<el-radio label="default">默认</el-radio> <el-radio label="normal">标准</el-radio>
<el-radio label="immersion">沉浸式</el-radio> <el-tooltip
content="沉侵式头部仅支持微信小程序、APP建议页面第一个组件为图片展示类组件"
placement="top"
>
<el-radio label="inner">沉浸式</el-radio>
</el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'"> <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow"> <el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio> <el-radio :label="false">关闭</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio> <el-radio :label="true">开启</el-radio>
</el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="高度" prop="navBarHeight"> <el-form-item label="背景类型" prop="bgType">
<el-slider <el-radio-group v-model="formData.bgType">
v-model="formData!.navBarHeight" <el-radio label="color">纯色</el-radio>
:max="100" <el-radio label="img">图片</el-radio>
:min="35" </el-radio-group>
show-input
input-size="small"
/>
</el-form-item> </el-form-item>
<el-form-item label="返回按钮" prop="showGoBack"> <el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<el-switch v-model="formData!.showGoBack" /> <ColorInput v-model="formData.bgColor" />
</el-form-item> </el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor"> <el-form-item label="背景图片" prop="bgImg" v-else>
<ColorInput v-model="formData!.backgroundColor" /> <UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
</el-form-item> </el-form-item>
<el-form-item label="背景图片" prop="backgroundImage"> <el-card class="property-group" shadow="never">
<UploadImg v-model="formData!.backgroundImage" :limit="1"> <template #header>
<template #tip>建议宽度 750px</template> <div class="flex items-center justify-between">
</UploadImg> <span>内容小程序</span>
<el-form-item prop="_local.previewMp" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
</el-form-item> </el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
</el-card>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容非小程序</span>
<el-form-item prop="_local.previewOther" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
</el-card>
</el-form> </el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NavigationBarProperty } from './config' import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util' import { usePropertyForm } from '@/components/DiyEditor/util'
import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
// //
defineOptions({ name: 'NavigationBarProperty' }) defineOptions({ name: 'NavigationBarProperty' })
// //
@ -58,6 +78,9 @@ const rules = {
const props = defineProps<{ modelValue: NavigationBarProperty }>() const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit) const { formData } = usePropertyForm(props.modelValue, emit)
if (!formData.value._local) {
formData.value._local = { previewMp: true, previewOther: false }
}
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -180,12 +180,12 @@ defineExpose({
</script> </script>
<template> <template>
<div class="z-99 border-1 border-[var(--el-border-color)] border-solid"> <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
<!-- 工具栏 --> <!-- 工具栏 -->
<Toolbar <Toolbar
:editor="editorRef" :editor="editorRef"
:editorId="editorId" :editorId="editorId"
class="border-0 b-b-1 border-[var(--el-border-color)] border-solid" class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
/> />
<!-- 编辑器 --> <!-- 编辑器 -->
<Editor <Editor

View File

@ -0,0 +1,4 @@
import { useFormCreateDesigner } from './src/useFormCreateDesigner'
import { useApiSelect } from './src/components/useApiSelect'
export { useFormCreateDesigner, useApiSelect }

View File

@ -0,0 +1,59 @@
<!-- 数据字典 Select 选择器 -->
<template>
<el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
<el-option
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
<el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
<el-checkbox
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-checkbox-group>
</template>
<script lang="ts" setup>
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
defineOptions({ name: 'DictSelect' })
const attrs = useAttrs()
//
interface Props {
dictType: string //
valueType?: 'str' | 'int' | 'bool' //
selectType?: 'select' | 'radio' | 'checkbox' // select checkbox radio
formCreateInject?: any
}
const props = withDefaults(defineProps<Props>(), {
valueType: 'str',
selectType: 'select'
})
//
const getDictOptions = computed(() => {
switch (props.valueType) {
case 'str':
return getStrDictOptions(props.dictType)
case 'int':
return getIntDictOptions(props.dictType)
case 'bool':
return getBoolDictOptions(props.dictType)
default:
return []
}
})
</script>

View File

@ -0,0 +1,143 @@
import request from '@/config/axios'
import { isEmpty } from '@/utils/is'
import { ApiSelectProps } from '@/components/FormCreate/src/type'
import { jsonParse } from '@/utils'
export const useApiSelect = (option: ApiSelectProps) => {
return defineComponent({
name: option.name,
props: {
// 选项标签
labelField: {
type: String,
default: () => option.labelField ?? 'label'
},
// 选项的值
valueField: {
type: String,
default: () => option.valueField ?? 'value'
},
// api 接口
url: {
type: String,
default: () => option.url ?? ''
},
// 请求类型
method: {
type: String,
default: 'GET'
},
// 请求参数
data: {
type: String,
default: ''
},
// 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
selectType: {
type: String,
default: 'select'
},
// 是否多选
multiple: {
type: Boolean,
default: false
}
},
setup(props) {
const attrs = useAttrs()
const options = ref<any[]>([]) // 下拉数据
const getOptions = async () => {
options.value = []
// 接口选择器
if (isEmpty(props.url)) {
return
}
let data = []
switch (props.method) {
case 'GET':
data = await request.get({ url: props.url })
break
case 'POST':
data = await request.post({ url: props.url, data: jsonParse(props.data) })
break
}
if (Array.isArray(data)) {
options.value = data.map((item: any) => ({
label: item[props.labelField],
value: item[props.valueField]
}))
return
}
console.error(`接口[${props.url}] 返回结果不是一个数组`)
}
onMounted(async () => {
await getOptions()
})
const buildSelect = () => {
if (props.multiple) {
// fix多写此步是为了解决 multiple 属性问题
return (
<el-select class="w-1/1" {...attrs} multiple>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
return (
<el-select class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
const buildCheckbox = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-checkbox-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-checkbox key={index} label={item.label} value={item.value} />
))}
</el-checkbox-group>
)
}
const buildRadio = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-radio-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-radio key={index} value={item.value}>
{item.label}
</el-radio>
))}
</el-radio-group>
)
}
return () => (
<>
{props.selectType === 'select'
? buildSelect()
: props.selectType === 'radio'
? buildRadio()
: props.selectType === 'checkbox'
? buildCheckbox()
: buildSelect()}
</>
)
}
})
}

View File

@ -0,0 +1,15 @@
import { useUploadFileRule } from './useUploadFileRule'
import { useUploadImgRule } from './useUploadImgRule'
import { useUploadImgsRule } from './useUploadImgsRule'
import { useDictSelectRule } from './useDictSelectRule'
import { useEditorRule } from './useEditorRule'
import { useSelectRule } from './useSelectRule'
export {
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule,
useDictSelectRule,
useEditorRule,
useSelectRule
}

View File

@ -0,0 +1,147 @@
const selectRule = [
{
type: 'select',
field: 'selectType',
title: '选择器类型',
value: 'select',
options: [
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '多选框', value: 'checkbox' }
],
// 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
control: [
{
value: 'select',
condition: '=',
method: 'hidden',
rule: ['multiple']
}
]
},
{ type: 'switch', field: 'multiple', title: '是否多选' },
{
type: 'switch',
field: 'disabled',
title: '是否禁用'
},
{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
{
type: 'switch',
field: 'collapseTags',
title: '多选时是否将选中值按文字的形式展示'
},
{
type: 'inputNumber',
field: 'multipleLimit',
title: '多选时用户最多可以选择的项目数,为 0 则不限制',
props: { min: 0 }
},
{
type: 'input',
field: 'autocomplete',
title: 'autocomplete 属性'
},
{ type: 'input', field: 'placeholder', title: '占位符' },
{
type: 'switch',
field: 'filterable',
title: '是否可搜索'
},
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
{
type: 'input',
field: 'noMatchText',
title: '搜索条件无匹配时显示的文字'
},
{
type: 'switch',
field: 'remote',
title: '其中的选项是否从服务器远程加载'
},
{
type: 'Struct',
field: 'remoteMethod',
title: '自定义远程搜索方法'
},
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
{
type: 'switch',
field: 'reserveKeyword',
title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
},
{
type: 'switch',
field: 'defaultFirstOption',
title: '在输入框按下回车,选择第一个匹配项'
},
{
type: 'switch',
field: 'popperAppendToBody',
title: '是否将弹出框插入至 body 元素',
value: true
},
{
type: 'switch',
field: 'automaticDropdown',
title: '对于不可搜索的 Select是否在输入框获得焦点后自动弹出选项菜单'
}
]
const apiSelectRule = [
{
type: 'input',
field: 'url',
title: 'url 地址',
props: {
placeholder: '/system/user/simple-list'
}
},
{
type: 'select',
field: 'method',
title: '请求类型',
value: 'GET',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }
],
control: [
{
value: 'GET',
condition: '!=',
method: 'hidden',
rule: [
{
type: 'input',
field: 'data',
title: '请求参数 JSON 格式',
props: {
autosize: true,
type: 'textarea',
placeholder: '{"type": 1}'
}
}
]
}
]
},
{
type: 'input',
field: 'labelField',
title: 'label 属性',
props: {
placeholder: 'nickname'
}
},
{
type: 'input',
field: 'valueField',
title: 'value 属性',
props: {
placeholder: 'id'
}
}
]
export { selectRule, apiSelectRule }

View File

@ -0,0 +1,62 @@
import { generateUUID } from '@/utils'
import * as DictDataApi from '@/api/system/dict/dict.type'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* 使使 useSelectRule
*/
export const useDictSelectRule = () => {
const label = '字典选择器'
const name = 'DictSelect'
const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
onMounted(async () => {
const data = await DictDataApi.getSimpleDictTypeList()
if (!data || data.length === 0) {
return
}
dictOptions.value =
data?.map((item: DictDataApi.DictTypeVO) => ({
label: item.name,
value: item.type
})) ?? []
})
return {
icon: 'icon-doc-text',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'dictType',
title: '字典类型',
value: '',
options: dictOptions.value
},
{
type: 'select',
field: 'dictValueType',
title: '字典值类型',
value: 'str',
options: [
{ label: '数字', value: 'int' },
{ label: '字符串', value: 'str' },
{ label: '布尔值', value: 'bool' }
]
},
...selectRule
])
}
}
}

View File

@ -0,0 +1,32 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useEditorRule = () => {
const label = '富文本'
const name = 'Editor'
return {
icon: 'icon-editor',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'input',
field: 'height',
title: '高度'
},
{ type: 'switch', field: 'readonly', title: '是否只读' }
])
}
}
}

View File

@ -0,0 +1,34 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
import { SelectRuleOption } from '@/components/FormCreate/src/type'
/**
* hook
*
* @param option
*/
export const useSelectRule = (option: SelectRuleOption) => {
const label = option.label
const name = option.name
return {
icon: option.icon,
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
if (!option.props) {
option.props = []
}
return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule])
}
}
}

View File

@ -0,0 +1,80 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadFileRule = () => {
const label = '文件上传'
const name = 'UploadFile'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'fileType',
title: '文件类型',
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
options: [
{ label: 'doc', value: 'doc' },
{ label: 'xls', value: 'xls' },
{ label: 'ppt', value: 'ppt' },
{ label: 'txt', value: 'txt' },
{ label: 'pdf', value: 'pdf' }
],
props: {
multiple: true
}
},
{
type: 'switch',
field: 'autoUpload',
title: '是否在选取文件后立即进行上传',
value: true
},
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'switch',
field: 'isShowTip',
title: '是否显示提示',
value: true
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false
}
])
}
}
}

View File

@ -0,0 +1,89 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgRule = () => {
const label = '单图上传'
const name = 'UploadImg'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
},
{
type: 'switch',
field: 'disabled',
title: '是否显示删除按钮',
value: true
},
{
type: 'switch',
field: 'showBtnText',
title: '是否显示按钮文字',
value: true
}
])
}
}
}

View File

@ -0,0 +1,84 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgsRule = () => {
const label = '多图上传'
const name = 'UploadImgs'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
}
])
}
}
}

View File

@ -0,0 +1,50 @@
import { Rule } from '@form-create/element-ui' //左侧拖拽按钮
// 左侧拖拽按钮
export interface MenuItem {
label: string
name: string
icon: string
}
// 左侧拖拽按钮分类
export interface Menu {
title: string
name: string
list: MenuItem[]
}
export interface MenuList extends Array<Menu> {}
// 拖拽组件的规则
export interface DragRule {
icon: string
name: string
label: string
children?: string
inside?: true
drag?: true | String
dragBtn?: false
mask?: false
rule(): Rule
props(v: any, v1: any): Rule[]
}
// 通用下拉组件 Props 类型
export interface ApiSelectProps {
name: string // 组件名称
labelField?: string // 选项标签
valueField?: string // 选项的值
url?: string // url 接口
isDict?: boolean // 是否字典选择器
}
// 选择组件规则配置类型
export interface SelectRuleOption {
label: string // label 名称
name: string // 组件名称
icon: string // 组件图标
props?: any[] // 组件规则
}

View File

@ -0,0 +1,100 @@
import {
useDictSelectRule,
useEditorRule,
useSelectRule,
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule
} from './config'
import { Ref } from 'vue'
import { Menu } from '@/components/FormCreate/src/type'
import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* hook
*
* -
* -
* -
* -
* -
* -
* -
*/
export const useFormCreateDesigner = async (designer: Ref) => {
const editorRule = useEditorRule()
const uploadFileRule = useUploadFileRule()
const uploadImgRule = useUploadImgRule()
const uploadImgsRule = useUploadImgsRule()
/**
*
*/
const buildFormComponents = () => {
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
designer.value?.removeMenuItem('upload')
// 移除自带的富文本组件规则,使用 editorRule 替代
designer.value?.removeMenuItem('fc-editor')
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
components.forEach((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `main` 分类下
designer.value?.appendMenuItem('main', {
icon: component.icon,
name: component.name,
label: component.label
})
})
}
const userSelectRule = useSelectRule({
name: 'UserSelect',
label: '用户选择器',
icon: 'icon-user-o'
})
const deptSelectRule = useSelectRule({
name: 'DeptSelect',
label: '部门选择器',
icon: 'icon-address-card-o'
})
const dictSelectRule = useDictSelectRule()
const apiSelectRule0 = useSelectRule({
name: 'ApiSelect',
label: '接口选择器',
icon: 'icon-server',
props: [...apiSelectRule]
})
/**
*
*/
const buildSystemMenu = () => {
// 移除自带的下拉选择器组件,使用 currencySelectRule 替代
designer.value?.removeMenuItem('select')
designer.value?.removeMenuItem('radio')
designer.value?.removeMenuItem('checkbox')
const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
const menu: Menu = {
name: 'system',
title: '系统字段',
list: components.map((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `system` 分类下
return {
icon: component.icon,
name: component.name,
label: component.label
}
})
}
designer.value?.addMenu(menu)
}
onMounted(async () => {
await nextTick()
buildFormComponents()
buildSystemMenu()
})
}

View File

@ -0,0 +1,79 @@
// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
export function makeRequiredRule() {
return {
type: 'Required',
field: 'formCreate$required',
title: '是否必填'
}
}
export const localeProps = (t, prefix, rules) => {
return rules.map((rule) => {
if (rule.field === 'formCreate$required') {
rule.title = t('props.required') || rule.title
} else if (rule.field && rule.field !== '_optionType') {
rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
}
return rule
})
}
export function upper(str) {
return str.replace(str[0], str[0].toLocaleUpperCase())
}
export function makeOptionsRule(t, to, userOptions) {
console.log(userOptions[0])
const options = [
{ label: t('props.optionsType.struct'), value: 0 },
{ label: t('props.optionsType.json'), value: 1 },
{ label: '用户数据', value: 2 }
]
const control = [
{
value: 0,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 1,
rule: [
{
type: 'Struct',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 2,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { modelValue: [] }
}
]
}
]
options.splice(0, 0)
control.push()
return {
type: 'radio',
title: t('props.options'),
field: '_optionType',
value: 0,
options,
props: {
type: 'button'
},
control
}
}

View File

@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
initialIndex = 0, initialIndex = 0,
infinite = true, infinite = true,
hideOnClickModal = false, hideOnClickModal = false,
appendToBody = false, teleported = false,
zIndex = 2000, zIndex = 2000,
show = true show = true
} = options } = options
@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
propsData.initialIndex = initialIndex propsData.initialIndex = initialIndex
propsData.infinite = infinite propsData.infinite = infinite
propsData.hideOnClickModal = hideOnClickModal propsData.hideOnClickModal = hideOnClickModal
propsData.appendToBody = appendToBody propsData.teleported = teleported
propsData.zIndex = zIndex propsData.zIndex = zIndex
propsData.show = show propsData.show = show

View File

@ -13,7 +13,7 @@ const props = defineProps({
initialIndex: propTypes.number.def(0), initialIndex: propTypes.number.def(0),
infinite: propTypes.bool.def(true), infinite: propTypes.bool.def(true),
hideOnClickModal: propTypes.bool.def(false), hideOnClickModal: propTypes.bool.def(false),
appendToBody: propTypes.bool.def(false), teleported: propTypes.bool.def(false),
show: propTypes.bool.def(false) show: propTypes.bool.def(false)
}) })

View File

@ -4,6 +4,6 @@ export interface ImageViewerProps {
initialIndex?: number initialIndex?: number
infinite?: boolean infinite?: boolean
hideOnClickModal?: boolean hideOnClickModal?: boolean
appendToBody?: boolean teleported?: boolean
show?: boolean show?: boolean
} }

View File

@ -189,7 +189,7 @@ const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas) const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
// //
const selectedHotAreaIndex = ref(-1) const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (hotArea: Rect, index: number) => { const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index) emit('hotAreaSelected', hotArea, index)

View File

@ -23,7 +23,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { OperateLogV2VO } from '@/api/system/operatelog' import { OperateLogVO } from '@/api/system/operatelog'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict' import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
import { ElTag } from 'element-plus' import { ElTag } from 'element-plus'
@ -31,7 +31,7 @@ import { ElTag } from 'element-plus'
defineOptions({ name: 'OperateLogV2' }) defineOptions({ name: 'OperateLogV2' })
interface Props { interface Props {
logList: OperateLogV2VO[] // logList: OperateLogVO[] //
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {

View File

@ -53,7 +53,7 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination']) const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const currentPage = computed({ const currentPage = computed({
get() { get() {
return props.page return props.page

View File

@ -26,7 +26,7 @@
placeholder="请输入菜单内容" placeholder="请输入菜单内容"
:remote-method="remoteMethod" :remote-method="remoteMethod"
class="overflow-hidden transition-all-600" class="overflow-hidden transition-all-600"
:class="showTopSearch ? 'w-220px ml2' : 'w-0'" :class="showTopSearch ? '!w-220px ml2' : '!w-0'"
@change="handleChange" @change="handleChange"
> >
<el-option <el-option

View File

@ -6,7 +6,9 @@
:action="uploadUrl" :action="uploadUrl"
:auto-upload="autoUpload" :auto-upload="autoUpload"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:limit="props.limit" :limit="props.limit"
:multiple="props.limit > 1" :multiple="props.limit > 1"
:on-error="excelUploadError" :on-error="excelUploadError"
@ -15,15 +17,14 @@
:on-remove="handleRemove" :on-remove="handleRemove"
:on-success="handleFileSuccess" :on-success="handleFileSuccess"
:show-file-list="true" :show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader" class="upload-file-uploader"
name="file" name="file"
> >
<el-button type="primary"> <el-button v-if="!disabled" type="primary">
<Icon icon="ep:upload-filled" /> <Icon icon="ep:upload-filled" />
选取文件 选取文件
</el-button> </el-button>
<template v-if="isShowTip" #tip> <template v-if="isShowTip && !disabled" #tip>
<div style="font-size: 8px"> <div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div> </div>
@ -31,6 +32,25 @@
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
</div> </div>
</template> </template>
<template #file="row">
<div class="flex items-center">
<span>{{ row.file.name }}</span>
<div class="ml-10px">
<el-link
:href="row.file.url"
:underline="false"
download
target="_blank"
type="primary"
>
下载
</el-link>
</div>
<div class="ml-10px">
<el-button link type="danger" @click="handleRemove(row.file)"> 删除</el-button>
</div>
</div>
</template>
</el-upload> </el-upload>
</div> </div>
</template> </template>
@ -48,13 +68,13 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({ const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired, modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
title: propTypes.string.def('文件上传'),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg'] fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // (MB) fileSize: propTypes.number.def(5), // (MB)
limit: propTypes.number.def(5), // limit: propTypes.number.def(5), //
autoUpload: propTypes.bool.def(true), // autoUpload: propTypes.bool.def(true), //
drag: propTypes.bool.def(false), // drag: propTypes.bool.def(false), //
isShowTip: propTypes.bool.def(true) // isShowTip: propTypes.bool.def(true), //
disabled: propTypes.bool.def(false) // ==> false
}) })
// ========== ========== // ========== ==========

View File

@ -6,17 +6,18 @@
:action="uploadUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:multiple="false" :multiple="false"
:on-error="uploadError" :on-error="uploadError"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:show-file-list="false" :show-file-list="false"
:http-request="httpRequest"
> >
<template v-if="modelValue"> <template v-if="modelValue">
<img :src="modelValue" class="upload-image" /> <img :src="modelValue" class="upload-image" />
<div class="upload-handle" @click.stop> <div class="upload-handle" @click.stop>
<div class="handle-icon" @click="editImg" v-if="!disabled"> <div v-if="!disabled" class="handle-icon" @click="editImg">
<Icon icon="ep:edit" /> <Icon icon="ep:edit" />
<span v-if="showBtnText">{{ t('action.edit') }}</span> <span v-if="showBtnText">{{ t('action.edit') }}</span>
</div> </div>
@ -77,10 +78,8 @@ const props = defineProps({
height: propTypes.string.def('150px'), // ==> 150px height: propTypes.string.def('150px'), // ==> 150px
width: propTypes.string.def('150px'), // ==> 150px width: propTypes.string.def('150px'), // ==> 150px
borderradius: propTypes.string.def('8px'), // ==> 8px borderradius: propTypes.string.def('8px'), // ==> 8px
// showDelete: propTypes.bool.def(true), //
showDelete: propTypes.bool.def(true), showBtnText: propTypes.bool.def(true) //
//
showBtnText: propTypes.bool.def(true)
}) })
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //

View File

@ -6,13 +6,14 @@
:action="uploadUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:limit="limit" :limit="limit"
:multiple="true" :multiple="true"
:on-error="uploadError" :on-error="uploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:http-request="httpRequest"
list-type="picture-card" list-type="picture-card"
> >
<div class="upload-empty"> <div class="upload-empty">

View File

@ -17,7 +17,11 @@ export const useUpload = () => {
// 1.2 获取文件预签名地址 // 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName) const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持) // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
return axios.put(presignedInfo.uploadUrl, options.file).then(() => { return axios.put(presignedInfo.uploadUrl, options.file, {
headers: {
'Content-Type': options.file.type,
}
}).then(() => {
// 1.4. 记录文件信息到后端(异步) // 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, fileName, options.file) createFile(presignedInfo, fileName, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致 // 通知成功,数据格式保持与后端上传的返回结果一致

View File

@ -3,13 +3,6 @@
<el-form label-width="90px" :model="needProps" :rules="rules"> <el-form label-width="90px" :model="needProps" :rules="rules">
<div v-if="needProps.type == 'bpmn:Process'"> <div v-if="needProps.type == 'bpmn:Process'">
<!-- 如果是 Process 信息的时候使用自定义表单 --> <!-- 如果是 Process 信息的时候使用自定义表单 -->
<el-link
href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B"
type="danger"
target="_blank"
>
如何实现实现会签或签
</el-link>
<el-form-item label="流程标识" prop="id"> <el-form-item label="流程标识" prop="id">
<el-input <el-input
v-model="needProps.id" v-model="needProps.id"
@ -139,14 +132,6 @@ const updateBaseInfo = (key) => {
} }
} }
onMounted(() => {
// bpmn 1 key name
setTimeout(() => {
handleKeyUpdate(props.model.key)
handleNameUpdate(props.model.name)
}, 110)
})
watch( watch(
() => props.businessObject, () => props.businessObject,
(val) => { (val) => {

View File

@ -79,13 +79,13 @@ const resetFlowCondition = () => {
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
bpmnElementSource.value = bpmnElement.value.source bpmnElementSource.value = bpmnElement.value.source
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
// typedefault
flowConditionForm.value = { type: 'default' }
if ( if (
bpmnElementSourceRef.value && bpmnElementSourceRef.value &&
bpmnElementSourceRef.value.default && bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id && bpmnElementSourceRef.value.default.id === bpmnElement.value.id
flowConditionForm.value.type == 'default'
) { ) {
//
flowConditionForm.value = { type: 'default' } flowConditionForm.value = { type: 'default' }
} else if (!bpmnElement.value.businessObject.conditionExpression) { } else if (!bpmnElement.value.businessObject.conditionExpression) {
// //

View File

@ -13,7 +13,7 @@ import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } f
import errorCode from './errorCode' import errorCode from './errorCode'
import { resetRouter } from '@/router' import { resetRouter } from '@/router'
import { useCache } from '@/hooks/web/useCache' import { deleteUserCache } from '@/hooks/web/useCache'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const { result_code, base_url, request_timeout } = config const { result_code, base_url, request_timeout } = config
@ -217,9 +217,8 @@ const handleAuthorized = () => {
confirmButtonText: t('login.relogin'), confirmButtonText: t('login.relogin'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const { wsCache } = useCache()
resetRouter() // 重置静态路由表 resetRouter() // 重置静态路由表
wsCache.clear() deleteUserCache() // 删除用户缓存
removeToken() removeToken()
isRelogin.show = false isRelogin.show = false
// 干掉token后再走一次路由让它过router.beforeEach的校验 // 干掉token后再走一次路由让它过router.beforeEach的校验

View File

@ -7,13 +7,18 @@ import WebStorageCache from 'web-storage-cache'
type CacheType = 'localStorage' | 'sessionStorage' type CacheType = 'localStorage' | 'sessionStorage'
export const CACHE_KEY = { export const CACHE_KEY = {
IS_DARK: 'isDark', // 用户相关
ROLE_ROUTERS: 'roleRouters',
USER: 'user', USER: 'user',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang', LANG: 'lang',
THEME: 'theme', THEME: 'theme',
LAYOUT: 'layout', LAYOUT: 'layout',
ROLE_ROUTERS: 'roleRouters', DICT_CACHE: 'dictCache',
DICT_CACHE: 'dictCache' // 登录表单
LoginForm: 'loginForm',
TenantId: 'tenantId'
} }
export const useCache = (type: CacheType = 'localStorage') => { export const useCache = (type: CacheType = 'localStorage') => {
@ -25,3 +30,10 @@ export const useCache = (type: CacheType = 'localStorage') => {
wsCache wsCache
} }
} }
export const deleteUserCache = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 注意,不要清理 LoginForm 登录表单
}

View File

@ -24,13 +24,12 @@ const toggleCollapse = () => {
</script> </script>
<template> <template>
<div :class="prefixCls"> <div :class="prefixCls" @click="toggleCollapse">
<Icon <Icon
:color="color" :color="color"
:icon="collapse ? 'ep:expand' : 'ep:fold'" :icon="collapse ? 'ep:expand' : 'ep:fold'"
:size="18" :size="18"
class="cursor-pointer" class="cursor-pointer"
@click="toggleCollapse"
/> />
</div> </div>
</template> </template>

View File

@ -124,16 +124,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
$prefix-cls: #{$namespace}-menu; $prefix-cls: #{$namespace}-menu;
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
.#{$prefix-cls} { .#{$prefix-cls} {
position: relative; position: relative;
transition: width var(--transition-time-02); transition: width var(--transition-time-02);
@ -159,7 +149,6 @@ $prefix-cls: #{$namespace}-menu;
} }
// //
.#{$elNamespace}-sub-menu.is-active,
.#{$elNamespace}-menu-item.is-active { .#{$elNamespace}-menu-item.is-active {
color: var(--left-menu-text-active-color) !important; color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important; background-color: var(--left-menu-bg-active-color) !important;
@ -171,10 +160,6 @@ $prefix-cls: #{$namespace}-menu;
.#{$elNamespace}-menu-item.is-active { .#{$elNamespace}-menu-item.is-active {
position: relative; position: relative;
&::after {
@extend .is-active--after;
}
} }
// //
@ -194,10 +179,6 @@ $prefix-cls: #{$namespace}-menu;
& > .is-active > .#{$elNamespace}-sub-menu__title { & > .is-active > .#{$elNamespace}-sub-menu__title {
position: relative; position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important; background-color: var(--left-menu-collapse-bg-active-color) !important;
&::after {
@extend .is-active--after;
}
} }
} }
@ -245,16 +226,6 @@ $prefix-cls: #{$namespace}-menu;
<style lang="scss"> <style lang="scss">
$prefix-cls: #{$namespace}-menu-popper; $prefix-cls: #{$namespace}-menu-popper;
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
.#{$prefix-cls}--vertical, .#{$prefix-cls}--vertical,
.#{$prefix-cls}--horizontal { .#{$prefix-cls}--horizontal {
// //
@ -281,10 +252,6 @@ $prefix-cls: #{$namespace}-menu-popper;
&:hover { &:hover {
background-color: var(--left-menu-bg-active-color) !important; background-color: var(--left-menu-bg-active-color) !important;
} }
&::after {
@extend .is-active--after;
}
} }
} }
</style> </style>

View File

@ -1,47 +1,39 @@
import { ElSubMenu, ElMenuItem } from 'element-plus' import { ElSubMenu, ElMenuItem } from 'element-plus'
import type { RouteMeta } from 'vue-router'
import { hasOneShowingChild } from '../helper' import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle' import { useRenderMenuTitle } from './useRenderMenuTitle'
import { useDesign } from '@/hooks/web/useDesign'
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
export const useRenderMenuItem = ( const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [], // allRouters: AppRouteRecordRaw[] = [],
menuMode: 'vertical' | 'horizontal' {
) => {
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers.map((v) => { return routers
const meta = (v.meta ?? {}) as RouteMeta .filter((v) => !v.meta?.hidden)
if (!meta.hidden) { .map((v) => {
const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
const { renderMenuTitle } = useRenderMenuTitle()
if ( if (
oneShowingChild && oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow !meta?.alwaysShow
) { ) {
return ( return (
<ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}> <ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{ {{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}} }}
</ElMenuItem> </ElMenuItem>
) )
} else { } else {
const { getPrefixCls } = useDesign()
const preFixCls = getPrefixCls('menu-popper')
return ( return (
<ElSubMenu <ElSubMenu index={fullPath}>
index={fullPath}
popperClass={
menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
}
>
{{ {{
title: () => renderMenuTitle(meta), title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath) default: () => renderMenuItem(v.children!, fullPath)
@ -49,7 +41,6 @@ export const useRenderMenuItem = (
</ElSubMenu> </ElSubMenu>
) )
} }
}
}) })
} }

View File

@ -1,5 +1,6 @@
import type { RouteMeta } from 'vue-router' import type { RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n'
export const useRenderMenuTitle = () => { export const useRenderMenuTitle = () => {
const renderMenuTitle = (meta: RouteMeta) => { const renderMenuTitle = (meta: RouteMeta) => {
@ -9,10 +10,14 @@ export const useRenderMenuTitle = () => {
return icon ? ( return icon ? (
<> <>
<Icon icon={meta.icon}></Icon> <Icon icon={meta.icon}></Icon>
<span class="v-menu__title">{t(title as string)}</span> <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</> </>
) : ( ) : (
<span class="v-menu__title">{t(title as string)}</span> <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
) )
} }

View File

@ -139,7 +139,7 @@ export default defineComponent({
id={`${variables.namespace}-menu`} id={`${variables.namespace}-menu`}
class={[ class={[
prefixCls, prefixCls,
'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right', 'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
{ {
'w-[var(--tab-menu-max-width)]': !unref(collapse), 'w-[var(--tab-menu-max-width)]': !unref(collapse),
'w-[var(--tab-menu-min-width)]': unref(collapse) 'w-[var(--tab-menu-min-width)]': unref(collapse)
@ -195,7 +195,7 @@ export default defineComponent({
</div> </div>
<Menu <Menu
class={[ class={[
'!absolute top-0', '!absolute top-0 z-11',
{ {
'!left-[var(--tab-menu-min-width)]': unref(collapse), '!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse), '!left-[var(--tab-menu-max-width)]': !unref(collapse),

View File

@ -5,6 +5,9 @@ import avatarImg from '@/assets/imgs/avatar.gif'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import LockDialog from './components/LockDialog.vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock'
defineOptions({ name: 'UserInfo' }) defineOptions({ name: 'UserInfo' })
@ -23,6 +26,14 @@ const prefixCls = getPrefixCls('user-info')
const avatar = computed(() => userStore.user.avatar ?? avatarImg) const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin') const userName = computed(() => userStore.user.nickname ?? 'Admin')
//
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const dialogVisible = ref<boolean>(false)
const lockScreen = () => {
dialogVisible.value = true
}
const loginOut = async () => { const loginOut = async () => {
try { try {
await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
@ -33,8 +44,7 @@ const loginOut = async () => {
await userStore.loginOut() await userStore.loginOut()
tagsViewStore.delAllViews() tagsViewStore.delAllViews()
replace('/login?redirect=/index') replace('/login?redirect=/index')
} } catch {}
catch { }
} }
const toProfile = async () => { const toProfile = async () => {
push('/user/profile') push('/user/profile')
@ -62,6 +72,10 @@ const toDocument = () => {
<Icon icon="ep:menu" /> <Icon icon="ep:menu" />
<div @click="toDocument">{{ t('common.document') }}</div> <div @click="toDocument">{{ t('common.document') }}</div>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem divided>
<Icon icon="ep:lock" />
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem divided @click="loginOut"> <ElDropdownItem divided @click="loginOut">
<Icon icon="ep:switch-button" /> <Icon icon="ep:switch-button" />
<div>{{ t('common.loginOut') }}</div> <div>{{ t('common.loginOut') }}</div>
@ -69,4 +83,31 @@ const toDocument = () => {
</ElDropdownMenu> </ElDropdownMenu>
</template> </template>
</ElDropdown> </ElDropdown>
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
<teleport to="body">
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</teleport>
</template> </template>
<style scoped lang="scss">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition:
opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import { useValidator } from '@/hooks/web/useValidator'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/store/modules/user'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
console.log('set: ', val)
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const formData = ref({
password: undefined
})
const formRules = reactive({
password: [required()]
})
const formRef = ref() // Ref
const handleLock = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
dialogVisible.value = false
lockStore.setLockInfo({
...formData.value,
isLock: true
})
}
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">
{{ userName }}
</span>
</div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item :label="t('lock.lockPassword')" prop="password">
<el-input
type="password"
v-model="formData.password"
:placeholder="'请输入' + t('lock.lockPassword')"
clearable
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>
</Dialog>
</template>
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
max-width: calc(100vw - 16px);
}
}
</style>

View File

@ -0,0 +1,270 @@
<script lang="ts" setup>
import { resetRouter } from '@/router'
import { deleteUserCache } from '@/hooks/web/useCache'
import { useLockStore } from '@/store/modules/lock'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
const tagsViewStore = useTagsViewStore()
const { replace } = useRouter()
const userStore = useUserStore()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
//
async function unLock() {
if (!password.value) {
return
}
let pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
//
async function goLogin() {
await userStore.loginOut().catch(() => {})
//
deleteUserCache() //
tagsViewStore.delAllViews()
resetRouter() //
lockStore.resetLockInfo()
replace('/login')
}
function handleShowForm(show = false) {
showDate.value = show
}
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">
{{ userName }}
</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: '#{$namespace}-lock-page';
// Small screen / tablet
$screen-sm: 576px;
// Medium screen / desktop
$screen-md: 768px;
// Large screen / wide desktop
$screen-lg: 992px;
// Extra large screen / full hd
$screen-xl: 1200px;
// Extra extra large screen / large desktop
$screen-2xl: 1600px;
$error-color: #ed6f6f;
.#{$prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: $screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: $screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: $error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@ -56,6 +56,16 @@ export default {
copySuccess: 'Copy Success', copySuccess: 'Copy Success',
copyError: 'Copy Error' copyError: 'Copy Error'
}, },
lock: {
lockScreen: 'Lock screen',
lock: 'Lock',
lockPassword: 'Lock screen password',
unlock: 'Click to unlock',
backToLogin: 'Back to login',
entrySystem: 'Entry the system',
placeholder: 'Please enter the lock screen password',
message: 'Lock screen password error'
},
error: { error: {
noPermission: `Sorry, you don't have permission to access this page.`, noPermission: `Sorry, you don't have permission to access this page.`,
pageError: 'Sorry, the page you visited does not exist.', pageError: 'Sorry, the page you visited does not exist.',

View File

@ -56,6 +56,16 @@ export default {
copySuccess: '复制成功', copySuccess: '复制成功',
copyError: '复制失败' copyError: '复制失败'
}, },
lock: {
lockScreen: '锁定屏幕',
lock: '锁定',
lockPassword: '锁屏密码',
unlock: '点击解锁',
backToLogin: '返回登录',
entrySystem: '进入系统',
placeholder: '请输入锁屏密码',
message: '锁屏密码错误'
},
error: { error: {
noPermission: `抱歉,您无权访问此页面。`, noPermission: `抱歉,您无权访问此页面。`,
pageError: '抱歉,您访问的页面不存在。', pageError: '抱歉,您访问的页面不存在。',

View File

@ -2,23 +2,24 @@ import * as echarts from 'echarts/core'
import { import {
BarChart, BarChart,
FunnelChart,
GaugeChart,
LineChart, LineChart,
PieChart,
MapChart, MapChart,
PictorialBarChart, PictorialBarChart,
RadarChart, PieChart,
GaugeChart RadarChart
} from 'echarts/charts' } from 'echarts/charts'
import { import {
TitleComponent,
TooltipComponent,
GridComponent,
PolarComponent,
AriaComponent, AriaComponent,
ParallelComponent, GridComponent,
LegendComponent, LegendComponent,
ParallelComponent,
PolarComponent,
TitleComponent,
ToolboxComponent, ToolboxComponent,
TooltipComponent,
VisualMapComponent VisualMapComponent
} from 'echarts/components' } from 'echarts/components'
@ -41,7 +42,8 @@ echarts.use([
CanvasRenderer, CanvasRenderer,
PictorialBarChart, PictorialBarChart,
RadarChart, RadarChart,
GaugeChart GaugeChart,
FunnelChart
]) ])
export default echarts export default echarts

View File

@ -1,23 +1,45 @@
import type { App } from 'vue' import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件 // 👇使用 form-create 需额外全局引入 element plus 组件
import { import {
ElAlert,
ElAside, ElAside,
ElPopconfirm,
ElHeader,
ElMain,
ElContainer, ElContainer,
ElDivider, ElDivider,
ElTransfer, ElHeader,
ElAlert, ElMain,
ElTabs, ElPopconfirm,
ElTable, ElTable,
ElTableColumn, ElTableColumn,
ElTabPane ElTabPane,
ElTabs,
ElTransfer
} from 'element-plus' } from 'element-plus'
import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui' import formCreate from '@form-create/element-ui'
import install from '@form-create/element-ui/auto-import' import install from '@form-create/element-ui/auto-import'
//======================= 自定义组件 =======================
import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
import { useApiSelect } from '@/components/FormCreate'
import { Editor } from '@/components/Editor'
import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
const UserSelect = useApiSelect({
name: 'UserSelect',
labelField: 'nickname',
valueField: 'id',
url: '/system/user/simple-list'
})
const DeptSelect = useApiSelect({
name: 'DeptSelect',
labelField: 'name',
valueField: 'id',
url: '/system/dept/simple-list'
})
const ApiSelect = useApiSelect({
name: 'ApiSelect'
})
const components = [ const components = [
ElAside, ElAside,
ElPopconfirm, ElPopconfirm,
@ -30,7 +52,15 @@ const components = [
ElTabs, ElTabs,
ElTable, ElTable,
ElTableColumn, ElTableColumn,
ElTabPane ElTabPane,
UploadImg,
UploadImgs,
UploadFile,
DictSelect,
UserSelect,
DeptSelect,
ApiSelect,
Editor
] ]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
@ -40,4 +70,5 @@ export const setupFormCreate = (app: App<Element>) => {
}) })
formCreate.use(install) formCreate.use(install)
app.use(formCreate) app.use(formCreate)
app.use(FcDesigner)
} }

View File

@ -1,7 +1,9 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia() const store = createPinia()
store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => { export const setupStore = (app: App<Element>) => {
app.use(store) app.use(store)

View File

@ -268,7 +268,8 @@ export const useAppStore = defineStore('app', {
setFooter(footer: boolean) { setFooter(footer: boolean) {
this.footer = footer this.footer = footer
} }
} },
persist: false
}) })
export const useAppStoreWithOut = () => { export const useAppStoreWithOut = () => {

48
src/store/modules/lock.ts Normal file
View File

@ -0,0 +1,48 @@
import { defineStore } from 'pinia'
import { store } from '@/store'
interface lockInfo {
isLock?: boolean
password?: string
}
interface LockState {
lockInfo: lockInfo
}
export const useLockStore = defineStore('lock', {
state: (): LockState => {
return {
lockInfo: {
// isLock: false, // 是否锁定屏幕
// password: '' // 锁屏密码
}
}
},
getters: {
getLockInfo(): lockInfo {
return this.lockInfo
}
},
actions: {
setLockInfo(lockInfo: lockInfo) {
this.lockInfo = lockInfo
},
resetLockInfo() {
this.lockInfo = {}
},
unLock(password: string) {
if (this.lockInfo?.password === password) {
this.resetLockInfo()
return true
} else {
return false
}
}
},
persist: true
})
export const useLockStoreWithOut = () => {
return useLockStore(store)
}

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { store } from '../index' import { store } from '@/store'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import remainingRouter from '@/router/modules/remaining' import remainingRouter from '@/router/modules/remaining'
import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
@ -59,7 +59,8 @@ export const usePermissionStore = defineStore('permission', {
setMenuTabRouters(routers: AppRouteRecordRaw[]): void { setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
this.menuTabRouters = routers this.menuTabRouters = routers
} }
} },
persist: false
}) })
export const usePermissionStoreWithOut = () => { export const usePermissionStoreWithOut = () => {

View File

@ -132,7 +132,8 @@ export const useTagsViewStore = defineStore('tagsView', {
} }
} }
} }
} },
persist: false
}) })
export const useTagsViewStoreWithOut = () => { export const useTagsViewStoreWithOut = () => {

View File

@ -1,7 +1,7 @@
import { store } from '../index' import { store } from '@/store'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getAccessToken, removeToken } from '@/utils/auth' import { getAccessToken, removeToken } from '@/utils/auth'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache' import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
import { getInfo, loginOut } from '@/api/login' import { getInfo, loginOut } from '@/api/login'
const { wsCache } = useCache() const { wsCache } = useCache()
@ -14,6 +14,7 @@ interface UserVO {
} }
interface UserInfoVO { interface UserInfoVO {
// USER 缓存
permissions: string[] permissions: string[]
roles: string[] roles: string[]
isSetUser: boolean isSetUser: boolean
@ -80,7 +81,7 @@ export const useUserStore = defineStore('admin-user', {
async loginOut() { async loginOut() {
await loginOut() await loginOut()
removeToken() removeToken()
wsCache.clear() deleteUserCache() // 删除用户缓存
this.resetState() this.resetState()
}, },
resetState() { resetState() {

Binary file not shown.

View File

@ -0,0 +1,22 @@
// 使用字体图标来源 https://fontello.com/
@font-face {
font-family: 'fc-icon';
src: url('@/styles/FormCreate/fonts/fontello.woff') format('woff');
}
.icon-doc-text:before {
content: '\f0f6';
}
.icon-server:before {
content: '\f233';
}
.icon-address-card-o:before {
content: '\f2bc';
}
.icon-user-o:before {
content: '\f2c0';
}

View File

@ -1,4 +1,5 @@
@import './var.css'; @import './var.css';
@import './FormCreate/index.scss';
@import 'element-plus/theme-chalk/dark/css-vars.css'; @import 'element-plus/theme-chalk/dark/css-vars.css';
.reset-margin [class*='el-icon'] + span { .reset-margin [class*='el-icon'] + span {

View File

@ -1,4 +1,4 @@
import { useCache } from '@/hooks/web/useCache' import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
import { TokenType } from '@/api/login/types' import { TokenType } from '@/api/login/types'
import { decrypt, encrypt } from '@/utils/jsencrypt' import { decrypt, encrypt } from '@/utils/jsencrypt'
@ -36,8 +36,6 @@ export const formatToken = (token: string): string => {
} }
// ========== 账号相关 ========== // ========== 账号相关 ==========
const LoginFormKey = 'LOGINFORM'
export type LoginFormType = { export type LoginFormType = {
tenantName: string tenantName: string
username: string username: string
@ -46,7 +44,7 @@ export type LoginFormType = {
} }
export const getLoginForm = () => { export const getLoginForm = () => {
const loginForm: LoginFormType = wsCache.get(LoginFormKey) const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
if (loginForm) { if (loginForm) {
loginForm.password = decrypt(loginForm.password) as string loginForm.password = decrypt(loginForm.password) as string
} }
@ -55,38 +53,19 @@ export const getLoginForm = () => {
export const setLoginForm = (loginForm: LoginFormType) => { export const setLoginForm = (loginForm: LoginFormType) => {
loginForm.password = encrypt(loginForm.password) as string loginForm.password = encrypt(loginForm.password) as string
wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 }) wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
} }
export const removeLoginForm = () => { export const removeLoginForm = () => {
wsCache.delete(LoginFormKey) wsCache.delete(CACHE_KEY.LoginForm)
} }
// ========== 租户相关 ========== // ========== 租户相关 ==========
const TenantIdKey = 'TENANT_ID'
const TenantNameKey = 'TENANT_NAME'
export const getTenantName = () => {
return wsCache.get(TenantNameKey)
}
export const setTenantName = (username: string) => {
wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 })
}
export const removeTenantName = () => {
wsCache.delete(TenantNameKey)
}
export const getTenantId = () => { export const getTenantId = () => {
return wsCache.get(TenantIdKey) return wsCache.get(CACHE_KEY.TenantId)
} }
export const setTenantId = (username: string) => { export const setTenantId = (username: string) => {
wsCache.set(TenantIdKey, username) wsCache.set(CACHE_KEY.TenantId, username)
}
export const removeTenantId = () => {
wsCache.delete(TenantIdKey)
} }

View File

@ -248,15 +248,15 @@ export const CouponTemplateTakeTypeEnum = {
*/ */
export const PromotionProductScopeEnum = { export const PromotionProductScopeEnum = {
ALL: { ALL: {
scope: 10, scope: 1,
name: '通用劵' name: '通用劵'
}, },
SPU: { SPU: {
scope: 20, scope: 2,
name: '商品劵' name: '商品劵'
}, },
CATEGORY: { CATEGORY: {
scope: 30, scope: 3,
name: '品类劵' name: '品类劵'
} }
} }

18
src/utils/dateUtil.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* Independent time operation tool to facilitate subsequent switch to dayjs
*/
// TODO 芋艿:【锁屏】可能后面删除掉
import dayjs from 'dayjs'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const DATE_FORMAT = 'YYYY-MM-DD'
export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
return dayjs(date).format(format)
}
export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
export const dateUtil = dayjs

View File

@ -104,6 +104,7 @@ export enum DICT_TYPE {
USER_TYPE = 'user_type', USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status', COMMON_STATUS = 'common_status',
TERMINAL = 'terminal', // 终端 TERMINAL = 'terminal', // 终端
DATE_INTERVAL = 'date_interval', // 数据间隔
// ========== SYSTEM 模块 ========== // ========== SYSTEM 模块 ==========
SYSTEM_USER_SEX = 'system_user_sex', SYSTEM_USER_SEX = 'system_user_sex',
@ -111,7 +112,6 @@ export enum DICT_TYPE {
SYSTEM_ROLE_TYPE = 'system_role_type', SYSTEM_ROLE_TYPE = 'system_role_type',
SYSTEM_DATA_SCOPE = 'system_data_scope', SYSTEM_DATA_SCOPE = 'system_data_scope',
SYSTEM_NOTICE_TYPE = 'system_notice_type', SYSTEM_NOTICE_TYPE = 'system_notice_type',
SYSTEM_OPERATE_TYPE = 'system_operate_type',
SYSTEM_LOGIN_TYPE = 'system_login_type', SYSTEM_LOGIN_TYPE = 'system_login_type',
SYSTEM_LOGIN_RESULT = 'system_login_result', SYSTEM_LOGIN_RESULT = 'system_login_result',
SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
@ -134,6 +134,7 @@ export enum DICT_TYPE {
INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
INFRA_CODEGEN_SCENE = 'infra_codegen_scene', INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
INFRA_FILE_STORAGE = 'infra_file_storage', INFRA_FILE_STORAGE = 'infra_file_storage',
INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ========== // ========== BPM 模块 ==========
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
@ -196,14 +197,15 @@ export enum DICT_TYPE {
// ========== CRM - 客户管理模块 ========== // ========== CRM - 客户管理模块 ==========
CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态 CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型 CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型
CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式 CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业
CRM_CUSTOMER_LEVEL = 'crm_customer_level', CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别
CRM_CUSTOMER_SOURCE = 'crm_customer_source', CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源
CRM_PRODUCT_STATUS = 'crm_product_status', CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态
CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别 CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别
CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位 CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位
CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式 CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式
// ========== ERP - 企业资源计划模块 ========== // ========== ERP - 企业资源计划模块 ==========
ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态 ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态

View File

@ -40,7 +40,7 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
export const setConfAndFields2 = ( export const setConfAndFields2 = (
detailPreview: object, detailPreview: object,
conf: string, conf: string,
fields: string, fields: string[],
value?: object value?: object
) => { ) => {
if (isRef(detailPreview)) { if (isRef(detailPreview)) {

View File

@ -329,10 +329,11 @@ const ERP_PRICE_DIGIT = 2
* *
* *
* @param num * @param num
* @package digit
* @return * @return
*/ */
export const erpNumberFormatter = (num: number | string | undefined, digit: number) => { export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {
if (num === null) { if (num == null) {
return '' return ''
} }
if (typeof num === 'string') { if (typeof num === 'string') {
@ -404,3 +405,47 @@ export const erpPriceMultiply = (price: number, count: number) => {
} }
return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT)) return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT))
} }
/**
* ERP
*
* total 0 0
*
* @param value
* @param total
*/
export const erpCalculatePercentage = (value: number, total: number) => {
if (total === 0) return 0
return ((value / total) * 100).toFixed(2)
}
/**
* echarts map
*
* @param areaName
*/
export const areaReplace = (areaName: string) => {
if (!areaName) {
return areaName
}
return areaName
.replace('维吾尔自治区', '')
.replace('壮族自治区', '')
.replace('回族自治区', '')
.replace('自治区', '')
.replace('省', '')
}
/**
* JSON
*
* @param str
*/
export function jsonParse(str: string) {
try {
return JSON.parse(str)
} catch (e) {
console.error(`str[${str}] 不是一个 JSON 字符串`)
return ''
}
}

View File

@ -2,6 +2,7 @@ import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
import { cloneDeep, omit } from 'lodash-es' import { cloneDeep, omit } from 'lodash-es'
import qs from 'qs'
const modules = import.meta.glob('../views/**/*.{vue,tsx}') const modules = import.meta.glob('../views/**/*.{vue,tsx}')
/** /**
@ -64,6 +65,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
const res: AppRouteRecordRaw[] = [] const res: AppRouteRecordRaw[] = []
const modulesRoutesKeys = Object.keys(modules) const modulesRoutesKeys = Object.keys(modules)
for (const route of routes) { for (const route of routes) {
// 1. 生成 meta 菜单元数据
const meta = { const meta = {
title: route.name, title: route.name,
icon: route.icon, icon: route.icon,
@ -73,10 +75,20 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
route.children && route.children &&
route.children.length === 1 && route.children.length === 1 &&
(route.alwaysShow !== undefined ? route.alwaysShow : true) (route.alwaysShow !== undefined ? route.alwaysShow : true)
} as any
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
// 这样,后续在 Vue 文件中,可以通过 const { currentRoute } = useRouter() 中,通过 meta.query 获取到参数
if (route.component && route.component.indexOf('?') > -1) {
const query = route.component.split('?')[1]
route.component = route.component.split('?')[0]
meta.query = qs.parse(query)
} }
// 2. 生成 dataAppRouteRecordRaw
// 路由地址转首字母大写驼峰作为路由名称适配keepAlive // 路由地址转首字母大写驼峰作为路由名称适配keepAlive
let data: AppRouteRecordRaw = { let data: AppRouteRecordRaw = {
path: route.path, path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
name: name:
route.componentName && route.componentName.length > 0 route.componentName && route.componentName.length > 0
? route.componentName ? route.componentName

View File

@ -62,7 +62,14 @@
<template #header> <template #header>
<div class="h-3 flex justify-between"> <div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span> <span>{{ t('workplace.project') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link> <el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div> </div>
</template> </template>
<el-skeleton :loading="loading" animated> <el-skeleton :loading="loading" animated>
@ -76,13 +83,13 @@
:sm="24" :sm="24"
:xs="24" :xs="24"
> >
<el-card shadow="hover"> <el-card shadow="hover" class="mr-5px mt-5px">
<div class="flex items-center"> <div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-8px" /> <Icon :icon="item.icon" :size="25" class="mr-8px" />
<span class="text-16px">{{ item.name }}</span> <span class="text-16px">{{ item.name }}</span>
</div> </div>
<div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div> <div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-16px flex justify-between text-12px text-gray-400"> <div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span> <span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span> <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div> </div>
@ -204,45 +211,45 @@ let projects = reactive<Project[]>([])
const getProject = async () => { const getProject = async () => {
const data = [ const data = [
{ {
name: 'Github', name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill', icon: 'akar-icons:github-fill',
message: 'workplace.introduction', message: 'https://github.com/YunaiV/ruoyi-vue-pro',
personal: 'Archer', personal: 'Spring Boot 单体架构',
time: new Date() time: new Date()
}, },
{ {
name: 'Vue', name: 'yudao-ui-admin-vue3',
icon: 'logos:vue', icon: 'logos:vue',
message: 'workplace.introduction', message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Archer', personal: 'Vue3 + element-plus',
time: new Date() time: new Date()
}, },
{ {
name: 'Angular', name: 'yudao-ui-admin-vben',
icon: 'logos:angular-icon', icon: 'logos:vue',
message: 'workplace.introduction', message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Archer', personal: 'Vue3 + vben(antd)',
time: new Date() time: new Date()
}, },
{ {
name: 'React', name: 'yudao-cloud',
icon: 'logos:react', icon: 'akar-icons:github',
message: 'workplace.introduction', message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Archer', personal: 'Spring Cloud 微服务架构',
time: new Date() time: new Date()
}, },
{ {
name: 'Webpack', name: 'yudao-ui-mall-uniapp',
icon: 'logos:webpack', icon: 'logos:vue',
message: 'workplace.introduction', message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Archer', personal: 'Vue3 + uniapp',
time: new Date() time: new Date()
}, },
{ {
name: 'Vite', name: 'yudao-ui-admin-vue2',
icon: 'vscode-icons:file-type-vite', icon: 'logos:vue',
message: 'workplace.introduction', message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Archer', personal: 'Vue2 + element-ui',
time: new Date() time: new Date()
} }
] ]
@ -254,27 +261,27 @@ let notice = reactive<Notice[]>([])
const getNotice = async () => { const getNotice = async () => {
const data = [ const data = [
{ {
title: '系统升级版本', title: '系统支持 JDK 8/17/21Vue 2/3',
type: '通知', type: '通知',
keys: ['通知', '升级'], keys: ['通知', '8', '17', '21', '2', '3'],
date: new Date() date: new Date()
}, },
{ {
title: '系统凌晨维护', title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '公告', type: '公告',
keys: ['公告', '维护'], keys: ['公告', 'Boot', 'Cloud'],
date: new Date() date: new Date()
}, },
{ {
title: '系统升级版本', title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '通知', type: '通知',
keys: ['通知', '升级'], keys: ['通知', '无需授权'],
date: new Date() date: new Date()
}, },
{ {
title: '系统凌晨维护', title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
type: '公告', type: '公告',
keys: ['公告', '维护'], keys: ['公告', '最广泛'],
date: new Date() date: new Date()
} }
] ]

View File

@ -64,7 +64,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="loginData.tenantEnable" prop="tenantName">
<el-input <el-input
v-model="loginData.loginForm.tenantName" v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')" :placeholder="t('login.tenantNamePlaceholder')"
@ -207,7 +207,7 @@ const loginData = reactive({
// //
const getCode = async () => { const getCode = async () => {
// //
if (loginData.captchaEnable) { if (!loginData.captchaEnable) {
await handleLogin({}) await handleLogin({})
} else { } else {
// //

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