【v3.8.0 合并】Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	README.md
#	jeecg-boot/db/tables_nacos.sql
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oConvertUtils.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger2Config.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java
#	jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgDemoController.java
#	jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgDemo.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/controller/OpenApiController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/controller/OpenApiLogController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/controller/OpenApiPermissionController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/entity/OpenApi.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/entity/OpenApiAuth.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/entity/OpenApiHeader.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/entity/OpenApiLog.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/entity/OpenApiParam.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/filter/ApiAuthFilter.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/mapper/OpenApiLogMapper.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/service/OpenApiLogService.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/DuplicateCheckController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/LoginController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysCommentController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDataSourceController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartPermissionController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartRoleController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictItemController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysGatewayRouteController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleIndexController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTableWhiteListController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysCheckRule.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysComment.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysDataSource.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysDepartPermission.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysDepartRole.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysDepartRolePermission.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysDepartRoleUser.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysFillRule.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysFormFile.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysGatewayRoute.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysPackPermission.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysPosition.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysRoleIndex.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysTableWhiteList.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysTenantPack.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysTenantPackUser.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAccount.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysUserPosition.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysUserTenant.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/model/DuplicateCheckVo.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/controller/${entityPackage}/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/config/flyway/FlywayConfig.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/SampleTest.java
#	jeecg-boot/jeecg-server-cloud/jeecg-cloud-gateway/src/main/java/org/jeecg/handler/swagger/SwaggerResourceController.java
#	jeecg-boot/jeecg-server-cloud/jeecg-cloud-gateway/src/main/java/org/jeecg/loader/DynamicRouteLoader.java
#	jeecg-boot/jeecg-server-cloud/jeecg-cloud-gateway/src/main/resources/application.yml
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-more/src/main/java/org/jeecg/modules/test/feign/controller/JeecgTestFeignController.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-rocketmq/src/main/java/org/jeecg/modules/test/rocketmq/controller/JeecgMqTestController.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/jeecg-cloud-test-seata-order/src/main/java/org/jeecg/modules/test/seata/order/controller/SeataOrderController.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java
#	jeecg-boot/pom.xml
This commit is contained in:
JEECG
2025-05-15 20:01:54 +08:00
639 changed files with 25705 additions and 10187 deletions

View File

@ -25,3 +25,6 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
#VITE_GLOB_QIANKUN_MICRO_APP_NAME=jeecg-vue3
# 作为乾坤子应用启动时必填需与qiankun主应用注册子应用时填写的 entry 保持一致
#VITE_GLOB_QIANKUN_MICRO_APP_ENTRY=//localhost:3001/jeecg-vue3
# 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开
#VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs

View File

@ -26,3 +26,6 @@ VITE_GLOB_API_URL_PREFIX=
#VITE_GLOB_QIANKUN_MICRO_APP_NAME=jeecg-vue3
# 作为乾坤子应用启动时必填需与qiankun主应用注册子应用时填写的 entry 保持一致
#VITE_GLOB_QIANKUN_MICRO_APP_ENTRY=//qiankun.boot3.jeecg.com/jeecg-vue3
# 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开
#VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs

View File

@ -1,10 +1,10 @@
JeecgBoot 企业级低代码开发平台
===============
当前最新版本: 3.7.3发布时间2025-02-10
当前最新版本: 3.8.0(预计发布时间2025-04-21
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com
/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -19,14 +19,14 @@ JeecgBoot-Vue3采用 Vue3.0、Vite、 Ant-Design-Vue4、TypeScript 等新技术
## 开发环境搭建
- [前端开发环境准备](https://help.jeecg.com/setup/dev.html)
- [前端项目快速启动](https://help.jeecg.com/setup/startup.html)
- [通过IDEA启动项目](https://help.jeecg.com/java/setup/idea/startup.html)
- [前端开发环境准备](https://help.jeecg.com/setup/dev)
- [前端项目快速启动](https://help.jeecg.com/setup/startup)
- [通过IDEA启动项目](https://help.jeecg.com/java/setup/idea/startup)
## 技术文档
- 官方文档:[https://help.jeecg.com](https://help.jeecg.com)
- 快速入门:[快速入门](http://jeecg.com/doc/quickstart) | [常见问题](http://help.jeecg.com/qa.html)
- 快速入门:[快速入门](http://jeecg.com/doc/quickstart) | [常见问题](http://help.jeecg.com/qa)
- QQ交流群⑩716488839、⑨808791225、其他满
- 在线演示 [系统演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取

View File

@ -1,6 +1,6 @@
{
"name": "jeecgboot-vue3",
"version": "3.7.3",
"version": "3.8.0",
"author": {
"name": "北京国炬信息技术有限公司",
"email": "jeecgos@163.com",
@ -21,7 +21,11 @@
"husky:install": "husky install"
},
"dependencies": {
"@jeecg/online": "3.7.1-RC",
"@jeecg/online": "3.7.4-beta",
"@jeecg/aiflow": "1.0.0",
"@logicflow/core": "^2.0.10",
"@logicflow/extension": "^2.0.14",
"@logicflow/vue-node-registry": "^1.0.12",
"@iconify/iconify": "^3.1.1",
"@ant-design/colors": "^7.2.0",
"@ant-design/icons-vue": "^7.0.1",
@ -57,11 +61,12 @@
"path-to-regexp": "^6.3.0",
"pinia": "2.1.7",
"print-js": "^1.6.0",
"qrcode": "^1.5.4",
"qs": "^6.13.1",
"qrcode": "^1.5.4",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^2.1.0",
"sortablejs": "^1.15.6",
"swagger-ui-dist": "^5.21.0",
"tinymce": "6.6.2",
"vditor": "^3.10.8",
"vue": "^3.5.13",

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,11 @@
} else {
Object.assign(data.token, { colorTextBase: '#333' });
}
// 定义主题色 css 变量
if (data.token.colorPrimary) {
document.documentElement.style.setProperty('--j-global-primary-color', data.token.colorPrimary);
}
}
};
// update-begin--author:liaozhiyang---date:20231218---for【QQYUN-6366】升级到antd4.x

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -89,7 +89,6 @@
opt.getContainer = `.${prefixVar}-layout-content` as any;
}
}
console.log('getProps:opt',opt);
return opt as DrawerProps;
});

View File

@ -35,7 +35,8 @@ export const basicProps = {
loading: { type: Boolean },
maskClosable: { type: Boolean, default: true },
getContainer: {
type: [Object, String] as PropType<any>,
type: [Object, String, Function, Boolean] as PropType<any>,
default: () => 'body',
},
closeFunc: {
type: [Function, Object] as PropType<any>,

View File

@ -27,6 +27,7 @@ export { default as JDictSelectTag } from './src/jeecg/components/JDictSelectTag
export { default as JTreeSelect } from './src/jeecg/components/JTreeSelect.vue';
export { default as JSearchSelect } from './src/jeecg/components/JSearchSelect.vue';
export { default as JSelectUserByDept } from './src/jeecg/components/JSelectUserByDept.vue';
export { default as JSelectUserByDepartment } from './src/jeecg/components/JSelectUserByDepartment.vue';
export { default as JEditor } from './src/jeecg/components/JEditor.vue';
export { default as JImageUpload } from './src/jeecg/components/JImageUpload.vue';
// Jeecg自定义校验

View File

@ -64,6 +64,7 @@ import JInput from './jeecg/components/JInput.vue';
import JTreeSelect from './jeecg/components/JTreeSelect.vue';
import JEllipsis from './jeecg/components/JEllipsis.vue';
import JSelectUserByDept from './jeecg/components/JSelectUserByDept.vue';
import JSelectUserByDepartment from './jeecg/components/JSelectUserByDepartment.vue';
import JUpload from './jeecg/components/JUpload/JUpload.vue';
import JSearchSelect from './jeecg/components/JSearchSelect.vue';
import JAddInput from './jeecg/components/JAddInput.vue';
@ -159,6 +160,7 @@ componentMap.set('JInput', JInput);
componentMap.set('JTreeSelect', JTreeSelect);
componentMap.set('JEllipsis', JEllipsis);
componentMap.set('JSelectUserByDept', JSelectUserByDept);
componentMap.set('JSelectUserByDepartment', JSelectUserByDepartment);
componentMap.set('JUpload', JUpload);
componentMap.set('JSearchSelect', JSearchSelect);
componentMap.set('JAddInput', JAddInput);

View File

@ -1,5 +1,12 @@
<template>
<Select @dropdownVisibleChange="handleFetch" v-bind="attrs_" @change="handleChange" :options="getOptions" v-model:value="state">
<Select
v-bind="attrs_"
v-model:value="state"
:options="getOptions"
@change="handleChange"
@dropdownVisibleChange="handleFetch"
@popupScroll="handlePopupScroll"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
@ -24,9 +31,10 @@
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
import { isNumber } from '/@/utils/is';
type OptionsItem = { label: string; value: string; disabled?: boolean };
//文档 https://help.jeecg.com/ui/apiSelect#pageconfig%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE
export default defineComponent({
name: 'ApiSelect',
components: {
@ -35,7 +43,7 @@
},
inheritAttrs: false,
props: {
value: [Array, Object, String, Number],
value: [Array, String, Number],
numberToString: propTypes.bool,
api: {
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>,
@ -46,6 +54,11 @@
type: Object as PropType<Recordable>,
default: () => ({}),
},
//分页配置
pageConfig: {
type: Object as PropType<Recordable>,
default: () => ({ isPage: false }),
},
// support xxx.xxx.xx
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
@ -60,7 +73,15 @@
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// update-begin--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
const hasMore = ref(true);
const pagination = ref({
pageNo: 1,
pageSize: 10,
total: 0,
});
const defPageConfig = { isPage: false, pageField: 'pageNo', pageSizeField: 'pageSize', totalField: 'total', listField: 'records' };
// update-end--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
// Embedded in the form, just use the hook binding to perform form verification
const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
// update-begin--author:liaozhiyang---date:20230830---for【QQYUN-6308】解决警告
@ -114,7 +135,7 @@
},
{ deep: true }
);
//监听数值修改,查询数据
//监听数值修改,查询数据
watchEffect(() => {
props.value && handleFetch();
});
@ -122,17 +143,33 @@
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
// update-begin--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
if (!props.pageConfig.isPage || pagination.value.pageNo == 1) {
options.value = [];
}
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
let { isPage, pageField, pageSizeField, totalField, listField } = { ...defPageConfig, ...props.pageConfig };
let params = isPage
? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize }
: { ...props.params };
// update-end--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
const res = await api(params);
if (isPage) {
// update-begin--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
options.value = [...options.value, ...res[listField]];
pagination.value.total = res[totalField] || 0;
hasMore.value = res[totalField] ? options.value.length < res[totalField] : res[listField] < pagination.value.pageSize;
// update-end--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
} else {
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
}
emitChange();
} catch (error) {
@ -151,9 +188,17 @@
function initValue() {
let value = props.value;
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
// update-begin--author:liaozhiyang---date:20250407---for【issues/8037】初始化值单选的值被错误地写入数组值
if (unref(attrs).mode == 'multiple') {
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
} else if (isNumber(value)) {
state.value = [value];
}
} else {
state.value = value;
}
// update-end--author:liaozhiyang---date:20250407---for【issues/8037】初始化值单选的值被错误地写入数组值
}
async function handleFetch() {
@ -171,8 +216,18 @@
vModalValue && vModalValue(_);
emitData.value = args;
}
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange };
// update-begin--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
// 滚动加载更多
function handlePopupScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isNearBottom = scrollHeight - scrollTop <= clientHeight + 20;
if (props.pageConfig.isPage && isNearBottom && hasMore.value && !loading.value) {
pagination.value.pageNo += 1;
fetch();
}
}
// update-end--author:liusq---date:20250407---for【QQYUN-11831】ApiSelect 分页下拉方案 #7883
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange, handlePopupScroll };
},
});
</script>

View File

@ -6,6 +6,7 @@
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
showCheckedStrategy="SHOW_ALL"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"

View File

@ -2,7 +2,7 @@
<template>
<div class="JPopup components-input-demo-presuffix" v-if="avalid">
<!--输入框-->
<a-input @click="handleOpen" v-model:value="showText" :placeholder="placeholder" readOnly v-bind="attrs">
<a-input @click="handleOpen" :value="innerShowText || showText" :placeholder="placeholder" readOnly v-bind="attrs">
<template #prefix>
<Icon icon="ant-design:cluster-outlined"></Icon>
</template>
@ -64,6 +64,8 @@
default: () => [],
},
showAdvancedButton: propTypes.bool.def(true),
// 是否是在 筛选search 中使用
inSearch: propTypes.bool.def(false),
},
emits: ['update:value', 'register', 'popUpChange', 'focus'],
setup(props, { emit, refs }) {
@ -72,6 +74,7 @@
//pop是否展示
const avalid = ref(true);
const showText = ref('');
const innerShowText = ref('')
//注册model
const [regModal, { openModal }] = useModal();
//表单值
@ -124,6 +127,7 @@
let { fieldConfig } = props;
//匹配popup设置的回调值
let values = {};
let labels = []
for (let item of fieldConfig) {
let val = rows.map((row) => row[item.source]);
// update-begin--author:liaozhiyang---date:20230831---for【QQYUN-7535】数组只有一个且是number类型join会改变值的类型为string
@ -132,7 +136,20 @@
item.target.split(',').forEach((target) => {
values[target] = val;
});
if (props.inSearch) {
// 处理显示值
if (item.label) {
let txt = rows.map((row) => row[item.label]);
txt = txt.length == 1 ? txt[0] : txt.join(',');
labels.push(txt);
} else {
labels.push(val);
}
}
}
innerShowText.value = labels.join(',');
//传入表单示例方式赋值
props.formElRef && props.formElRef.setFieldsValue(values);
//传入赋值方法方式赋值
@ -146,6 +163,7 @@
return {
showText,
innerShowText,
avalid,
uniqGroupId,
attrs,

View File

@ -12,6 +12,8 @@
@register="regModal"
:code="code"
:multi="multi"
:selected="selected"
:rowkey="valueFiled"
:sorter="sorter"
:groupId="''"
:param="param"
@ -72,6 +74,7 @@
const code = props.dictCode.split(',')[0];
const labelFiled = props.dictCode.split(',')[1];
const valueFiled = props.dictCode.split(',')[2];
const selected = ref([]);
if (!code || !valueFiled || !labelFiled) {
createMessage.error('popupDict参数未正确配置!');
}
@ -159,6 +162,7 @@
options.value = data.records.map((item) => {
return { value: item[valueFiled], text: item[labelFiled] };
});
selected.value = data.records;
}
})
.finally(() => {
@ -200,6 +204,8 @@
code,
options,
loading,
selected,
valueFiled,
};
},
});

View File

@ -1,7 +1,7 @@
<!--职务选择组件-->
<template>
<div class="JSelectPosition">
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="(changeValue) => $emit('update:value', changeValue)"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<PositionSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></PositionSelectModal>
@ -70,9 +70,15 @@
/**
* 监听组件值
*/
watchEffect(() => {
props.value && initValue();
});
// update-begin--author:liaozhiyang---date:20250423---for【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态需要点击第二次才生效。
watch(
() => props.value,
() => {
props.value && initValue();
},
{ deep: true, immediate: true }
);
// update-end--author:liaozhiyang---date:20250423---for【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态需要点击第二次才生效。
/**
* 监听selectValues变化

View File

@ -41,7 +41,7 @@
default: () => {},
},
},
emits: ['options-change', 'change'],
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
@ -118,6 +118,9 @@
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
// update-begin--author:liaozhiyang---date:20250318---for【issues/7948】修复JselectRole组件不支持双向绑定
emit('update:value', values);
// update-end--author:liaozhiyang---date:20250318---for【issues/7948】修复JselectRole组件不支持双向绑定
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {

View File

@ -0,0 +1,176 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="handleSelectChange"></JSelectBiz>
<JSelectUserByDepartmentModal
v-if="modalShow"
:selectedUser="selectOptions"
:modalTitle="modalTitle"
:rowKey="rowKey"
:labelKey="labelKey"
:isRadioSelection="isRadioSelection"
:params="params"
@register="regModal"
@change="setValue"
@close="() => (modalShow = false)"
v-bind="attrs"
></JSelectUserByDepartmentModal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, provide } from 'vue';
import JSelectUserByDepartmentModal from './modal/JSelectUserByDepartmentModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { isArray, isString, isObject } from '/@/utils/is';
import { getTableList as getTableListOrigin } from '/@/api/common/api';
import { useMessage } from '/@/hooks/web/useMessage';
defineOptions({ name: 'JSelectUserByDepartment' });
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
modalTitle: {
type: String,
default: '部门用户选择',
},
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
//查询参数
params: {
type: Object,
default: () => {},
},
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['options-change', 'change', 'update:value']);
const { createMessage } = useMessage();
//注册model
const [regModal, { openModal }] = useModal();
// 是否显示弹窗
const modalShow = ref(false);
//下拉框选项值
const selectOptions: any = ref<SelectValue>([]);
//下拉框选中值
let selectValues: any = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const attrs: any = useAttrs();
// 打开弹窗
function handleOpen() {
modalShow.value = true;
setTimeout(() => {
openModal(true, {
isUpdate: false,
});
}, 0);
}
const handleSelectChange = (data) => {
selectOptions.value = selectOptions.value.filter((item) => data.includes(item[props.rowKey]));
setValue(selectOptions.value);
};
// 设置下拉框的值
const setValue = (data) => {
selectOptions.value = data.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
selectValues.value = data.map((item) => item[props.rowKey]);
// 更新value
emit('update:value', selectValues.value);
// 触发change事件不转是因为basicForm提交时会自动将字符串转化为数组
emit('change', selectValues.value);
// 触发options-change事件
emit('options-change', selectOptions.value);
};
// 翻译
const transform = () => {
let value = props.value;
let len;
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length;
value = value.join(',');
} else {
len = value.split(',').length;
}
value = value.trim();
if (value) {
// 如果value的值在selectedUser中存在则不请求翻译
let isNotRequestTransform = false;
isNotRequestTransform = value.split(',').every((value) => !!selectOptions.value.find((item) => item[props.rowKey] === value));
if (isNotRequestTransform) {
selectValues.value = value.split(',')
return;
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value };
if (isObject(attrs.params)) {
Object.assign(params, attrs.params);
}
getTableListOrigin(params).then((result: any) => {
const records = result.records ?? [];
selectValues.value = records.map((item) => item[props.rowKey]);
selectOptions.value = records.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
});
}
} else {
selectValues.value = [];
}
};
// 监听value变化
watch(
() => props.value,
() => {
transform();
},
{ deep: true, immediate: true }
);
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>

View File

@ -32,7 +32,7 @@
import { useAttrs } from '/@/hooks/core/useAttrs';
import { TreeSelect } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { isObject } from '/@/utils/is';
import { isObject, isArray } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
enum Api {
url = '/sys/dict/loadTreeData',
@ -143,6 +143,23 @@
if(props.url){
getItemFromTreeData();
}else{
// update-begin--author:liaozhiyang---date:20250423---for【issues/8093】选择节点后会先变成编码再显示label文字
if (props.value) {
if (isArray(treeValue.value)) {
let isNotRequestTransform = false;
const value = isArray(props.value) ? props.value : props.value.split(',');
isNotRequestTransform = value.every((value) => !!treeValue.value.find((item) => item.value === value));
if (isNotRequestTransform) {
return;
}
} else if (isObject(treeValue.value) && unref(treeValue).label != null) {
if (props.value == unref(treeValue).value) {
// 不需要再去请求翻译
return;
}
}
}
// update-end--author:liaozhiyang---date:20250423---for【issues/8093】选择节点后会先变成编码再显示label文字
let params = { key: props.value };
let result = await defHttp.get({ url: `${Api.view}${props.dict}`, params }, { isTransformResponse: false });
if (result.success) {
@ -291,7 +308,22 @@
} else {
emitValue(value.value);
}
treeValue.value = value;
// update-begin--author:liaozhiyang---date:20250423---for【issues/8093】删除后会先变成编码再显示label文字
if (isArray(value)) {
// 编辑删除时有选中的值是异步第二级以上的会不显示label
value.forEach((item) => {
if (item.label === undefined && item.value != null) {
const findItem = treeValue.value.find((o) => o.value === item.value);
if (findItem) {
item.label = findItem.label;
}
}
});
treeValue.value = value;
} else {
treeValue.value = value;
}
// update-end--author:liaozhiyang---date:20250423---for【issues/8093】删除后会先变成编码再显示label文字
}
function emitValue(value) {

View File

@ -38,6 +38,7 @@
import { UploadTypeEnum } from './upload.data';
import { getFileAccessHttpUrl, getHeaders } from '/@/utils/common/compUtils';
import UploadItemActions from './components/UploadItemActions.vue';
import { split } from '/@/utils/index';
const { createMessage, createConfirm } = useMessage();
const { prefixCls } = useDesign('j-upload');
@ -201,7 +202,10 @@
return;
}
let list: any[] = [];
for (const item of paths.split(',')) {
// update-begin--author:liaozhiyang---date:20250325---for【issues/7990】图片参数中包含逗号会错误的识别成多张图
const result = split(paths);
// update-end--author:liaozhiyang---date:20250325---for【issues/7990】图片参数中包含逗号会错误的识别成多张图
for (const item of result) {
let url = getFileAccessHttpUrl(item);
list.push({
uid: uidGenerator(),

View File

@ -49,7 +49,7 @@
:canResize="false"
:bordered="true"
:loading="loading"
:rowKey="combineRowKey"
:rowKey="rowkey ? rowkey : combineRowKey"
:columns="columns"
:showIndexColumn="false"
:dataSource="dataSource"
@ -90,7 +90,7 @@
loading: true,
}),
},
props: ['multi', 'code', 'sorter', 'groupId', 'param','showAdvancedButton', 'getFormValues'],
props: ['multi', 'code', 'sorter', 'groupId', 'param','showAdvancedButton', 'getFormValues', 'selected', 'rowkey'],
emits: ['ok', 'register'],
setup(props, { emit }) {
const { createMessage } = useMessage();
@ -284,7 +284,15 @@
createImgPreview({ imageList: imgList });
}
}
// update-begin--author:liaozhiyang---date:20250415--for【issues/3656】popupdict回显
watchEffect(() => {
if (props.selected && props.rowkey) {
const selected = props.multi ? props.selected : [props.selected];
checkedKeys!.value = selected.map((item) => item[props.rowkey]);
selectRows!.value = selected;
}
});
// update-end--author:liaozhiyang---date:20250415--for【issues/3656】popupdict回显
return {
attrs,
register,

View File

@ -0,0 +1,833 @@
<template>
<BasicModal
wrapClassName="JSelectUserByDepartmentModal"
v-bind="$attrs"
@register="register"
:title="modalTitle"
width="800px"
@ok="handleOk"
destroyOnClose
@visible-change="visibleChange"
>
<div class="j-select-user-by-dept">
<div class="modal-content">
<!-- 左侧搜索和组织列表 -->
<div class="left-content">
<!-- 搜索框 -->
<div class="search-box">
<a-input v-model:value.trim="searchText" placeholder="搜索" @change="handleSearch" @pressEnter="handleSearch" allowClear />
</div>
<!-- 组织架构 -->
<div class="tree-box">
<template v-if="searchText.length">
<template v-if="searchResult.depart.length || searchResult.user.length">
<div class="search-result">
<template v-if="searchResult.user.length">
<div class="search-user">
<p class="search-user-title">人员</p>
<template v-for="item in searchResult.user" :key="item.id">
<div class="search-user-item" @click="handleSearchUserCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="search-user-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="search-user-item-info">
<div class="search-user-item-name">{{ item.realname }}</div>
<div class="search-user-item-org">{{ item.orgCodeTxt }}</div>
</div>
</div>
</div>
</template>
</div>
</template>
<template v-if="searchResult.depart.length">
<div class="search-depart">
<p class="search-depart-title">部门</p>
<template v-for="item in searchResult.depart" :key="item.id">
<div class="search-depart-item" @click="handleSearchDepartClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleSearchDepartCheck($event, item)" />
<div class="search-depart-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
</template>
</div>
</template>
<template v-else>
<div class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</template>
<template v-else>
<a-breadcrumb v-if="breadcrumb.length">
<a-breadcrumb-item @click="handleBreadcrumbClick()">
<HomeOutlined />
</a-breadcrumb-item>
<template v-for="item in breadcrumb" :key="item?.id">
<a-breadcrumb-item @click="handleBreadcrumbClick(item)">
<span>{{ item.departName }}</span>
</a-breadcrumb-item>
</template>
</a-breadcrumb>
<div v-if="currentDepartUsers.length">
<!-- 当前部门用户树 -->
<div class="depart-users-tree">
<div v-if="!currentDepartTree.length" class="allChecked">
<a-checkbox v-model:checked="currentDepartAllUsers" @change="handleAllUsers">全选</a-checkbox>
</div>
<template v-for="item in currentDepartUsers" :key="item.id">
<div class="depart-users-tree-item" @click="handleDepartUsersTreeCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="depart-users-tree-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="depart-users-tree-item-name">{{ item.realname }}</div>
</div>
</div>
</template>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
<template v-for="item in currentDepartTree" :key="item.id">
<div class="depart-tree-item" @click="handleDepartTreeClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleDepartTreeCheck($event, item)" />
<div class="depart-tree-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
<div v-if="currentDepartTree.length === 0 && currentDepartUsers.length === 0" class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</div>
</div>
<!-- 右侧已选人员展示 -->
<div class="right-content">
<div class="selected-header"> 已选人员{{ selectedUsers.length }} </div>
<div class="selected-users">
<div class="content">
<div v-for="user in selectedUsers" :key="user.id" class="user-avatar" @click="handleDelUser(user)">
<div class="avatar-circle">
<img v-if="user.avatar" :src="getFileAccessHttpUrl(user.avatar)" alt="avatar" />
<div class="mask">
<CloseOutlined></CloseOutlined>
</div>
</div>
<div class="user-name">{{ user.realname }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { RightOutlined, HomeOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryTreeList, getTableList as getTableListOrigin } from '/@/api/common/api';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { isArray } from '/@/utils/is';
import { defHttp } from '/@/utils/http/axios';
defineOptions({ name: 'JSelectUserByDepartmentModal' });
const props = defineProps({
// ...selectProps,
//回传value字段名
rowKey: {
type: String,
default: 'id',
},
//回传文本字段名
labelKey: {
type: String,
default: 'name',
},
modalTitle: {
type: String,
default: '部门用户选择',
},
selectedUser: {
type: Array,
default: () => [],
},
//查询参数
params: {
type: Object,
default: () => {},
},
//最大选择数量
maxSelectCount: {
type: Number,
default: 0,
},
// 是否单选
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['close', 'register', 'change']);
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 搜索文本
const searchText = ref('');
const breadcrumb = ref<any[]>([]);
// 部门树(整颗树)
const departTree = ref([]);
// 当前部门树
const currentDepartTree = ref<any[]>([]);
// 选中的部门节点
const checkedDepartIds = ref<string[]>([]);
// 当前部门用户
const currentDepartUsers = ref([]);
// 已选用户
const selectedUsers = ref<any[]>([]);
// 全选
const currentDepartAllUsers = ref(false);
// 搜索结构
const searchResult: any = reactive({
depart: [],
user: [],
});
// 映射部门和人员的关系
const cacheDepartUser = {};
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
// 初始化
if (props.selectedUser.length) {
// 编辑时,传进来已选中的数据
selectedUsers.value = props.selectedUser;
}
getQueryTreeList();
});
const visibleChange = (visible) => {
if (visible === false) {
setTimeout(() => {
emit('close');
}, 300);
}
};
const handleOk = () => {
if (selectedUsers.value.length == 0) {
createMessage.warning('请选择人员');
return;
}
if (props.isRadioSelection && selectedUsers.value.length > 1) {
createMessage.warning('只允许选择一个用户');
return;
}
if (props.maxSelectCount && selectedUsers.value.length > props.maxSelectCount) {
createMessage.warning(`最多只能选择${props.maxSelectCount}个用户`);
return;
}
emit('change', selectedUsers.value);
closeModal();
};
// 搜索人员/部门
const handleSearch = () => {
if (searchText.value) {
defHttp
.get({
url: `/sys/user/listAll`,
params: {
column: 'createTime',
order: 'desc',
pageNo: 1,
pageSize: 100,
realname: `*${searchText.value}*`,
},
})
.then((result: any) => {
result.records?.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
}
});
searchResult.user = result.records ?? [];
});
searchResult.depart = getDepartByName(searchText.value) ?? [];
} else {
searchResult.user = [];
searchResult.depart = [];
}
};
// 面包屑
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = [];
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id);
if (findIndex != -1) {
breadcrumb.value = breadcrumb.value.filter((item, index) => {
console.log(item);
return index <= findIndex;
});
}
const data = getDepartTreeNodeById(item.id, departTree.value);
currentDepartTree.value = data.children;
} else {
// 根节点
currentDepartTree.value = departTree.value;
breadcrumb.value = [];
}
};
// 点击部门树复选框触发
const handleDepartTreeCheck = (e, item) => {
const { target } = e;
if (target.checked) {
// 选中
getUsersByDeptId(item['id']).then((users) => {
addUsers(users);
});
checkedDepartIds.value.push((item as any).id);
// 检查父节点下所有子节点是否选中
const parentItem = getDepartTreeParentById(item.id);
if (parentItem?.children) {
const isChildAllChecked = parentItem.children.every((item) => item.checked);
if (isChildAllChecked) {
parentItem.checked = true;
} else {
parentItem.checked = false;
}
}
} else {
// 取消选中
const findIndex = checkedDepartIds.value.findIndex((o: any) => o.id === item.id);
if (findIndex != -1) {
checkedDepartIds.value.splice(findIndex, 1);
}
// 如果父节点是选中,则需要取消
const parentItem = getDepartTreeParentById(item.id);
if (parentItem) {
parentItem.checked = false;
}
getUsersByDeptId(item['id']).then((users) => {
users.forEach((item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
});
});
}
};
// 点击部门树节点触发
const handleDepartTreeClick = (item) => {
breadcrumb.value = [...breadcrumb.value, item];
if (item.children) {
// 有子节点,则显示部门
if (item.checked) {
// 父节点勾选,则子节点全部勾选
item.children.forEach((item) => {
item.checked = true;
});
}
currentDepartTree.value = item.children;
defHttp
.get({
url: '/sys/sysDepart/getUsersByDepartId',
params: {
id: item['id'],
},
})
.then((res: any) => {
const result = res ?? [];
if (item.checked) {
// 父节点勾选,则默认勾选
result.forEach((item) => {
item.checked = true;
});
}
// 右侧勾选了,则默认勾选(用户存在多部门,在别的部门被选中了)
if (selectedUsers.value.length) {
result.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id === item.id);
if (findItem) {
// 说明在selectedUsers中被找到
item.checked = true;
}
});
}
currentDepartUsers.value = result;
});
} else {
// 没有子节点,则显示用户
currentDepartTree.value = [];
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res?.records) {
let checked = true;
res.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
checked = false;
}
});
currentDepartAllUsers.value = checked;
currentDepartUsers.value = res.records;
}
});
}
};
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
if (item.checked == false) {
// 有一个是false则全选false
currentDepartAllUsers.value = false;
}
};
// 全选
const handleAllUsers = ({ target }) => {
const { checked } = target;
if (checked) {
currentDepartUsers.value.forEach((item: any) => (item.checked = true));
addUsers(currentDepartUsers.value);
} else {
currentDepartUsers.value.forEach((item: any) => (item.checked = false));
selectedUsers.value = selectedUsers.value.filter((user) => {
const userId = user.id;
const findItem = currentDepartUsers.value.find((item: any) => item.id === userId);
if (findItem) {
return false;
} else {
return true;
}
});
}
};
// 删除人员
const handleDelUser = (item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
const findItem: any = currentDepartUsers.value.find((user: any) => user.id === item.id);
if (findItem) {
findItem.checked = false;
currentDepartAllUsers.value = false;
}
};
// 点击搜索用户复选框
const handleSearchUserCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
};
// 点击搜索部门复选框
const handleSearchDepartCheck = (e, item) => {
handleDepartTreeCheck(e, item);
};
// 点击搜索部门
const handleSearchDepartClick = (item) => {
searchResult.depart = [];
searchResult.user = [];
breadcrumb.value = getPathToNodeById(item.id);
handleDepartTreeClick(item);
};
// 添加人员到右侧
const addUsers = (users) => {
let newUsers: any = [];
if (isArray(users)) {
// selectedUsers里面没有才添加防止重复
newUsers = users.filter((user: any) => !selectedUsers.value.find((item) => item.id === user.id));
} else {
if (!selectedUsers.value.find((user) => user.id === users.id)) {
// selectedUsers里面没有才添加防止重复
newUsers = [users];
}
}
selectedUsers.value = [...selectedUsers.value, ...newUsers];
const result = currentDepartUsers.value.every((item: any) => !!item.checked);
currentDepartAllUsers.value = result;
};
// 解析参数
const parseParams = (params) => {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
};
const getQueryTreeList = (params?) => {
params = parseParams(params);
queryTreeList({ ...params }).then((res) => {
if (res) {
departTree.value = res;
currentDepartTree.value = res;
}
});
};
// 根据部门id获取用户
const getTableList = (params) => {
params = parseParams(params);
return getTableListOrigin({ ...params });
};
const getUsersByDeptId = (id) => {
return new Promise<any[]>((resolve) => {
if (cacheDepartUser[id]) {
resolve(cacheDepartUser[id]);
} else {
getTableList({
departId: id,
}).then((res: any) => {
cacheDepartUser[id] = res.records ?? [];
if (res?.records?.length) {
resolve(res.records ?? []);
}
});
}
});
};
// 根据id获取根节点到当前节点路径
const getPathToNodeById = (id: string, tree = departTree.value, path = []): any[] => {
for (const node of tree) {
if ((node as any).id === id) {
return [...path];
}
if ((node as any).children) {
const foundPath = getPathToNodeById(id, (node as any).children, [...path, node]);
if (foundPath.length) {
return foundPath;
}
}
}
return [];
};
// 根据id获取部门树父节点数据
const getDepartTreeParentById = (id: string, tree = departTree.value, parent = null): any => {
for (const node of tree) {
if ((node as any).id === id) {
return parent;
}
if ((node as any).children) {
const found = getDepartTreeParentById(id, (node as any).children, node);
if (found) {
return found;
}
}
}
return null;
};
// 通过名称搜索部门支持模糊
const getDepartByName = (name: string, tree = departTree.value): any[] => {
const result: any[] = [];
const search = (nodes: any[]) => {
for (const node of nodes) {
if (node.departName?.toLowerCase().includes(name.toLowerCase())) {
result.push(node);
}
if (node.children?.length) {
search(node.children);
}
}
};
search(tree);
return result;
};
// 根据id获取部门树当前节点数据
const getDepartTreeNodeById = (id: string, tree = departTree.value): any => {
for (const node of tree) {
if ((node as any).id === id) {
return node;
}
if ((node as any).children) {
const found = getDepartTreeNodeById(id, (node as any).children);
if (found) {
return found;
}
}
}
return null;
};
</script>
<style lang="less">
.JSelectUserByDepartmentModal {
.scroll-container {
padding: 0;
}
}
</style>
<style lang="less" scoped>
.j-select-user-by-dept {
background: #fff;
border-radius: 4px;
}
.modal-content {
display: flex;
padding: 20px;
padding-bottom: 0;
padding-left: 0;
height: 400px;
font-size: 12px;
}
.left-content {
display: flex;
flex-direction: column;
flex: 1;
border-right: 1px solid #e8e8e8;
.search-box {
margin: 0 16px 16px 16px;
}
:deep(.ant-breadcrumb) {
font-size: 12px;
margin-left: 16px;
color: inherit;
cursor: pointer;
li {
.ant-breadcrumb-link {
cursor: pointer;
&:hover {
color: @primary-color;
}
}
&:last-child {
.ant-breadcrumb-link {
pointer-events: none;
}
}
}
}
.tree-box {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
.no-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.depart-tree {
.depart-tree-item {
padding: 0 16px;
line-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
}
.depart-tree-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 8px;
}
}
}
.depart-users-tree {
.allChecked {
padding: 0 16px;
margin-bottom: 16px;
padding-top: 12px;
:deep(.ant-checkbox-wrapper) {
font-size: 12px;
}
}
.depart-users-tree-item {
line-height: 50px;
padding: 0 16px;
display: flex;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.depart-users-tree-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #aaa;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
}
}
.depart-users-tree-item-name {
margin-left: 8px;
}
}
}
.search-depart {
margin-bottom: 8px;
.search-depart-title {
padding-left: 16px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.search-depart-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.search-depart-item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.search-user {
margin-bottom: 8px;
.search-user-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
padding-left: 16px;
}
.search-user-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.search-user-item-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
}
.search-user-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background-color: #aaa;
}
.search-user-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-user-item-org {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.right-content {
width: 400px;
display: flex;
flex-direction: column;
padding-left: 16px;
.selected-header {
margin-bottom: 16px;
}
.selected-users {
flex: 1;
overflow-y: auto;
}
.content {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.user-avatar {
text-align: center;
width: 70px;
cursor: pointer;
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: 0 auto 8px;
position: relative;
background-color: rgba(0, 0, 0, 0.5);
img {
width: 100%;
height: 100%;
}
&:hover {
.mask {
opacity: 1;
}
}
.mask {
opacity: 0;
transition: opacity;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
}
.user-name {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
</style>

View File

@ -3,7 +3,7 @@
<div @click="showModal" :class="disabled ? 'select-input disabled-select' : 'select-input'">
<template v-if="selectedList.length > 0">
<template v-for="(item, index) in selectedList">
<SelectedUserItem v-if="index < maxCount" :info="item" @unSelect="unSelect" query />
<SelectedUserItem v-if="index < maxSelectCount" :info="item" @unSelect="unSelect" query />
</template>
</template>
<span v-else style="height: 30px; line-height: 30px; display: inline-block; margin-left: 7px; color: #bfbfbf">请选择</span>
@ -26,7 +26,7 @@
import { Form } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user';
const maxCount = 2;
const maxCount = 3;
export default defineComponent({
name: 'RoleSelectInput',
@ -39,6 +39,12 @@
type: Boolean,
default: false,
},
// update-begin--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
maxSelectCount: {
type: Number,
default: 2,
},
// update-end--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
store: {
type: String,
default: 'id',
@ -77,7 +83,7 @@
}
const ellipsisInfo = computed(() => {
let max = maxCount;
let max = props.maxSelectCount;
let len = selectedList.value.length;
if (len > max) {
return { status: true, count: len - max };
@ -189,6 +195,12 @@
selectType: 'sys_role',
});
}
// 根据 ids 的顺序对 arr 进行排序
if (props.store === 'code') {
arr.sort((a, b) => ids.indexOf(a.code) - ids.indexOf(b.code));
} else {
arr.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
}
selectedList.value = arr;
} else {
@ -199,7 +211,6 @@
return {
selectedList,
ellipsisInfo,
maxCount,
registerRoleModal,
closeRoleModal,
showModal,

View File

@ -132,13 +132,23 @@
}
return list.filter(item=>item.name.indexOf(text)>=0)
});
const selectedKeys = ref<string[]>([]);
const selectedList = computed(()=>{
let list = dataList.value;
if(!list || list.length ==0 ){
return []
}
return list.filter(item=>item.checked)
list = list.filter(item=>item.checked)
// 根据 selectedKeys 的顺序排序
let arr: any[] = [];
for (let key of selectedKeys.value) {
let item = list.find(item => item.id == key);
if (item) {
arr.push(item);
}
}
return arr;
});
function unSelect(id) {
@ -146,8 +156,11 @@
if(!list || list.length ==0 ){
return;
}
let arr = list.filter(item=>item.id == id);
arr[0].checked = false;
// update-begin--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
let findItem = list.find((item) => item.id == id);
findItem.checked = false;
selectedKeys.value = selectedKeys.value.filter((key) => key != id);
// update-end--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
}
async function loadDataList() {
@ -180,18 +193,30 @@
console.log('loadDataList', data);
}
function onSelect(e, item) {
prevent(e);
console.log('onselect');
// 单选判断 只能选中一条数据 其余数据置false
if(props.multi === false){
let list = dataList.value;
for(let item of list){
item.checked = false;
}
// update-begin--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
// 单选模式下,先清除所有选中状态
if (!props.multi) {
dataList.value.forEach(dataItem => {
if (dataItem.id != item.id) {
dataItem.checked = false;
}
});
// 清空已选择的keys
selectedKeys.value = [];
}
// 切换当前项的选中状态
item.checked = !item.checked;
// 更新selectedKeys数组
if (item.checked) {
selectedKeys.value.push(item.id);
} else {
selectedKeys.value = selectedKeys.value.filter(key => key !== item.id);
}
// update-end--author:liaozhiyang---date:20250414--for【issues/8078】角色选择组件点击文字部分会一直选中
}
function prevent(e) {

View File

@ -45,7 +45,9 @@ export function useSelectBiz(getList, props, emit?) {
});
}
//设置列表默认选中
checkedKeys['value'] = selectValues['value'];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
},
{ immediate: true }
);
@ -109,7 +111,9 @@ export function useSelectBiz(getList, props, emit?) {
code: selectValues['value'].join(','),
pageSize: selectValues['value'].length,
});
checkedKeys['value'] = selectValues['value'];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
selectRows['value'] = records;
}
@ -118,6 +122,9 @@ export function useSelectBiz(getList, props, emit?) {
*/
async function visibleChange(visible) {
if (visible) {
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12179】弹窗勾选了值点击取消再次打开弹窗遗留了上次的勾选的值
checkedKeys['value'] = [...selectValues['value']];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12179】弹窗勾选了值点击取消再次打开弹窗遗留了上次的勾选的值
//设置列表默认选中
props.showSelected && initSelectRows();
} else {

View File

@ -139,6 +139,7 @@ export type ComponentType =
| 'JTreeSelect'
| 'JEllipsis'
| 'JSelectUserByDept'
| 'JSelectUserByDepartment'
| 'JUpload'
| 'JSearchSelect'
| 'JAddInput'

View File

@ -185,11 +185,10 @@
}
ul span {
font-size: 1.5rem !important;
border: 1px solid #f1f1f1;
padding: 0.2rem;
margin: 0.3rem;
}
.icon-border span {
.icon-border {
border: 1px solid rgba(24, 144, 255) !important;
}
.justify-content-right {

View File

@ -20,44 +20,49 @@
/>
</template>
</a-input>
<a-modal :bodyStyle="{ padding: '24px'}" v-bind="$attrs" v-model:open="iconOpen" :keyboard="false" :width="800" @ok="handleOk" :ok-text="t('common.okText')" :cancel-text="t('common.cancelText')">
<a-tabs style="padding-left: 15px;padding-right: 15px">
<a-tab-pane tab="方向性图标" key="1">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="directionIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="指示性图标" key="2">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="suggestionIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="编辑类图标" key="3">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="editIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="数据类图标" key="4">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="dataIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="网站通用图标" key="5">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="webIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="品牌和标识" key="6">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="logoIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="其他" key="7">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-page="true" :is-search="true" :is-svg-mode="isSvgMode" :current-list="otherIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
</a-tabs>
<a-modal :bodyStyle="{ padding: '24px', paddingTop: mode === 'svg' ? '48px' : '24px'}" v-bind="$attrs" v-model:open="iconOpen" :keyboard="false" :width="800" @ok="handleOk" :ok-text="t('common.okText')" :cancel-text="t('common.cancelText')">
<template v-if="mode === 'iconify'">
<a-tabs style="padding-left: 15px;padding-right: 15px">
<a-tab-pane tab="方向性图标" key="1">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="directionIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="指示性图标" key="2">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="suggestionIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="编辑类图标" key="3">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="editIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="数据类图标" key="4">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="dataIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="网站通用图标" key="5">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="webIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="品牌和标识" key="6">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-svg-mode="isSvgMode" :current-list="logoIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
<a-tab-pane tab="其他" key="7">
<a-form-item-rest>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-page="true" :is-search="true" :is-svg-mode="isSvgMode" :current-list="otherIcons" v-model:value="selectIcon" />
</a-form-item-rest>
</a-tab-pane>
</a-tabs>
</template>
<template v-else>
<icon-list ref="iconListRef" :clear-select="clearSelect" :copy="copy" :is-page="true" :is-search="true" :is-svg-mode="isSvgMode" :current-list="otherIcons" v-model:value="selectIcon" />
</template>
</a-modal>
</template>
<script lang="ts" setup name="icon-picker">

View File

@ -30,7 +30,8 @@
:maxHeight="getProps.maxHeight"
:height="getWrapperHeight"
:visible="visibleRef"
:modalFooterHeight="footer !== undefined && !footer ? 0 : undefined"
:modalHeaderHeight="getProps.modalHeaderHeight"
:modalFooterHeight="footer !== undefined && !footer ? 0 : getProps.modalFooterHeight"
v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')"
@ext-height="handleExtHeight"
@height-change="handleHeightChange">

View File

@ -17,6 +17,9 @@ export const modalProps = {
okText: { type: String, default: t('common.okText') },
closeFunc: Function as PropType<() => Promise<boolean>>,
modalHeaderHeight: Number,
modalFooterHeight: Number,
};
export const basicProps = Object.assign({}, modalProps, {

View File

@ -199,6 +199,9 @@ export interface ModalProps {
zIndex?: number;
enableComment?: boolean;
modalHeaderHeight: number;
modalFooterHeight: number;
}
export interface ModalWrapperProps {

View File

@ -247,7 +247,11 @@
const { getHeaderProps } = useTableHeader(getProps, slots, handlers);
// update-begin--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
const getSummaryProps = computed(() => {
return pick(unref(getProps), ['summaryFunc', 'summaryData', 'hasExpandedRow', 'rowKey']);
// update-begin--author:liaozhiyang---date:20250318---for【issues/7956】修复showSummary: false时且有内嵌子表时合计栏错位
const result = pick(unref(getProps), ['summaryFunc', 'summaryData', 'hasExpandedRow', 'rowKey']);
result['hasExpandedRow'] = Object.keys(slots).includes('expandedRowRender');
// update-end--author:liaozhiyang---date:20250318---for【issues/7956】修复showSummary: false时且有内嵌子表时合计栏错位
return result;
});
const getIsEmptyData = computed(() => {
return (unref(getDataSourceRef) || []).length === 0;

View File

@ -371,9 +371,7 @@
if (props.column.dataIndex) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
// update-begin--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
props.record.editValueRefs[props.column.dataIndex] = unref(currentValueRef);
// update-end--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {

View File

@ -73,8 +73,6 @@ export type EditRecordRow<T = Recordable> = Partial<
submitCbs: Cbs[];
cancelCbs: Cbs[];
validCbs: Cbs[];
// update-begin--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
editValueRefs: Recordable;
// update-end--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@ -188,7 +188,7 @@
const sortableOrder = ref<string[]>();
const localeStore = useLocaleStoreWithOut();
// 列表字段配置缓存
const { saveSetting, resetSetting } = useColumnsCache(
const { saveSetting, resetSetting, getCache } = useColumnsCache(
{
state,
popoverVisible,
@ -204,8 +204,7 @@
watchEffect(() => {
setTimeout(() => {
const columns = table.getColumns();
if (columns.length && !state.isInit) {
if (!state.isInit) {
init();
}
}, 0);
@ -227,7 +226,13 @@
function getColumns() {
const ret: Options[] = [];
table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => {
// update-begin--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
let t = table.getColumns({ ignoreIndex: true, ignoreAction: true });
if (!t.length) {
t = table.getCacheColumns();
}
// update-end--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
t.forEach((item) => {
ret.push({
label: (item.title as string) || (item.customTitle as string),
value: (item.dataIndex || item.title) as string,
@ -237,7 +242,7 @@
return ret;
}
function init() {
async function init() {
const columns = getColumns();
const checkList = table
@ -249,11 +254,22 @@
return item.dataIndex || item.title;
})
.filter(Boolean) as string[];
// update-begin--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
const { sortedList = [] } = getCache() || {};
await nextTick();
// update-end--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
if (!plainOptions.value.length) {
plainOptions.value = columns;
plainSortOptions.value = columns;
cachePlainOptions.value = columns;
// update-begin--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
let tmp = columns;
if (sortedList?.length) {
tmp = columns.sort((prev, next) => {
return sortedList.indexOf(prev.value) - sortedList.indexOf(next.value);
});
}
// update-end--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
plainOptions.value = tmp;
plainSortOptions.value = tmp;
cachePlainOptions.value = tmp;
state.defaultCheckList = checkList;
} else {
// const fixedColumns = columns.filter((item) =>
@ -266,6 +282,13 @@
item.fixed = findItem.fixed;
}
});
// update-begin--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
if (sortedList?.length) {
plainOptions.value.sort((prev, next) => {
return sortedList.indexOf(prev.value) - sortedList.indexOf(next.value);
});
}
// update-end--author:liaozhiyang---date:20250403---for【issues/7996】表格列组件取消所有或者只勾选中间显示非预期
}
state.isInit = true;
state.checkedList = checkList;

View File

@ -144,5 +144,6 @@ export function useColumnsCache(opt, setColumns, handleColumnFixed) {
return {
saveSetting,
resetSetting,
getCache: () => $ls.get(cacheKey.value),
};
}

View File

@ -1,6 +1,6 @@
import type { ComputedRef, Ref } from 'vue';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, toRaw } from 'vue';
import { computed, unref, ref, toRaw, watch } from 'vue';
import { ROW_KEY } from '../const';
export function useTableExpand(propsRef: ComputedRef<BasicTableProps>, tableData: Ref<Recordable[]>, emit: EmitType) {
@ -28,6 +28,13 @@ export function useTableExpand(propsRef: ComputedRef<BasicTableProps>, tableData
};
});
// 监听并同步props中的expandedRowKeys
watch(() => propsRef.value?.expandedRowKeys, (keys) => {
if (Array.isArray(keys)) {
expandedRowKeys.value = keys;
}
}, {immediate: true});
function expandAll() {
const keys = getAllKeys();
expandedRowKeys.value = keys;

View File

@ -7,6 +7,8 @@ interface PaginationRenderProps {
originalElement: any;
}
type Position = 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight';
export declare class PaginationConfig extends Pagination {
position?: 'top' | 'bottom' | 'both';
}
@ -96,4 +98,11 @@ export interface PaginationProps {
* @type Function
*/
itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
// update-begin--author:liaozhiyang---date:20250423---for【pull/8013】修复 BasicTable position 属性类型配置
/**
* specify the position of Pagination
* @type Position[]
*/
position?: Position[];
// update-end--author:liaozhiyang---date:20250423---for【pull/8013】修复 BasicTable position 属性类型配置
}

View File

@ -1,9 +1,43 @@
export function checkFileType(file: File, accepts: string[]) {
const newTypes = accepts.join('|');
// const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
return reg.test(file.name);
// update-begin--author:liaozhiyang---date:20250318---for【issues/7954】BasicUpload组件上传文件限制上传格式校验出错
const mimePatterns: string[] = [];
const suffixList: string[] = [];
// 分类处理 accepts
for (const item of accepts) {
if (item.includes('/')) {
mimePatterns.push(item);
} else {
// 支持.png 或 png带点后缀或者不带点后缀
const suffix = item.startsWith('.') ? item.slice(1) : item;
suffixList.push(suffix);
}
}
// 后缀匹配逻辑
let suffixMatch = false;
if (suffixList.length > 0) {
const suffixRegex = new RegExp(`\\.(${suffixList.join('|')})$`, 'i');
suffixMatch = suffixRegex.test(file.name);
}
// MIME类型匹配逻辑
let mimeMatch = false;
if (mimePatterns.length > 0 && file.type) {
mimeMatch = mimePatterns.some((pattern) => {
// 先转义特殊字符,再处理通配符
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 先转义特殊字符
.replace(/\*/g, '.*'); // 再替换通配符
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(file.type);
});
}
if (mimePatterns.length && suffixList.length) {
return suffixMatch || mimeMatch;
} else if (mimePatterns.length) {
return mimeMatch;
} else if (suffixList.length) {
return suffixMatch;
}
// update-end--author:liaozhiyang---date:20250318---for【issues/7954】BasicUpload组件上传文件限制上传格式校验出错
}
export function checkImgType(file: File) {

View File

@ -1,473 +0,0 @@
<template>
<div class="chatWrap">
<div class="content">
<div class="main">
<div id="scrollRef" ref="scrollRef" class="scrollArea">
<template v-if="chatData.length">
<div class="chatContentArea">
<chatMessage
v-for="(item, index) of chatData"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
></chatMessage>
</div>
<div v-if="loading" class="stopArea">
<a-button type="primary" danger @click="handleStop" class="stopBtn">
<svg
t="1706148514627"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5214"
width="18"
height="18"
>
<path
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
fill="currentColor"
p-id="5215"
></path>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
</svg>
<span>停止响应</span>
</a-button>
</div>
</template>
<template v-else>
<div class="emptyArea">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="mr-2 text-3xl iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M16 16a3 3 0 1 1 0 6a3 3 0 0 1 0-6M6 12a4 4 0 1 1 0 8a4 4 0 0 1 0-8m8.5-10a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11"
></path>
</svg>
<span>新建聊天</span>
</div>
</template>
</div>
</div>
<div class="footer">
<div class="topArea">
<presetQuestion @outQuestion="handleOutQuestion"></presetQuestion>
</div>
<div class="bottomArea">
<a-button type="text" class="delBtn" @click="handleDelSession">
<svg
t="1706504908534"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1584"
width="18"
height="18"
>
<path
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
fill="currentColor"
p-id="1585"
></path>
</svg>
</a-button>
<a-button type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
></path>
</svg>
</a-button>
<a-textarea
ref="inputRef"
v-model:value="prompt"
:autoSize="{ minRows: 1, maxRows: 6 }"
:placeholder="placeholder"
@pressEnter="handleEnter"
autofocus
></a-textarea>
<a-button
@click="
() => {
handleSubmit();
}
"
:disabled="loading"
type="primary"
class="sendBtn"
>
<svg
t="1706147858151"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4237"
width="1em"
height="1em"
>
<path
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
p-id="4238"
fill="currentColor"
></path>
</svg>
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Ref } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
import { useScroll } from '../hooks/useScroll';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { ConfigEnum } from '/@/enums/httpEnum';
import { getToken } from '/@/utils/auth';
import { getAppEnvConfig } from '/@/utils/env';
import chatMessage from './chatMessage.vue';
import presetQuestion from './presetQuestion.vue';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { message, Modal, Tabs } from 'ant-design-vue';
import { isObject, isString } from '/@/utils/is';
import '../style/github-markdown.less';
import '../style/highlight.less';
import '../style/style.less';
const props = defineProps(['chatData', 'uuid', 'dataSource']);
const emit = defineEmits(['save']);
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
const prompt = ref<string>('');
const loading = ref<boolean>(false);
const inputRef = ref<Ref | null>(null);
// const chatData = computed(() => {
// return props.chatData;
// });
// 当前模式下, 发送消息会携带之前的聊天记录
const usingContext = ref<any>(true);
const uuid = computed(() => {
return props.uuid;
});
let evtSource: any = null;
// const presetQuestion = ref(['小红书文案', '朋友圈文案', '演讲稿生成']);
const { VITE_GLOB_API_URL } = getAppEnvConfig();
const conversationList = computed(() => props.chatData.filter((item) => !item.inversion && !!item.conversationOptions));
const placeholder = computed(() => {
return '来说点什么吧...Shift + Enter = 换行)';
});
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
function handleSubmit() {
let message = prompt.value;
if (!message || message.trim() === '') return;
prompt.value = '';
onConversation(message);
}
const handleOutQuestion = (message) => {
onConversation(message);
};
async function onConversation(message) {
if (loading.value) return;
loading.value = true;
if (props.chatData.length == 0) {
const findItem = props.dataSource.history.find((item) => item.uuid === uuid.value);
if (findItem && findItem.title == '新建聊天') {
findItem.title = message;
}
}
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
text: message,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
});
scrollToBottom();
let options: any = {};
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
if (lastContext && usingContext.value) options = { ...lastContext };
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
text: '思考中...',
loading: true,
inversion: false,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
const initEventSource = () => {
let lastText = '';
if (typeof EventSource !== 'undefined') {
const token = getToken();
evtSource = new EventSourcePolyfill(
`${VITE_GLOB_API_URL}/test/ai/chat/send?message=${message}${options.parentMessageId ? '&topicId=' + options.parentMessageId : ''}`,
{
withCredentials: true,
headers: {
[ConfigEnum.TOKEN]: token,
},
}
); // 后端接口,要配置允许跨域属性
// 与事件源的连接刚打开时触发
evtSource.onopen = function (e) {
console.log(e);
};
// 当从事件源接收到数据时触发
evtSource.onmessage = function (e) {
const data = e.data;
let delay = 0;
setTimeout(() => {
if (data === '[DONE]') {
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
scrollToBottom();
handleStop();
evtSource.close(); // 关闭连接
} else {
try {
const _data = JSON.parse(data);
const content = _data.content;
if (content != undefined) {
lastText += content;
updateChat(uuid.value, props.chatData.length - 1, {
dateTime: new Date().toLocaleString(),
text: lastText,
inversion: false,
error: false,
loading: true,
conversationOptions: e.lastEventId == '[ERR]' ? null : { conversationId: data.conversationId, parentMessageId: e.lastEventId },
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
} else {
// updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
// scrollToBottom();
// handleStop();
}
} catch (error: any) {
console.log('ai 聊天:::', error);
if (isObject(error) && isString(error.message) && error.message.endsWith('is not valid JSON')) {
return;
}
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
scrollToBottom();
handleStop();
evtSource.close(); // 关闭连接
}
}
}, delay);
};
// 与事件源的连接无法打开时触发
evtSource.onerror = function (e) {
// console.log(e);
if (e.error?.message || e.statusText) {
updateChat(uuid.value, props.chatData.length - 1, {
dateTime: new Date().toLocaleString(),
text: e.error?.message ?? e.statusText,
inversion: false,
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
}
evtSource.close(); // 关闭连接
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
handleStop();
};
} else {
console.log('当前浏览器不支持使用EventSource接收服务器推送事件!');
}
};
initEventSource();
}
onUnmounted(() => {
evtSource?.close();
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
});
const addChat = (uuid, data) => {
props.chatData.push({ ...data });
};
const updateChat = (uuid, index, data) => {
props.chatData.splice(index, 1, data);
};
const updateChatSome = (uuid, index, data) => {
props.chatData[index] = { ...props.chatData[index], ...data };
};
// 清空会话
const handleDelSession = () => {
Modal.confirm({
title: '清空会话',
icon: createVNode(ExclamationCircleOutlined),
content: '是否清空会话?',
closable: true,
okText: '确定',
cancelText: '取消',
async onOk() {
try {
return await new Promise<void>((resolve) => {
props.chatData.length = 0;
emit('save');
resolve();
});
} catch {
return console.log('Oops errors!');
}
},
});
};
// 停止响应
const handleStop = () => {
console.log('ai 聊天:::---停止响应');
if (loading.value) {
loading.value = false;
}
if (evtSource) {
evtSource?.close();
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
}
};
// 是否使用上下文
const handleUsingContext = () => {
usingContext.value = !usingContext.value;
if (usingContext.value) {
message.success('当前模式下, 发送消息会携带之前的聊天记录');
} else {
message.warning('当前模式下, 发送消息不会携带之前的聊天记录');
}
};
onMounted(() => {
scrollToBottom();
});
</script>
<style lang="less" scoped>
.chatWrap {
width: 100%;
height: 100%;
padding: 20px;
.content {
height: 100%;
width: 100%;
background: #fff;
display: flex;
flex-direction: column;
}
}
.main {
flex: 1;
min-height: 0;
.scrollArea {
overflow-y: auto;
height: 100%;
}
.chatContentArea {
padding: 10px;
}
}
.emptyArea {
display: flex;
justify-content: center;
align-items: center;
color: #d4d4d4;
}
.stopArea {
display: flex;
justify-content: center;
padding: 10px 0;
.stopBtn {
display: flex;
justify-content: center;
align-items: center;
svg {
margin-right: 5px;
}
}
}
.footer {
display: flex;
flex-direction: column;
padding: 6px 16px;
.topArea {
padding-left: 94px;
margin-bottom: 6px;
}
.bottomArea {
display: flex;
align-items: center;
.ant-input {
margin: 0 16px;
}
.ant-input,
.ant-btn {
height: 36px;
}
textarea.ant-input {
padding-top: 6px;
padding-bottom: 6px;
}
.contextBtn,
.delBtn {
padding: 0;
width: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.delBtn {
margin-right: 8px;
}
.contextBtn {
color: #a8071a;
&.enabled {
color: @primary-color;
}
font-size: 18px;
}
.sendBtn {
padding: 0 10px;
font-size: 22px;
display: flex;
align-items: center;
}
}
}
</style>

View File

@ -1,74 +0,0 @@
<template>
<div class="chat" :class="[inversion ? 'self' : 'chatgpt']">
<div class="avatar">
<img v-if="inversion" :src="avatar()" />
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
<path
d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"
fill="currentColor"
/>
</svg>
</div>
<div class="content">
<p class="date">{{ dateTime }}</p>
<div class="msgArea">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import chatText from './chatText.vue';
import defaultAvatar from '../assets/avatar.jpg';
import { useUserStore } from '/@/store/modules/user';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
const { userInfo } = useUserStore();
const avatar = () => {
return getFileAccessHttpUrl(userInfo?.avatar)|| defaultAvatar;
};
</script>
<style lang="less" scoped>
.chat {
display: flex;
margin-bottom: 1.5rem;
&.self {
flex-direction: row-reverse;
.avatar {
margin-right: 0;
margin-left: 10px;
}
.msgArea {
flex-direction: row-reverse;
}
.date {
text-align: right;
}
}
}
.avatar {
flex: none;
margin-right: 10px;
img {
width: 34px;
height: 34px;
border-radius: 50%;
overflow: hidden;
}
svg {
font-size: 28px;
}
}
.content {
.date {
color: #b4bbc4;
font-size: 0.75rem;
margin-bottom: 10px;
}
.msgArea {
display: flex;
}
}
</style>

View File

@ -1,186 +0,0 @@
export const localData = {
active: 1002,
usingContext: true,
history: [
{
title: '标题02',
uuid: 1706083575869,
isEdit: false,
},
{
uuid: 1002,
title: '标题01',
isEdit: false,
},
],
chat: [
{
uuid: 1706083575869,
data: [
{
dateTime: '2024/1/24 16:06:27',
text: '',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '',
options: null,
},
},
{
dateTime: '2024/1/24 16:06:29',
text: 'Hello! How can I assist you today?',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kSZA0wju7X8sOdJIyxtpDj0RQVu1',
},
requestOptions: {
prompt: '',
options: {},
},
},
],
},
{
uuid: 1002,
data: [
{
dateTime: '2024/1/24 14:01:52',
text: '1',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '1',
options: null,
},
},
{
dateTime: '2024/1/24 14:01:54',
text: 'Yes, how can I assist you?',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
},
requestOptions: {
prompt: '1',
options: {},
},
},
{
dateTime: '2024/1/24 14:03:45',
text: '',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '',
options: null,
},
},
{
dateTime: '2024/1/24 14:03:47',
text: "I'm sorry if my previous response was not clear. Please let me know how I can help you or what you would like to discuss.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
},
requestOptions: {
prompt: '',
options: {
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
},
},
},
{
dateTime: '2024/1/24 14:10:19',
text: 'js 递归',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: 'js 递归',
options: null,
},
},
{
dateTime: '2024/1/24 14:10:33',
text: 'JavaScript supports recursion, which is the process of a function calling itself. Recursion can be useful for solving problems that can be broken down into smaller, similar sub-problems.\n\nHere\'s an example of a simple recursive function in JavaScript:\n\n```javascript\nfunction countdown(n) {\n if (n <= 0) {\n console.log("Done!");\n } else {\n console.log(n);\n countdown(n - 1); // recursive call\n }\n}\n\ncountdown(5);\n```\n\nIn this example, the `countdown` function takes an argument `n` and logs the value of `n` to the console. If `n` is greater than zero, it then calls itself with `n - 1`. This process continues until `n` becomes less than or equal to zero, at which point it logs "Done!".\n\nRecursion can be helpful in solving problems that involve tree structures, factorial calculations, searching algorithms, and more. However, it\'s important to use recursion properly to avoid infinite loops or excessive stack usage.',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
},
requestOptions: {
prompt: 'js 递归',
options: {
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
},
},
},
{
dateTime: '2024/1/24 14:17:15',
text: 'js 递归',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: 'js 递归',
options: null,
},
},
{
dateTime: '2024/1/24 14:23:50',
text: "Certainly! Here's an example of how you can use recursion in JavaScript:\n\n```javascript\nfunction factorial(n) {\n if (n === 0) {\n return 1;\n } else {\n return n * factorial(n - 1);\n }\n}\n\nconsole.log(factorial(5)); // Output: 120\n```\n\nIn this example, the `factorial` function calculates the factorial of a given number `n` using recursion. If `n` is equal to 0, it returns 1, which is the base case. Otherwise, it recursively calls itself with `n - 1`, multiplying the current value of `n` with the result of the recursive call.\n\nWhen calling `factorial(5)`, the function will execute as follows:\n\n- `factorial(5)` calls `factorial(4)`\n- `factorial(4)` calls `factorial(3)`\n- `factorial(3)` calls `factorial(2)`\n- `factorial(2)` calls `factorial(1)`\n- `factorial(1)` calls `factorial(0)`\n- `factorial(0)` returns 1\n- `factorial(1)` returns 1 * 1 = 1\n- `factorial(2)` returns 2 * 1 = 2\n- `factorial(3)` returns 3 * 2 = 6\n- `factorial(4)` returns 4 * 6 = 24\n- `factorial(5)` returns 5 * 24 = 120\n\nThe final result is then printed to the console using `console.log`.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
},
requestOptions: {
prompt: 'js 递归',
options: {
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
},
},
},
{
dateTime: '2024/1/24 15:05:30',
text: '///',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '///',
options: null,
},
},
{
dateTime: '2024/1/24 15:05:33',
text: "I apologize if my previous response was not what you were expecting. If you have any specific questions or need further assistance, please let me know and I'll be happy to help.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kRcAggkC4u47d34UcQW3cI0htw0w',
},
requestOptions: {
prompt: '///',
options: {
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
},
},
},
],
},
],
};

View File

@ -1,44 +0,0 @@
import type { Ref } from 'vue'
import { nextTick, ref } from 'vue'
type ScrollElement = HTMLDivElement | null
interface ScrollReturn {
scrollRef: Ref<ScrollElement>
scrollToBottom: () => Promise<void>
scrollToTop: () => Promise<void>
scrollToBottomIfAtBottom: () => Promise<void>
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null)
const scrollToBottom = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
const scrollToTop = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = 0
}
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 100 // Threshold, indicating the distance threshold to the bottom of the scroll bar.
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
}
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
}
}

View File

@ -1,222 +0,0 @@
<template>
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
<template v-if="dataSource">
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div class="content">
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave"></slide>
</div>
<div class="toggle-btn" @click="handleToggle">
<span class="icon">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="currentColor"
></path>
</svg>
</span>
</div>
</div>
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
<chat v-if="uuid && chatVisible" :uuid="uuid" :chatData="chatData" :dataSource="dataSource" @save="handleSave"></chat>
</div>
</template>
<Spin v-else :spinning="true"></Spin>
</div>
</template>
<script setup lang="ts">
import slide from './components/slide.vue';
import chat from './components/chat.vue';
import { Spin } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted } from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
const configUrl = {
get: '/test/ai/chat/history/get',
save: '/test/ai/chat/history/save',
};
const userId = useUserStore().getUserInfo?.id;
const localKey = JEECG_CHAT_KEY + userId;
let timer: any = null;
let unwatch01: any = null;
let unwatch02: any = null;
const dataSource = ref<any>(null);
const uuid = ref(null);
const chatData = ref([]);
const expand = ref<any>(true);
const chatVisible = ref(true);
const chatContainerRef = ref<any>(null);
const chatContainerStyle = ref({});
const handleToggle = () => {
expand.value = !expand.value;
};
// 初始查询历史
const init = () => {
const priming = () => {
dataSource.value = {
active: 1002,
usingContext: true,
history: [{ uuid: 1002, title: '新建聊天', isEdit: false }],
chat: [{ uuid: 1002, data: [] }],
};
};
defHttp
.get({ url: configUrl.get })
.then((res) => {
const { content } = res;
if (content) {
const json = JSON.parse(content);
if (json.history?.length) {
dataSource.value = json;
} else {
priming();
}
} else {
priming();
}
!unwatch01 && execute();
})
.catch(() => {
priming();
});
};
const save = (content) => {
defHttp.post({ url: configUrl.save, params: { content: JSON.stringify(content) } }, { isTransformResponse: false });
};
const handleSave = () => {
// 删除标签或清空内容之后的保存
save(dataSource.value);
setTimeout(() => {
// 删除标签或清空内容也会触发watch保存此时不需watch保存需清除
clearTimeout(timer);
}, 50);
};
// 监听dataSource变化执行操作
const execute = () => {
unwatch01 = watch(
() => dataSource.value.active,
(value) => {
if (value) {
const findItem = dataSource.value.chat.find((item) => item.uuid === value);
if (findItem) {
uuid.value = findItem.uuid;
chatData.value = findItem.data;
}
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
}
},
{ immediate: true }
);
unwatch02 = watch(dataSource.value, () => {
clearInterval(timer);
timer = setTimeout(() => {
save(dataSource.value);
}, 2e3);
});
};
onUnmounted(() => {
unwatch01 && unwatch01();
unwatch02 && unwatch02();
});
watch(
() => chatContainerRef.value,
() => {
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight}px` };
}
);
init();
</script>
<style scoped lang="less">
@width: 260px;
.chat-container {
position: relative;
height: 100%;
box-shadow:
0 0 #0000,
0 0 #0000,
0 0 #0000,
0 0 #0000,
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
border-width: 1px;
border-radius: 0.375rem;
display: flex;
overflow: hidden;
:deep(.ant-spin) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.leftArea {
width: @width;
transition: 0.3s left;
position: absolute;
left: 0;
height: 100%;
.content {
width: 100%;
height: 100%;
overflow: hidden;
}
&.shrink {
left: -@width;
.toggle-btn {
.icon {
transform: rotate(0deg);
}
}
}
.toggle-btn {
transition:
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
width: 24px;
height: 24px;
position: absolute;
top: 50%;
right: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: rgb(51, 54, 57);
border: 1px solid rgb(239, 239, 245);
background-color: #fff;
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
transform: translateX(50%) translateY(-50%);
z-index: 1;
}
.icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: rotate(180deg);
font-size: 18px;
height: 18px;
svg {
height: 1em;
width: 1em;
vertical-align: top;
}
}
}
.rightArea {
margin-left: @width;
transition: 0.3s margin-left;
&.shrink {
margin-left: 0;
}
flex: 1;
min-width: 0;
}
</style>

View File

@ -1,206 +0,0 @@
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
color: #abb2bf;
background: #282c34
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee
}
.hljs-function .hljs-params {
color: #a6e22e
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f
}
.hljs-module-access .hljs-module {
color: #7e57c2
}
.hljs-constructor {
color: #e2b93d
}
.hljs-constructor .hljs-string {
color: #9ccc65
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic
}
.hljs-doctag,
.hljs-formula {
color: #c678dd
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75
}
.hljs-literal {
color: #56b6c2
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649
}
.hljs-literal {
color: #0184bb
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}

View File

@ -1,135 +0,0 @@
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #fff;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
animation: blink 1s steps(5, start) infinite;
color: #000;
content: '_';
font-weight: 700;
margin-left: 3px;
vertical-align: baseline;
}
@keyframes blink {
to {
visibility: hidden;
}
}
}
html.dark {
.markdown-body {
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
color: #65a665;
}
}
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
@media screen and (max-width: 533px) {
.markdown-body .code-block-wrapper {
padding: unset;
code {
padding: 24px 16px 16px 16px;
}
}
}

View File

@ -1,6 +1,7 @@
import type { JPromptProps } from '../typing';
import { render, createVNode, nextTick } from 'vue';
import { error } from '/@/utils/log';
import { getAppContext } from "@/store";
import JPrompt from '../JPrompt.vue';
export function useJPrompt() {
@ -21,6 +22,7 @@ export function useJPrompt() {
document.body.removeChild(box);
},
});
vm.appContext = getAppContext()!;
// 挂载到 body
render(vm, box);
document.body.appendChild(box);

View File

@ -1,5 +1,5 @@
import type { JVxeColumn, JVxeDataProps, JVxeTableProps } from '../types';
import { computed, nextTick } from 'vue';
import { computed, nextTick, toRaw } from 'vue';
import { isArray, isEmpty, isPromise } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
@ -25,6 +25,11 @@ export interface HandleArgs {
export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
data.vxeColumns = computed(() => {
// update-begin--author:liaozhiyang---date:20250403---for【issues/7812】linkageConfig改变了vxetable没更新
// linkageConfig变化时也需要执行
const linkageConfig = toRaw(props.linkageConfig);
console.log(linkageConfig);
// update-end--author:liaozhiyang---date:20250403---for【issues/7812】linkageConfig改变了vxetable没更新
let columns: JVxeColumn[] = [];
if (isArray(props.columns)) {
// handle 方法参数

View File

@ -41,8 +41,8 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
},
expandConfig: {
iconClose: 'ant-table-row-expand-icon ant-table-row-expand-icon-collapsed',
iconOpen: 'ant-table-row-expand-icon ant-table-row-expand-icon-expanded',
iconClose: 'vxe-icon-arrow-right',
iconOpen: 'vxe-icon-arrow-down',
},
// 虚拟滚动配置y轴大于xx条数据时启用虚拟滚动
scrollY: {

View File

@ -140,11 +140,15 @@ export function useJVxeComponent(props: JVxeComponent.Props) {
const ctx = { props, context };
// 获取组件增强
const enhanced = getEnhanced(props.type);
let enhanced = getEnhanced(props.type);
watch(
value,
(newValue) => {
// -update-begin--author:liaozhiyang---date:20241210---for【issues/7497】隐藏某一列后字典没翻译恢复后正常
// TODO 先这样修复解决问题,根因后期再看看
enhanced = getEnhanced(props.type);
// -update-end--author:liaozhiyang---date:20241210---for【issues/7497】隐藏某一列后字典没翻译恢复后
// 验证值格式
let getValue = enhanced.getValue(newValue, ctx);
if (newValue !== getValue) {

View File

@ -52,7 +52,9 @@ export function usePagination(props: JVxeTableProps, methods: JVxeTableMethods)
[
h(Pagination, {
...bindProps.value,
disabled: props.disabled,
// update-begin--author:liaozhiyang---date:20250423---for【issues/8137】vxetable表格禁用后分页隐藏了
disabled: false,
// update-end--author:liaozhiyang---date:20250423---for【issues/8137】vxetable表格禁用后分页隐藏了
onChange: handleChange,
onShowSizeChange: handleShowSizeChange,
}),

View File

@ -39,6 +39,12 @@ export function registerJVxeTable(app: App) {
function preventClosingPopUp(this: any, params) {
// 获取组件增强
let col = params.column.params;
// update-begin--author:liaozhiyang---date:20250429---for【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错
if (col === undefined) {
// 说明使用的是纯原生的vxe-table
return;
}
// update-end--author:liaozhiyang---date:20250429---for【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错
let { $event } = params;
const interceptor = getEnhanced(col.type).interceptor;
// 执行增强

View File

@ -77,13 +77,13 @@
.vxe-table {
//.vxe-table--footer-wrapper.body--wrapper,
.vxe-table--body-wrapper.body--wrapper {
overflow-x: hidden;
// overflow-x: hidden;
}
&:hover {
//.vxe-table--footer-wrapper.body--wrapper,
.vxe-table--body-wrapper.body--wrapper {
overflow-x: auto;
// overflow-x: auto;
}
}
}

View File

@ -10,6 +10,7 @@ import { useMethods } from '/@/hooks/system/useMethods';
import { importViewsFile, _eval } from '/@/utils';
import {getToken} from "@/utils/auth";
import {replaceUserInfoByExpression} from "@/utils/common/compUtils";
import { isString } from '/@/utils/is';
export function usePopBiz(ob, tableRef?) {
// update-begin--author:liaozhiyang---date:20230811---for【issues/675】子表字段Popup弹框数据不更新
@ -178,10 +179,16 @@ export function usePopBiz(ob, tableRef?) {
*/
function combineRowKey(record) {
let res = record?.id || '';
Object.keys(record).forEach((key) => {
res = key == 'rowIndex' ? record[key] + res : res + record[key];
});
res = res.length > 50 ? res.substring(0, 50) : res;
if (props?.rowkey) {
// update-begin--author:liaozhiyang---date:20250415--for【issues/3656】popupdict回显
res = record[props.rowkey];
// update-end--author:liaozhiyang---date:20250415--for【issues/3656】popupdict回显
} else {
Object.keys(record).forEach((key) => {
res = key == 'rowIndex' ? record[key] + res : res + record[key];
});
res = res.length > 50 ? res.substring(0, 50) : res;
}
return res;
}
@ -211,6 +218,15 @@ export function usePopBiz(ob, tableRef?) {
currColumns[a].sortOrder = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
}
}
// update-begin--author:liaozhiyang---date:20250114---for【issues/946】popup列宽和在线报表列宽读取配置
currColumns.forEach((item) => {
if (item.fieldWidth != null) {
if (isString(item.fieldWidth) && item.fieldWidth.trim().length == 0) return;
item.width = item.fieldWidth;
delete item.fieldWidth;
}
});
// update-end--author:liaozhiyang---date:20250114---for【issues/946】popup列宽和在线报表列宽读取配置
if (currColumns[0].key !== 'rowIndex') {
currColumns.unshift({
title: '序号',
@ -258,7 +274,16 @@ export function usePopBiz(ob, tableRef?) {
// href 跳转
const fieldHrefSlotKeysMap = {};
fieldHrefSlots.forEach((item) => (fieldHrefSlotKeysMap[item.slotName] = item));
let currColumns = handleColumnHrefAndDict(metaColumnList, fieldHrefSlotKeysMap);
let currColumns: any = handleColumnHrefAndDict(metaColumnList, fieldHrefSlotKeysMap);
// update-begin--author:liaozhiyang---date:20250114---for【issues/946】popup列宽和在线报表列宽读取配置
currColumns.forEach((item) => {
if (isString(item.fieldWidth) && item.fieldWidth.trim().length == 0) return;
if (item.fieldWidth != null) {
item.width = item.fieldWidth;
delete item.fieldWidth;
}
});
// update-end--author:liaozhiyang---date:20250114---for【issues/946】popup列宽和在线报表列宽读取配置
// popup需要序号 普通列表不需要
if (clickThenCheckFlag === true) {

View File

@ -38,6 +38,7 @@ interface OnlineColumn {
dbType?:string;
//他表字段用
linkField?:string;
fieldExtendJson?:string
}
export { OnlineColumn, HrefSlots };

View File

@ -76,7 +76,9 @@
*/
async function getCaptchaCode() {
await resetFields();
randCodeData.checkKey = new Date().getTime();
//update-begin---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
randCodeData.checkKey = new Date().getTime() + Math.random().toString(36).slice(-4); // 1629428467008;
//update-end---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
getCodeInfo(randCodeData.checkKey).then((res) => {
randCodeData.randCodeImage = res;
randCodeData.requestCodeSuccess = true;

View File

@ -52,7 +52,20 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
window['_CONFIG'] = {}
}
// update-begin--author:sunjianlei---date:220250115---for【QQYUN-10956】配置了自定义前缀外部连接打不开需要兼容处理
let domainURL = VITE_GLOB_DOMAIN_URL;
// 如果不是以http(s)开头的,也不是以域名开头的,那么就是拼接当前域名
if (!/^http(s)?/.test(domainURL) && !/^(\/\/)?(.*\.)?.+\..+/.test(domainURL)) {
if (!domainURL.startsWith('/')) {
domainURL = '/' + domainURL;
}
domainURL = window.location.origin + domainURL;
}
// update-end--author:sunjianlei---date:220250115---for【QQYUN-10956】配置了自定义前缀外部连接打不开需要兼容处理
// @ts-ignore
window._CONFIG['domianURL'] = VITE_GLOB_DOMAIN_URL;
window._CONFIG['domianURL'] = domainURL;
return glob as Readonly<GlobConfig>;
};

View File

@ -8,6 +8,7 @@ import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { useDesign } from '/@/hooks/web/useDesign';
import { filterObj } from '/@/utils/common/compUtils';
import { isFunction } from '@/utils/is';
const { handleExportXls, handleImportXls } = useMethods();
// 定义 useListPage 方法所需参数
@ -24,7 +25,7 @@ interface ListPageOptions {
// 导出文件名
name?: string | (() => string);
//导出参数
params?: object;
params?: object | (() => object);
};
// 导入配置
importConfig?: {
@ -71,23 +72,32 @@ export function useListPage(options: ListPageOptions) {
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导出报错,原因未知-
let paramsForm:any = {};
try {
paramsForm = await getForm().validate();
//update-begin-author:liusq---date:2025-03-20--for: [QQYUN-11627]代码生成原生表单,数据导出,前端报错,并且范围参数没有转换 #7962
//当useSearchFor不等于false的时候才去触发validate
if (options?.tableProps?.useSearchForm !== false) {
paramsForm = await getForm().validate();
console.log('paramsForm', paramsForm);
}
//update-end-author:liusq---date:2025-03-20--for:[QQYUN-11627]代码生成原生表单,数据导出,前端报错,并且范围参数没有转换 #7962
} catch (e) {
console.error(e);
console.warn(e);
}
//update-end-author:taoyan date:20220507 for: erp代码生成 子表 导出报错,原因未知-
//update-begin-author:liusq date:20230410 for:[/issues/409]导出功能没有按排序结果导出,设置导出默认排序,创建时间倒序
if(!paramsForm?.column){
Object.assign(paramsForm,{column:'createTime',order:'desc'});
}
//update-begin-author:liusq date:20230410 for: [/issues/409]导出功能没有按排序结果导出,设置导出默认排序,创建时间倒序
//如果参数不为空,则整合到一起
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导出动态设置mainId
if (params) {
Object.keys(params).map((k) => {
let temp = (params as object)[k];
//update-begin-author:liusq---date:2025-03-20--for: [QQYUN-11627]代码生成原生表单,数据导出,前端报错,并且范围参数没有转换 #7962
const realParams = isFunction(params) ? await params() : { ...(params || {}) };
//update-end-author:liusq---date:2025-03-20--for:[QQYUN-11627]代码生成原生表单,数据导出,前端报错,并且范围参数没有转换 #7962
Object.keys(realParams).map((k) => {
let temp = (realParams as object)[k];
if (temp) {
paramsForm[k] = unref(temp);
}

View File

@ -52,7 +52,7 @@
const getShowHeader = computed(() => {
// 控制是否显示顶部
if (appStore.mainAppProps.hideHeader) {
if (appStore.getLayoutHideHeader) {
return false;
}
return unref(getShowInsetHeaderRef);
@ -60,7 +60,7 @@
const getShowTabs = computed(() => {
// 控制是否显示多Tabs切换
if (appStore.mainAppProps.hideMultiTabs) {
if (appStore.getLayoutHideMultiTabs) {
return false;
}
return unref(getShowMultipleTab) && !unref(getFullContent);

View File

@ -38,8 +38,8 @@
<UserDropDown :theme="getHeaderTheme" />
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
<!-- ai助手 -->
<Aide></Aide>
<!-- ai助手
<Aide></Aide> -->
</div>
</Header>
<LoginSelect ref="loginSelectRef" @success="loginSelectOk"></LoginSelect>

View File

@ -72,7 +72,7 @@
const showClassSideBarRef = computed(() => {
// 控制是否显示侧边栏
if (appStore.mainAppProps.hideSider) {
if (appStore.getLayoutHideSider) {
return false;
}
return unref(getSplit) ? !unref(getMenuHidden) : true;

View File

@ -16,6 +16,7 @@ import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/locales/setupI18n';
import { registerGlobComp } from '/@/components/registerGlobComp';
import { registerThirdComp } from '/@/settings/registerThirdComp';
import { registerSuper } from '/@/views/super/registerSuper';
import { useSso } from '/@/hooks/web/useSso';
import { checkIsQiankunMicro } from "/@/qiankun/micro";
import { autoUseQiankunMicro } from "/@/qiankun/micro/qiankunMicro";
@ -70,6 +71,9 @@ async function bootstrap(props?: MainAppProps) {
//CAS单点登录
await useSso().ssoLogin();
// 注册super应用路由
await registerSuper(app);
// 配置路由
setupRouter(app);

View File

@ -12,6 +12,7 @@ import {
} from '/@/enums/appEnum';
import { SIDE_BAR_BG_COLOR_LIST, HEADER_PRESET_BG_COLOR_LIST } from './designSetting';
import { primaryColor } from '../../build/config/themeConfig';
import { darkMode } from '/@/settings/designSetting';
// ! 改动后需要清空浏览器缓存
const setting: ProjectConfig = {
@ -43,6 +44,10 @@ const setting: ProjectConfig = {
// 项目主题色
themeColor: primaryColor,
// update-begin--author:liaozhiyang---date:20250414--for【QQYUN-11956】修复projectSetting中配置主题模式不生效
// 项目主题模式
themeMode: darkMode,
// update-end--author:liaozhiyang---date:20250414--for【QQYUN-11956】修复projectSetting中配置主题模式不生效
// 网站灰色模式,用于可能悼念的日期开启
grayMode: false,

View File

@ -2,12 +2,14 @@ import type { App } from 'vue';
import type { Pinia } from 'pinia';
import { createPinia } from 'pinia';
let app: Nullable<App<Element>> = null;
let store: Nullable<Pinia> = null;
export function setupStore(app: App<Element>) {
export function setupStore($app: App<Element>) {
if (store == null) {
store = createPinia();
}
app = $app;
app.use(store);
}
@ -16,4 +18,7 @@ export function destroyStore() {
store = null;
}
export { store };
// 获取app实例
export const getAppContext = () => app?._context;
export {app, store};

View File

@ -11,6 +11,8 @@ import { Persistent } from '/@/utils/cache/persistent';
import { darkMode } from '/@/settings/designSetting';
import { resetRouter } from '/@/router';
import { deepMerge } from '/@/utils';
import { getHideLayoutTypes } from '/@/utils/env';
import setting from '/@/settings/projectSetting';
interface AppState {
darkMode?: ThemeEnum;
@ -41,7 +43,20 @@ export const useAppStore = defineStore({
return this.pageLoading;
},
getDarkMode(): 'light' | 'dark' | string {
return this.darkMode || localStorage.getItem(APP_DARK_MODE_KEY_) || darkMode;
// liaozhiyang---date:20250411---for【QQYUN-11956】修复projectSetting中配置主题模式不生效
const getSettingTheme = () => {
const theme = setting.themeMode;
if (theme) {
if (theme == ThemeEnum.DARK) {
// 为了index.html页面loading时是暗黑
localStorage.setItem(APP_DARK_MODE_KEY_, theme);
}
return theme;
}
return '';
};
// liaozhiyang---date:20250411---for【QQYUN-11956】修复projectSetting中配置主题模式不生效
return this.darkMode || localStorage.getItem(APP_DARK_MODE_KEY_) || getSettingTheme() || darkMode;
},
getBeforeMiniInfo(): BeforeMiniState {
@ -70,6 +85,28 @@ export const useAppStore = defineStore({
getMainAppProps(): MainAppProps {
return this.mainAppProps;
},
getLayoutHideSider(): boolean {
const hideLayoutTypes = getHideLayoutTypes();
if (hideLayoutTypes.includes('sider')) {
return true;
}
return !!this.mainAppProps.hideSider;
},
getLayoutHideHeader(): boolean {
const hideLayoutTypes = getHideLayoutTypes();
if (hideLayoutTypes.includes('header')) {
return true;
}
return !!this.mainAppProps.hideHeader;
},
getLayoutHideMultiTabs(): boolean {
const hideLayoutTypes = getHideLayoutTypes();
if (hideLayoutTypes.includes('multi-tabs')) {
return true;
}
return !!this.mainAppProps.hideMultiTabs;
},
},
actions: {
setPageLoading(loading: boolean): void {
@ -79,6 +116,7 @@ export const useAppStore = defineStore({
setDarkMode(mode: ThemeEnum): void {
this.darkMode = mode;
localStorage.setItem(APP_DARK_MODE_KEY_, mode);
this.setProjectConfig({ themeMode: mode });
},
setBeforeMiniInfo(state: BeforeMiniState): void {

View File

@ -209,18 +209,11 @@ export const useUserStore = defineStore({
//update-begin---author:wangshuai ---date:20230424 for【QQYUN-5195】登录之后直接刷新页面导致没有进入创建组织页面------------
if (redirect && goHome) {
//update-end---author:wangshuai ---date:20230424 for【QQYUN-5195】登录之后直接刷新页面导致没有进入创建组织页面------------
// update-begin--author:liaozhiyang---date:20240104---forQQYUN-7804】部署生产环境登录跳转404问题
let publicPath = import.meta.env.VITE_PUBLIC_PATH;
if (publicPath && publicPath != '/') {
// update-begin--author:liaozhiyang---date:20240509---for【issues/1147】登录跳转时去掉发布路径的最后一个/以解决404问题
if (publicPath.endsWith('/')) {
publicPath = publicPath.slice(0, -1);
}
redirect = publicPath + redirect;
}
// update-end--author:liaozhiyang---date:20240509---for【issues/1147】登录跳转时去掉发布路径的最后一个/以解决404问题
// update-begin--author:liaozhiyang---date:20250407---forissues/8034】hash模式下退出重登录默认跳转地址异常
// router.options.history.base可替代之前的publicPath
// 当前页面打开
window.open(redirect, '_self')
window.open(`${router.options.history.base}${redirect}`, '_self');
// update-end--author:liaozhiyang---date:20250407---for【issues/8034】hash模式下退出重登录默认跳转地址异常
return data;
}
// update-end-author:sunjianlei date:20230306 for: 修复登录成功后,没有正确重定向的问题

View File

@ -6,6 +6,7 @@ import { reactive } from "vue";
import { getTenantId, getToken } from "/@/utils/auth";
import { useUserStoreWithOut } from "/@/store/modules/user";
import dayjs from 'dayjs';
import Big from 'big.js';
import { Modal } from "ant-design-vue";
import { defHttp } from "@/utils/http/axios";
@ -148,11 +149,16 @@ export function mapTableTotalSummary(tableData: Recordable[], fieldKeys: string[
// update-begin--author:liaozhiyang---date:20240118---for【QQYUN-7891】PR 合计工具方法转换为Nuber类型再计算
const value = Number(next[key]);
if (!Number.isNaN(value)) {
prev += value;
// update-begin--author:liaozhiyang---date:20250224---for【issues/7830】合计小数计算精度
prev = Big(prev).plus(value).toString();
// update-end--author:liaozhiyang---date:20250224---for【issues/7830】合计小数计算精度
}
// update-end--author:liaozhiyang---date:20240118---forQQYUN-7891】PR 合计工具方法转换为Nuber类型再计算
// update-end--author:liaozhiyang---date:20240118---forissues/7830】PR 合计工具方法转换为Nuber类型再计算
return prev;
}, 0);
// update-begin--author:liaozhiyang---date:20250224---for【issues/7830】合计小数计算精度
totals[key] = +totals[key];
// update-end--author:liaozhiyang---date:20250224---for【issues/7830】合计小数计算精度
});
return totals;
}

View File

@ -24,6 +24,21 @@ export const getDictItemsByCode = (code) => {
// update-end--author:liaozhiyang---date:20230908---for【QQYUN-6417】生产环境字典慢的问题
};
/**
* Popup字典翻译方法
* @param text
* @param code
*/
export const getPopDictByCode = (text, codeStr) => {
const [code, dictCode, dictText] = codeStr.split(',');
if (!code || !dictCode || !dictText) {
return [];
}
return defHttp.get(
{ url: `/online/api/cgreportGetDataPackage`, params: { code, dictText, dictCode, dataList: text } },
{ isTransformResponse: false }
);
};
/**
* 获取字典数组
* @param dictCode 字典Code

View File

@ -36,6 +36,8 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_CAS_BASE_URL,
VITE_GLOB_DOMAIN_URL,
VITE_GLOB_ONLINE_VIEW_URL,
// 全局隐藏哪些布局,多个用逗号隔开
VITE_GLOB_HIDE_LAYOUT_TYPES,
// 【JEECG作为乾坤子应用】
VITE_GLOB_QIANKUN_MICRO_APP_NAME,
@ -59,6 +61,7 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_CAS_BASE_URL,
VITE_GLOB_DOMAIN_URL,
VITE_GLOB_ONLINE_VIEW_URL,
VITE_GLOB_HIDE_LAYOUT_TYPES,
// 【JEECG作为乾坤子应用】
VITE_GLOB_QIANKUN_MICRO_APP_NAME,
@ -102,3 +105,11 @@ export function isDevMode(): boolean {
export function isProdMode(): boolean {
return import.meta.env.PROD;
}
export function getHideLayoutTypes(): string[] {
const {VITE_GLOB_HIDE_LAYOUT_TYPES} = getAppEnvConfig();
if (typeof VITE_GLOB_HIDE_LAYOUT_TYPES !== 'string') {
return [];
}
return VITE_GLOB_HIDE_LAYOUT_TYPES.split(',');
}

View File

@ -3,7 +3,7 @@ import type { App, Plugin } from 'vue';
import type { FormSchema } from "@/components/Form";
import { unref } from 'vue';
import { isObject } from '/@/utils/is';
import { isObject, isFunction, isString } from '/@/utils/is';
import Big from 'big.js';
// update-begin--author:sunjianlei---date:20220408---for: 【VUEN-656】配置外部网址打不开原因是带了#号,需要替换一下
export const URL_HASH_TAB = `__AGWE4H__HASH__TAG__PWHRG__`;
@ -41,7 +41,28 @@ export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
let key: string;
for (key in target) {
// update-begin--author:liaozhiyang---date:20240329---for【QQYUN-7872】online表单label较长优化
src[key] = isObject(src[key]) && isObject(target[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
if (isObject(src[key]) && isObject(target[key])) {
src[key] = deepMerge(src[key], target[key]);
} else {
// update-begin--author:liaozhiyang---date:20250318---for【issues/7940】componentProps写成函数形式时updateSchema写成对象时参数没合并
try {
if (isFunction(src[key]) && isObject(src[key]()) && isObject(target[key])) {
// src[key]是函数且返回对象且target[key]是对象
src[key] = deepMerge(src[key](), target[key]);
} else if (isObject(src[key]) && isFunction(target[key]) && isObject(target[key]())) {
// target[key]是函数且返回对象且src[key]是对象
src[key] = deepMerge(src[key], target[key]());
} else if (isFunction(src[key]) && isFunction(target[key]) && isObject(src[key]()) && isObject(target[key]())) {
// src[key]是函数且返回对象target[key]是函数且返回对象
src[key] = deepMerge(src[key](), target[key]());
} else {
src[key] = target[key];
}
} catch (error) {
src[key] = target[key];
}
// update-end--author:liaozhiyang---date:20250318---for【issues/7940】componentProps写成函数形式时updateSchema写成对象时参数没合并
}
// update-end--author:liaozhiyang---date:20240329---for【QQYUN-7872】online表单label较长优化
}
return src;
@ -575,3 +596,39 @@ export const getUrlParams = (url) => {
}
return result;
};
/* 20250325
* liaozhiyang
* 分割url字符成数组
* 【issues/7990】图片参数中包含逗号会错误的识别成多张图
* */
export const split = (str) => {
if (isString(str)) {
const text = str.trim();
if (text.startsWith('http')) {
const parts = str.split(',');
const urls: any = [];
let currentUrl = '';
for (const part of parts) {
if (part.startsWith('http://') || part.startsWith('https://')) {
// 如果遇到新的URL开头保存当前URL并开始新的URL
if (currentUrl) {
urls.push(currentUrl);
}
currentUrl = part;
} else {
// 否则是当前URL的一部分如参数
currentUrl += ',' + part;
}
}
// 添加最后一个URL
if (currentUrl) {
urls.push(currentUrl);
}
return urls;
} else {
return str.split(',');
}
}
return str;
};

View File

@ -2,10 +2,12 @@ import type { App } from 'vue';
import { warn } from '/@/utils/log';
import { registerDynamicRouter } from '/@/utils/monorepo/dynamicRouter';
// 引入模块
import PACKAGE_TEST_JEECG_ONLINE from '@jeecg/online';
import PACKAGE_JEECG_ONLINE from '@jeecg/online';
import PACKAGE_JEECG_AIFLOW from '@jeecg/aiflow';
export function registerPackages(app: App) {
use(app, PACKAGE_TEST_JEECG_ONLINE);
use(app, PACKAGE_JEECG_ONLINE);
use(app, PACKAGE_JEECG_AIFLOW);
}
// noinspection JSUnusedGlobalSymbols

View File

@ -7,7 +7,7 @@
</template>
<script setup>
import AiChat from '/@/components/jeecg/AiChat/index.vue';
import AiChat from '/@/views/super/airag/aiapp/chat/AiChat.vue';
</script>
@ -15,12 +15,10 @@
.wrap {
height: 100%;
width: 100%;
padding: 20px;
.content {
background: #fff;
width: 100%;
height: 100%;
padding: 20px;
}
}
</style>

View File

@ -420,9 +420,9 @@ export const schemas: FormSchema[] = [
component: 'RoleSelect',
componentProps: {
//最大选择数量
maxSelectCount: 3,
maxSelectCount: 4,
//是否单选
isRadioSelection: false
multi: true
},
},
{

View File

@ -9,10 +9,10 @@
</CollapseContainer>
<CollapseContainer class="mt-4" title="标签页操作">
<a-button class="mr-2" @click="closeAll"> 关闭所有 </a-button>
<a-button class="mr-2" @click="closeLeft"> 关闭左侧 </a-button>
<a-button class="mr-2" @click="closeRight"> 关闭右侧 </a-button>
<a-button class="mr-2" @click="closeOther"> 关闭其他 </a-button>
<a-button class="mr-2" @click="() => closeAll()"> 关闭所有 </a-button>
<a-button class="mr-2" @click="() => closeLeft()"> 关闭左侧 </a-button>
<a-button class="mr-2" @click="() => closeRight()"> 关闭右侧 </a-button>
<a-button class="mr-2" @click="() => closeOther()"> 关闭其他 </a-button>
<a-button class="mr-2" @click="closeCurrent"> 关闭当前 </a-button>
<a-button class="mr-2" @click="refreshPage"> 刷新当前 </a-button>
</CollapseContainer>

View File

@ -258,6 +258,20 @@ export const schemas: FormSchema[] = [
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'user4',
component: 'JSelectUserByDepartment',
label: '部门选择用户',
helpMessage: ['component模式'],
defaultValue: '',
componentProps: {
labelKey: 'realname',
rowKey: 'username',
},
colProps: {
span: 12,
},
},
{
field: 'role2',
component: 'JSelectRole',

View File

@ -4,7 +4,8 @@
<a-tabs v-model:activeKey="activeKey" @change="tabChange">
<a-tab-pane key="1" tab="服务器信息"></a-tab-pane>
<a-tab-pane key="2" tab="JVM信息" force-render></a-tab-pane>
<a-tab-pane key="3" tab="Tomcat信息"></a-tab-pane>
<!-- <a-tab-pane key="3" tab="Tomcat信息"></a-tab-pane> -->
<a-tab-pane key="6" tab="Undertow信息"></a-tab-pane>
<a-tab-pane key="4" tab="磁盘监控">
<DiskInfo v-if="activeKey == 4" style="height: 100%"></DiskInfo>
</a-tab-pane>

View File

@ -30,6 +30,11 @@ enum Api {
tomcatSessionsRejected = '/actuator/metrics/tomcat.sessions.rejected',
memoryInfo = '/sys/actuator/memory/info',
// undertow 监控
undertowSessionsCreated = '/actuator/metrics/undertow.sessions.created',
undertowSessionsExpired = '/actuator/metrics/undertow.sessions.expired',
undertowSessionsActiveCurrent = '/actuator/metrics/undertow.sessions.active.current',
undertowSessionsActiveMax = '/actuator/metrics/undertow.sessions.active.max',
}
/**
@ -207,6 +212,34 @@ export const getTomcatSessionsRejected = () => {
return defHttp.get({ url: Api.tomcatSessionsRejected }, { isTransformResponse: false });
};
/**
*undertow 已创建 session 数
*/
export const getUndertowSessionsCreated = () => {
return defHttp.get({ url: Api.undertowSessionsCreated }, { isTransformResponse: false });
};
/**
*undertow 已过期 session 数
*/
export const getUndertowSessionsExpired = () => {
return defHttp.get({ url: Api.undertowSessionsExpired }, { isTransformResponse: false });
};
/**
*undertow 当前活跃 session 数
*/
export const getUndertowSessionsActiveCurrent = () => {
return defHttp.get({ url: Api.undertowSessionsActiveCurrent }, { isTransformResponse: false });
};
/**
*undertow 活跃 session 数峰值
*/
export const getUndertowSessionsActiveMax = () => {
return defHttp.get({ url: Api.undertowSessionsActiveMax }, { isTransformResponse: false });
};
/**
* 内存信息
*/
@ -230,6 +263,9 @@ export const getMoreInfo = (infoType) => {
if (infoType == '5') {
return {};
}
if (infoType == '6') {
return {};
}
};
export const getTextInfo = (infoType) => {
@ -293,6 +329,16 @@ export const getTextInfo = (infoType) => {
'memory.runtime.usage': { color: 'purple', text: 'JVM内存使用率', unit: '%', valueType: 'Number' },
};
}
if (infoType == '6') {
// undertow 监控
return {
'undertow.sessions.created': { color: 'green', text: 'undertow 已创建 session 数', unit: '个' },
'undertow.sessions.expired': { color: 'green', text: 'undertow 已过期 session 数', unit: '个' },
'undertow.sessions.active.current': { color: 'green', text: 'undertow 当前活跃 session 数', unit: '个' },
'undertow.sessions.active.max': { color: 'green', text: 'undertow 活跃 session 数峰值', unit: '个' },
'undertow.sessions.rejected': { color: 'green', text: '超过session 最大配置后,拒绝的 session 个数', unit: '个' },
};
}
};
/**
@ -334,4 +380,13 @@ export const getServerInfo = (infoType) => {
if (infoType == '5') {
return Promise.all([getMemoryInfo()]);
}
// undertow监控
if (infoType == '6') {
return Promise.all([
getUndertowSessionsActiveCurrent(),
getUndertowSessionsActiveMax(),
getUndertowSessionsCreated(),
getUndertowSessionsExpired(),
]);
}
};

View File

@ -0,0 +1,118 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/openapi/list',
save='/openapi/add',
edit='/openapi/edit',
deleteOne = '/openapi/delete',
deleteBatch = '/openapi/deleteBatch',
genPath = '/openapi/genPath',
importExcel = '/openapi/importExcel',
exportXls = '/openapi/exportXls',
openApiHeaderList = '/openapi/list',
openApiParamList = '/openapi/list',
openApiJson = '/openapi/json',
}
/**
* 子表单查询接口
* @param params
*/
export const genPath = Api.genPath
/**
* swagger文档json
* @param params
*/
export const openApiJson = Api.openApiJson
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 子表单查询接口
* @param params
*/
export const queryOpenApiHeader = Api.openApiHeaderList
/**
* 子表单查询接口
* @param params
*/
export const queryOpenApiParam = Api.openApiParamList
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({url: Api.edit, params});
} else {
return defHttp.post({url: Api.save, params});
}
}
/**
* 获取接口地址
* @param params
*/
export const getGenPath = (params) =>
defHttp.get({url: Api.genPath, params},{isTransformResponse:false});
/**
* 子表列表接口
* @param params
*/
export const openApiHeaderList = (params) =>
defHttp.get({url: Api.openApiHeaderList, params},{isTransformResponse:false});
/**
* 子表列表接口
* @param params
*/
export const openApiParamList = (params) =>
defHttp.get({url: Api.openApiParamList, params},{isTransformResponse:false});
/**
* swagger文档json
* @param params
*/
export const getOpenApiJson = (params) =>
defHttp.get({url: Api.openApiJson, params},{isTransformResponse:false});

View File

@ -0,0 +1,347 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types'
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '接口名称',
align:"center",
dataIndex: 'name'
},
{
title: '请求方法',
align:"center",
dataIndex: 'requestMethod'
},
{
title: '接口地址',
align:"center",
dataIndex: 'requestUrl'
},
{
title: 'IP 黑名单',
align:"center",
dataIndex: 'blackList'
},
{
title: '状态',
align:"center",
dataIndex: 'status'
},
{
title: '创建人',
align:"center",
dataIndex: 'createBy'
},
{
title: '创建时间',
align:"center",
dataIndex: 'createTime'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
{
label: "接口名称",
field: "name",
component: 'JInput',
},
{
label: "创建人",
field: "createBy",
component: 'JInput',
},
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '接口名称',
field: 'name',
component: 'Input',
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入接口名称!'},
];
},
},
{
label: '请求方法',
field: 'requestMethod',
component: 'JSearchSelect',
componentProps:{
dictOptions: [
{
text: 'POST',
value: 'POST',
},
{
text: 'GET',
value: 'GET',
},
{
text: 'HEAD',
value: 'HEAD',
},
{
text: 'PUT',
value: 'PUT',
},
{
text: 'PATCH',
value: 'PATCH',
},
{
text: 'DELETE',
value: 'DELETE',
},{
text: 'OPTIONS',
value: 'OPTIONS',
},{
text: 'TRACE',
value: 'TRACE',
},
]
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入请求方法!'},
];
},
},
{
label: '接口地址',
field: 'requestUrl',
component: 'Input',
dynamicDisabled:true
},
{
label: 'IP 黑名单',
field: 'blackList',
component: 'Input',
},
{
label: '请求体内容',
component:"Input",
field: 'body'
},
{
label: '原始地址',
field: 'originUrl',
component: 'Input',
},
{
label: '删除标识',
field: 'delFlag',
component: 'Input',
defaultValue:0,
show:false
},
{
label: '状态',
field: 'status',
component: 'Input',
defaultValue:"1",
show:false
},
// TODO 主键隐藏字段目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false
},
];
//子表单数据
//子表列表数据
export const openApiHeaderColumns: BasicColumn[] = [
// {
// title: 'apiId',
// align:"center",
// dataIndex: 'apiId'
// },
{
title: '请求头Key',
align:"center",
dataIndex: 'headerKey'
},
{
title: '是否必填',
align:"center",
dataIndex: 'required_dictText'
},
{
title: '默认值',
align:"center",
dataIndex: 'defaultValue'
},
{
title: '备注',
align:"center",
dataIndex: 'note'
},
];
//子表列表数据
export const openApiParamColumns: BasicColumn[] = [
// {
// title: 'apiId',
// align:"center",
// dataIndex: 'apiId'
// },
{
title: '参数Key',
align:"center",
dataIndex: 'paramKey'
},
{
title: '是否必填',
align:"center",
dataIndex: 'required_dictText'
},
{
title: '默认值',
align:"center",
dataIndex: 'defaultValue'
},
{
title: '备注',
align:"center",
dataIndex: 'note'
},
];
//子表表格配置
export const openApiHeaderJVxeColumns: JVxeColumn[] = [
// {
// title: 'apiId',
// key: 'apiId',
// type: JVxeTypes.input,
// width:"200px",
// placeholder: '请输入${title}',
// defaultValue:'',
// },
{
title: '请求头Key',
key: 'headerKey',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '是否必填',
key: 'required',
type: JVxeTypes.checkbox,
options:[],
// dictCode:"yn",
width:"100px",
placeholder: '请输入${title}',
defaultValue:'',
customValue: ['1','0']
},
{
title: '默认值',
key: 'defaultValue',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '备注',
key: 'note',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
]
export const openApiParamJVxeColumns: JVxeColumn[] = [
// {
// title: 'apiId',
// key: 'apiId',
// type: JVxeTypes.input,
// width:"200px",
// placeholder: '请输入${title}',
// defaultValue:'',
// },
{
title: '参数Key',
key: 'paramKey',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '是否必填',
key: 'required',
type: JVxeTypes.checkbox,
options:[],
// dictCode:"yn",
width:"100px",
placeholder: '请输入${title}',
defaultValue:'',
customValue: ['1','0']
},
{
title: '默认值',
key: 'defaultValue',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '备注',
key: 'note',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
]
// 高级查询数据
export const superQuerySchema = {
name: {title: '接口名称',order: 0,view: 'text', type: 'string',},
requestMethod: {title: '请求方法',order: 1,view: 'list', type: 'string',dictCode: '',},
requestUrl: {title: '接口地址',order: 2,view: 'text', type: 'string',},
blackList: {title: 'IP 黑名单',order: 3,view: 'text', type: 'string',},
status: {title: '状态',order: 5,view: 'number', type: 'number',},
createBy: {title: '创建人',order: 6,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 7,view: 'datetime', type: 'string',},
//子表高级查询
openApiHeader: {
title: '请求头表',
view: 'table',
fields: {
// apiId: {title: 'apiId',order: 0,view: 'text', type: 'string',},
headerKey: {title: '请求头Key',order: 1,view: 'text', type: 'string',},
required: {title: '是否必填',order: 2,view: 'number', type: 'number',dictCode: 'yn',},
defaultValue: {title: '默认值',order: 3,view: 'text', type: 'string',},
note: {title: '备注',order: 4,view: 'text', type: 'string',},
}
},
openApiParam: {
title: '请求参数部分',
view: 'table',
fields: {
// apiId: {title: 'apiId',order: 0,view: 'text', type: 'string',},
paramKey: {title: '参数Key',order: 1,view: 'text', type: 'string',},
required: {title: '是否必填',order: 2,view: 'number', type: 'number',dictCode: 'yn',},
defaultValue: {title: '默认值',order: 3,view: 'text', type: 'string',},
note: {title: '备注',order: 4,view: 'text', type: 'string',},
}
},
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[]{
// 默认和原始表单保持一致 如果流程中配置了权限数据这里需要单独处理formSchema
return formSchema;
}

View File

@ -0,0 +1,120 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/openapi/auth/list',
save='/openapi/auth/add',
edit='/openapi/auth/edit',
apiList= '/openapi/list',
genAKSK = '/openapi/auth/genAKSK',
permissionList='/openapi/permission/list',
permissionAdd='/openapi/permission/add',
deleteOne = '/openapi/auth/delete',
deleteBatch = '/openapi/auth/deleteBatch',
importExcel = '/openapi/auth/importExcel',
exportXls = '/openapi/auth/exportXls',
}
/**
* 获取API
* @param params
*/
export const apiList = Api.apiList;
/**
* 权限添加
* @param params
*/
export const permissionAdd = Api.permissionAdd;
/**
* 生成AKSK
* @param params
*/
export const genAKSK = Api.genAKSK;
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除单个
* @param params
* @param handleSuccess
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
* @param handleSuccess
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
* @param isUpdate
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params }, { isTransformResponse: false });
}
/**
* 全部权限列表接口
* @param params
*/
export const getApiList = (params) => defHttp.get({ url: Api.apiList, params });
/**
* 获取已授权项目的接口
* @param params
*/
export const getPermissionList = (params) => defHttp.get({ url: Api.permissionList, params });
/**
* 授权保存方法
* @param params
* @param isUpdate
*/
export const permissionAddFunction = (params) => {
return defHttp.post({ url: Api.permissionAdd, params }, { isTransformResponse: false });
}
/**
* 授权保存方法
* @param params
* @param isUpdate
*/
export const getGenAKSK = (params) => {
return defHttp.get({ url: Api.genAKSK, params });
}

View File

@ -0,0 +1,49 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '授权名称',
align: "center",
dataIndex: 'name'
},
{
title: 'AK',
align: "center",
dataIndex: 'ak'
},
{
title: 'SK',
align: "center",
dataIndex: 'sk'
},
{
title: '创建人',
align: "center",
dataIndex: 'createBy'
},
{
title: '创建时间',
align: "center",
dataIndex: 'createTime'
},
{
title: '关联系统用户名',
align: "center",
dataIndex: 'systemUserId_dictText',
},
];
// 高级查询数据
export const superQuerySchema = {
name: {title: '授权名称',order: 0,view: 'text', type: 'string',},
ak: {title: 'AK',order: 1,view: 'text', type: 'string',},
sk: {title: 'SK',order: 2,view: 'text', type: 'string',},
createBy: {title: '创建人',order: 3,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 4,view: 'datetime', type: 'string',},
systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
};

View File

@ -0,0 +1,277 @@
<template>
<div class="p-2">
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item name="name">
<template #label><span title="授权名称">授权名称</span></template>
<a-input placeholder="请输入授权名称" v-model:value="queryParam.name" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="systemUserId">
<template #label><span title="关联系统用户名">关联系统用户名</span></template>
<JSearchSelect dict="sys_user,username,id" v-model:value="queryParam.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
<!-- <a-input placeholder="请输入关联系统用户名" v-model:value="queryParam.systemUserId" allow-clear ></a-input>-->
</a-form-item>
</a-col>
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
<a @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
{{ toggleSearchStatus ? '收起' : '展开' }}
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</a>
</a-col>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'openapi:open_api_auth:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiAuthModal ref="registerModal" @success="handleSuccess"></OpenApiAuthModal>
<AuthModal ref="authModal" @success="handleSuccess"></AuthModal>
</div>
</template>
<script lang="ts" name="openapi-openApiAuth" setup>
import { ref, reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, superQuerySchema } from './OpenApiAuth.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './OpenApiAuth.api';
import OpenApiAuthModal from './components/OpenApiAuthModal.vue'
import AuthModal from './components/AuthModal.vue'
import { useUserStore } from '/@/store/modules/user';
import JSearchSelect from "../../components/Form/src/jeecg/components/JSearchSelect.vue";
const formRef = ref();
const queryParam = reactive<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
const authModal = ref();
const userStore = useUserStore();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '授权管理',
api: list,
columns,
canResize:false,
useSearchForm: false,
actionColumn: {
width: 200,
fixed: 'right',
},
beforeFetch: async (params) => {
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: "授权管理",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
});
const [registerTable, { reload, updateTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs:24,
sm:10,
xl:6,
xxl:10
});
const wrapperCol = reactive({
xs: 24,
sm: 20,
});
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
searchQuery();
}
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
function handleAuth(record: Recordable) {
authModal.value.disableSubmit = false;
authModal.value.edit(record);
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
registerModal.value.disableSubmit = false;
registerModal.value.edit(record);
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '授权',
onClick: handleAuth.bind(null, record),
auth: 'openapi:open_api_auth:edit'
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'openapi:open_api_auth:edit'
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:delete'
}
]
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
formRef.value.resetFields();
selectedRowKeys.value = [];
//刷新数据
reload();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
padding: 0;
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
.query-group-cust{
min-width: 100px !important;
}
.query-group-split-cust{
width: 30px;
display: inline-block;
text-align: center
}
.ant-form-item:not(.ant-form-item-with-help){
margin-bottom: 16px;
height: 32px;
}
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection" @expand="handleExpand">
<!-- 内嵌table区域 begin -->
<!-- <template #expandedRowRender="{record}">-->
<!-- <a-tabs tabPosition="top">-->
<!-- <a-tab-pane tab="请求头表" key="openApiHeader" forceRender>-->
<!-- <openApiHeaderSubTable v-if="expandedRowKeys.includes(record.id)" :id="record.id" />-->
<!-- </a-tab-pane>-->
<!-- <a-tab-pane tab="请求参数部分" key="openApiParam" forceRender>-->
<!-- <openApiParamSubTable v-if="expandedRowKeys.includes(record.id)" :id="record.id" />-->
<!-- </a-tab-pane>-->
<!-- </a-tabs>-->
<!-- </template>-->
<!-- 内嵌table区域 end -->
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'openapi:open_api:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'openapi:open_api:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiModal @register="registerModal" @success="handleSuccess"></OpenApiModal>
</div>
</template>
<script lang="ts" name="openapi-openApi" setup>
import {ref, reactive, computed, unref} from 'vue';
import {BasicTable, useTable, TableAction} from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage'
import {useModal} from '/@/components/Modal';
import OpenApiModal from './components/OpenApiModal.vue'
import OpenApiHeaderSubTable from './subTables/OpenApiHeaderSubTable.vue'
import OpenApiParamSubTable from './subTables/OpenApiParamSubTable.vue'
import {columns, searchFormSchema, superQuerySchema} from './OpenApi.data';
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './OpenApi.api';
import {downloadFile} from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
const queryParam = reactive<any>({});
// 展开key
const expandedRowKeys = ref<any[]>([]);
//注册model
const [registerModal, {openModal}] = useModal();
const userStore = useUserStore();
//注册table数据
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
title: '接口管理',
api: list,
columns,
canResize:false,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter:true,
showAdvancedButton:true,
fieldMapToNumber: [
],
fieldMapToTime: [
],
},
actionColumn: {
width: 120,
fixed:'right'
},
beforeFetch: (params) => {
return Object.assign(params, queryParam);
},
},
exportConfig: {
name:"接口管理",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
})
const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 展开事件
* */
function handleExpand(expanded, record){
expandedRowKeys.value=[];
if (expanded === true) {
expandedRowKeys.value.push(record.id)
}
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({id: record.id}, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ids: selectedRowKeys.value},handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'openapi:open_api:edit'
}
]
}
/**
* 下拉操作栏
*/
function getDropDownAction(record){
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft'
},
auth: 'openapi:open_api:delete'
}
]
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div ref="swaggerUiRef" style="height: 100%;"></div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
// 尝试直接导入 SwaggerUI 而不是使用 * as
import SwaggerUI from 'swagger-ui-dist/swagger-ui-bundle'; // 确保使用 ESM 版本
import 'swagger-ui-dist/swagger-ui.css';
import { getOpenApiJson } from './OpenApi.api';
const swaggerUiRef = ref<HTMLElement | null>(null);
const API_DOMAIN = import.meta.env.VITE_GLOB_DOMAIN_URL
onMounted(async () => {
try {
const response = await getOpenApiJson();
const openApiJson = response;
if (swaggerUiRef.value) {
SwaggerUI({
domNode: swaggerUiRef.value,
spec: openApiJson,
});
}
} catch (error) {
console.error('Failed to fetch OpenAPI JSON:', error);
}
});
</script>
<style scoped>
/* 确保容器有高度 */
.swagger-ui-container {
height: 100%;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<a-spin :spinning="confirmLoading">
<JFormContainer :disabled="disabled">
<template #detail>
<BasicTable @register="registerTable" :rowSelection="rowSelection"> </BasicTable>
</template>
</JFormContainer>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { permissionAddFunction, getPermissionList, getApiList } from '../OpenApiAuth.api';
import { Form } from 'ant-design-vue';
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
import { BasicTable } from '@/components/Table';
import { useListPage } from '@/hooks/system/useListPage';
import { columns } from '@/views/openapi/OpenApi.data';
const queryParam = reactive<any>({});
//注册table数据
const { tableContext } = useListPage({
tableProps: {
title: '授权',
api: getApiList,
columns,
canResize: false,
useSearchForm: false,
beforeFetch: async (params) => {
return Object.assign(params, queryParam);
},
},
});
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
tableContext;
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => ({}) },
formBpm: { type: Boolean, default: true },
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
apiAuthId: '',
apiIdList: [],
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
//表单验证
const validatorRules = reactive({});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
// 表单禁用
const disabled = computed(() => {
if (props.formBpm === true) {
if (props.formData.disabled === false) {
return false;
} else {
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
function edit(record) {
nextTick(() => {
resetFields();
//赋值
formData.apiAuthId = record.id;
// 获取当前已授权的项目
getPermissionList({ apiAuthId: record.id }).then((res) => {
if (res && res.length > 0) {
let list = res.result.records || res.result;
let ids = [];
list.forEach((item) => {
ids.push(item.apiId);
});
selectedRowKeys.value = ids;
formData.apiIdList = ids;
}
});
});
}
/**
* 提交数据
*/
async function submitForm() {
if(selectedRowKeys.value.length === 0)
return emit('ok');
try {
// 触发表单验证
await validate();
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
}
confirmLoading.value = true;
//时间格式化
let model = formData;
// model.apiIdList = selectedRowKeys.value;
let apiId = ""
selectedRowKeys.value.forEach((item) => {
apiId += item +",";
})
model.apiId = apiId;
delete model.apiIdList
await permissionAddFunction(model)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({
add,
edit,
submitForm,
});
</script>
<style lang="less" scoped>
.antd-modal-form {
padding: 14px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<AuthForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></AuthForm>
</j-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import AuthForm from './AuthForm.vue'
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 授权
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '授权';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style lang="less">
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
<style lang="less" scoped></style>

View File

@ -0,0 +1,175 @@
<template>
<a-spin :spinning="confirmLoading">
<JFormContainer :disabled="disabled">
<template #detail>
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="OpenApiAuthForm">
<a-row>
<a-col :span="24">
<a-form-item label="授权名称" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
<a-input v-model:value="formData.name" placeholder="请输入授权名称" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="AK" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
<a-input v-model:value="formData.ak" placeholder="请输入AK" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="SK" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
<a-input v-model:value="formData.sk" placeholder="请输入SK" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="关联系统用户名" v-bind="validateInfos.systemUserId" id="OpenApiAuthForm-systemUserId" name="systemUserId">
<JSearchSelect dict="sys_user,username,id" v-model:value="formData.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</JFormContainer>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed, } from 'vue';
import { USER_INFO_KEY} from '/@/enums/cacheEnum';
import { useMessage } from '/@/hooks/web/useMessage';
import { getValueType } from '/@/utils';
import { saveOrUpdate,getGenAKSK } from '../OpenApiAuth.api';
import { Form } from 'ant-design-vue';
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
import { getAuthCache } from "@/utils/auth";
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => ({})},
formBpm: { type: Boolean, default: true },
title: { type: String, default: "" },
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
id: '',
name: '',
ak: '',
sk: '',
systemUserId: '',
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
//表单验证
const validatorRules = reactive({
name:[{ required: true, message: '请输入授权名称!'},],
systemUserId:[{ required: true, message: '请输入关联系统用户名!'},],
});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
// 表单禁用
const disabled = computed(()=>{
if(props.formBpm === true){
if(props.formData.disabled === false){
return false;
}else{
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
async function add() {
edit({});
const AKSKObj = await getGenAKSK({});
formData.ak = AKSKObj[0];
formData.sk = AKSKObj[1];
}
/**
* 编辑
*/
function edit(record) {
const userData = getAuthCache(USER_INFO_KEY)
if(props.title == "新增"){
record.systemUserId = userData.id
}
nextTick(() => {
resetFields();
const tmpData = {};
Object.keys(formData).forEach((key) => {
if(record.hasOwnProperty(key)){
tmpData[key] = record[key]
}
})
//赋值
Object.assign(formData, tmpData);
});
}
/**
* 提交数据
*/
async function submitForm() {
try {
// 触发表单验证
await validate();
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
}
confirmLoading.value = true;
const isUpdate = ref<boolean>(false);
//时间格式化
let model = formData;
if (model.id) {
isUpdate.value = true;
}
//循环数据
for (let data in model) {
//如果该数据是数组并且是字符串类型
if (model[data] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, data);
//如果是字符串类型的需要变成以逗号分割的字符串
if (valueType === 'string') {
model[data] = model[data].join(',');
}
}
}
await saveOrUpdate(model, isUpdate.value)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({
add,
edit,
submitForm,
});
</script>
<style lang="less" scoped>
.antd-modal-form {
padding: 14px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<OpenApiAuthForm ref="registerForm" @ok="submitCallback" :title="title" :formDisabled="disableSubmit" :formBpm="false"></OpenApiAuthForm>
</j-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import OpenApiAuthForm from './OpenApiAuthForm.vue'
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style lang="less">
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
<style lang="less" scoped></style>

View File

@ -0,0 +1,177 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="'80%'" @ok="handleSubmit">
<a-row :gutter="24">
<a-col :span="12">
<BasicForm @register="registerForm" ref="formRef" name="OpenApiForm" />
</a-col>
<a-col :span="12">
<a-row :gutter="24">
<a-col :span="24">
<JVxeTable
keep-source
resizable
ref="openApiHeader"
:loading="openApiHeaderTable.loading"
:columns="openApiHeaderTable.columns"
:dataSource="openApiHeaderTable.dataSource"
:height="340"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
/>
</a-col>
<a-col :span="24">
<JVxeTable
keep-source
resizable
ref="openApiParam"
:loading="openApiParamTable.loading"
:columns="openApiParamTable.columns"
:dataSource="openApiParamTable.dataSource"
:height="340"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
/>
</a-col>
</a-row>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
import { formSchema, openApiHeaderJVxeColumns, openApiParamJVxeColumns } from '../OpenApi.data';
import { saveOrUpdate, queryOpenApiHeader, queryOpenApiParam, getGenPath } from '../OpenApi.api';
import { VALIDATE_FAILED } from '/@/utils/common/vxeUtils';
import { useMessage } from "@/hooks/web/useMessage";
// Emits声明
const $message = useMessage();
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const formDisabled = ref(false);
const refKeys = ref(['openApiHeader', 'openApiParam']);
const activeKey = ref('openApiHeader');
const openApiHeader = ref();
const openApiParam = ref();
const tableRefs = { openApiHeader, openApiParam };
const openApiHeaderTable = reactive({
loading: false,
dataSource: [],
columns: openApiHeaderJVxeColumns,
});
const openApiParamTable = reactive({
loading: false,
dataSource: [],
columns: openApiParamJVxeColumns,
});
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await reset();
setModalProps({ confirmLoading: false, showCancelBtn: data?.showFooter, showOkBtn: data?.showFooter });
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
// 请求后端接口获取数据
// requestSubTableData(queryOpenApiHeader, {id:data?.record?.id}, openApiHeaderTable)
// requestSubTableData(queryOpenApiParam, {id:data?.record?.id}, openApiParamTable)
openApiHeaderTable.dataSource = !!data.record.headersJson?JSON.parse(data.record.headersJson):[];
openApiParamTable.dataSource = !!data.record.paramsJson?JSON.parse(data.record.paramsJson):[];
} else {
// /openapi/genpath
const requestUrlObj = await getGenPath({});
await setFieldsValue({
requestUrl: requestUrlObj.result
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//方法配置
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
requestAddOrEdit,
classifyIntoFormData,
tableRefs,
activeKey,
refKeys
);
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
async function reset() {
await resetFields();
activeKey.value = 'openApiHeader';
openApiHeaderTable.dataSource = [];
openApiParamTable.dataSource = [];
}
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue);
return {
...main, // 展开
headersJson: allValues.tablesValue[0].tableData,
paramsJson: allValues.tablesValue[1].tableData,
};
}
//表单提交事件
async function requestAddOrEdit(values) {
let headersJson = !!values.headersJson?JSON.stringify(values.headersJson):null;
let paramsJson = !!values.headersJson?JSON.stringify(values.paramsJson):null;
try {
if (!!values.body){
try {
if (typeof JSON.parse(values.body)!='object'){
$message.createMessage.error("JSON格式化错误,请检查输入数据");
return;
}
} catch (e) {
$message.createMessage.error("JSON格式化错误,请检查输入数据");
return;
}
}
setModalProps({ confirmLoading: true });
values.headersJson = headersJson
values.paramsJson = paramsJson
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div>
<!--引用表格-->
<BasicTable bordered size="middle" :loading="loading" rowKey="id" :canResize="false" :columns="openApiHeaderColumns" :dataSource="dataSource" :pagination="false">
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
</div>
</template>
<script lang="ts" setup>
import {ref,watchEffect} from 'vue';
import {BasicTable} from '/@/components/Table';
import {openApiHeaderColumns} from '../OpenApi.data';
import {openApiHeaderList} from '../OpenApi.api';
import { downloadFile } from '/@/utils/common/renderUtils';
const props = defineProps({
id: {
type: String,
default: '',
},
})
const loading = ref(false);
const dataSource = ref([]);
watchEffect(() => {
props.id && loadData(props.id);
});
function loadData(id) {
dataSource.value = []
loading.value = true
openApiHeaderList({id}).then((res) => {
if (res.success) {
dataSource.value = res.result.records
}
}).finally(() => {
loading.value = false
})
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<div>
<!--引用表格-->
<BasicTable bordered size="middle" :loading="loading" rowKey="id" :canResize="false" :columns="openApiParamColumns" :dataSource="dataSource" :pagination="false">
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
</div>
</template>
<script lang="ts" setup>
import {ref,watchEffect} from 'vue';
import {BasicTable} from '/@/components/Table';
import {openApiParamColumns} from '../OpenApi.data';
import {openApiParamList} from '../OpenApi.api';
import { downloadFile } from '/@/utils/common/renderUtils';
const props = defineProps({
id: {
type: String,
default: '',
},
})
const loading = ref(false);
const dataSource = ref([]);
watchEffect(() => {
props.id && loadData(props.id);
});
function loadData(id) {
dataSource.value = []
loading.value = true
openApiParamList({id}).then((res) => {
if (res.success) {
dataSource.value = res.result.records
}
}).finally(() => {
loading.value = false
})
}
</script>

View File

@ -0,0 +1,81 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
export enum Api {
//知识库管理
list = '/airag/app/list',
save = '/airag/app/edit',
delete = '/airag/app/delete',
queryById = '/airag/app/queryById',
queryBathById = '/airag/knowledge/query/batch/byId',
queryFlowById = '/airag/flow/queryById',
promptGenerate = '/airag/app/prompt/generate',
}
/**
* 查询应用
* @param params
*/
export const appList = (params) => {
return defHttp.get({ url: Api.list, params }, { isTransformResponse: false });
};
/**
* 查询知识库
* @param params
*/
export const queryKnowledgeBathById = (params) => {
return defHttp.get({ url: Api.queryBathById, params }, { isTransformResponse: false });
};
/**
* 根据应用id查询应用
* @param params
*/
export const queryById = (params) => {
return defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
};
/**
* 新增应用
* @param params
*/
export const saveApp = (params) => {
return defHttp.put({ url: Api.save, params });
};
/**
* 删除应用
* @param params
* @param handleSuccess
*/
export const deleteApp = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除名称为'+params.name+'的应用吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 根据应用id查询流程
* @param params
*/
export const queryFlowById = (params) => {
return defHttp.get({ url: Api.queryFlowById, params }, { isTransformResponse: false });
};
/**
* 应用编排
* @param params
*/
export const promptGenerate = (params) => {
return defHttp.get({ url: Api.promptGenerate, params,timeout: 5 * 60 * 1000 }, { isTransformResponse: false });
};

View File

@ -0,0 +1,88 @@
import { FormSchema } from '@/components/Form';
/**
* 表单
*/
export const formSchema: FormSchema[] = [
{
label: 'id',
field: 'id',
component: 'Input',
show: false,
},
{
label: '应用名称',
field: 'name',
required: true,
componentProps: {
//是否展示字数
showCount: true,
maxlength: 64,
},
component: 'Input',
},
{
label: '应用描述',
field: 'descr',
component: 'InputTextArea',
componentProps: {
placeholder: '描述该应用的应用场景及用途',
rows: 4,
//是否展示字数
showCount: true,
maxlength: 256,
},
},
{
label: '应用图标',
field: 'icon',
component: 'JImageUpload',
},
{
label: '选择应用类型',
field: 'type',
component: 'Input',
ifShow:({ values })=>{
return !values.id;
},
slot: 'typeSlot',
},
];
/**
* 快捷指令表单
*/
export const quickCommandFormSchema: FormSchema[] = [
{
label: 'key',
field: 'key',
component: 'Input',
show: false,
},
{
label: '按钮名称',
field: 'name',
required: true,
component: 'Input',
componentProps: {
showCount: true,
maxLength: 10,
},
},
{
label: '按钮图标',
field: 'icon',
component: 'IconPicker',
},
{
label: '指令内容',
field: 'descr',
required: true,
component: 'InputTextArea',
componentProps: {
autosize: { minRows: 4, maxRows: 4 },
showCount: true,
maxLength: 100,
}
},
];

View File

@ -0,0 +1,494 @@
<!--知识库文档列表-->
<template>
<div class="p-2 knowledge">
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form
ref="formRef"
@keyup.enter.native="reload"
:model="queryParam"
:label-col="labelCol"
:wrapper-col="wrapperCol"
style="background-color: #f7f8fc"
>
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item name="name" label="应用名称">
<JInput v-model:value="queryParam.name" placeholder="请输入应用名称" />
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="type" label="应用类型">
<j-dict-select-tag v-model:value="queryParam.type" dict-code="ai_app_type" placeholder="请选择应用类型" />
</a-form-item>
</a-col>
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="reload">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
</a-col>
</span>
</a-col>
</a-row>
</a-form>
</div>
<a-row :span="24" class="knowledge-row">
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
<span style="line-height: 18px; font-weight: 500; color: #676f83; font-size: 12px">创建应用</span>
<div class="add-knowledge-doc" @click="handleCreateApp">
<Icon icon="ant-design:form-outlined" size="13"></Icon>
<span>创建空白应用</span>
</div>
</a-card>
</a-col>
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeAppDataList">
<a-card class="knowledge-card pointer" @click="handleEditClick(item)">
<div class="flex">
<img class="header-img" :src="getImage(item.icon)" />
<div class="header-text">
<span class="header-text-top header-name ellipsis"> {{ item.name }} </span>
<span class="header-text-top header-create ellipsis"> 创建者{{ item.createBy }} </span>
</div>
</div>
<div class="header-tag">
<a-tag color="#EBF1FF" style="margin-right: 0" v-if="item.type === 'chatSimple'">
<span style="color: #3370ff">简单配置</span>
</a-tag>
<a-tag color="#FDF6EC" style="margin-right: 0" v-if="item.type === 'chatFLow'">
<span style="color: #e6a343">高级编排</span>
</a-tag>
</div>
<div class="card-description">
<span>{{ item.descr || '暂无描述' }}</span>
</div>
<div class="card-footer">
<a-tooltip title="演示">
<div class="card-footer-icon" @click.prevent.stop="handleViewClick(item.id)">
<Icon class="operation" icon="ant-design:youtube-outlined" size="20" color="#1F2329"></Icon>
</div>
</a-tooltip>
<a-divider type="vertical" style="float: left" />
<a-tooltip title="删除">
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
<Icon icon="ant-design:delete-outlined" class="operation" size="20" color="#1F2329"></Icon>
</div>
</a-tooltip>
<a-divider type="vertical" style="float: left" />
<a-tooltip title="发布">
<a-dropdown class="card-footer-icon" placement="bottomRight" :trigger="['click']">
<div @click.prevent.stop>
<Icon icon="ant-design:send-outlined"></Icon>
</div>
<template #overlay>
<a-menu>
<a-menu-item key="web" @click.prevent.stop="handleSendClick(item,'web')">
<Icon icon="ant-design:dribbble-outlined" size="16"></Icon>
嵌入网站
</a-menu-item>
<a-menu-item key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
<Icon icon="ant-design:menu-outlined" size="16"></Icon> 配置菜单
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</div>
</a-card>
</a-col>
</a-row>
<Pagination
v-if="knowledgeAppDataList.length > 0"
:current="pageNo"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:total="total"
:showQuickJumper="true"
:showSizeChanger="true"
@change="handlePageChange"
class="list-footer"
size="small"
/>
<!-- Ai新增弹窗 -->
<AiAppModal @register="registerModal" @success="handleSuccess"></AiAppModal>
<!-- Ai设置弹窗 -->
<AiAppSettingModal @register="registerSettingModal" @success="reload"></AiAppSettingModal>
<!-- 发布弹窗 -->
<AiAppSendModal @register="registerAiAppSendModal"/>
</div>
</template>
<script lang="ts">
import { ref, reactive } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { Avatar, Modal, Pagination } from 'ant-design-vue';
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
import defaultImg from './img/ailogo.png';
import AiAppModal from './components/AiAppModal.vue';
import AiAppSettingModal from './components/AiAppSettingModal.vue';
import AiAppSendModal from './components/AiAppSendModal.vue';
import Icon from '@/components/Icon';
import { appList, deleteApp } from './AiApp.api';
import { useMessage } from '@/hooks/web/useMessage';
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
export default {
name: 'AiAppList',
components: {
JDictSelectTag,
JInput,
AiAppSendModal,
Icon,
Pagination,
Avatar,
LoadingOutlined,
BasicModal,
AiAppModal,
AiAppSettingModal,
},
emits: ['success', 'register'],
setup(props, { emit }) {
/**
* 创建应用的集合
*/
const knowledgeAppDataList = ref<any>([]);
//当前页数
const pageNo = ref<number>(1);
//每页条数
const pageSize = ref<number>(10);
//总条数
const total = ref<number>(0);
//可选择的页数
const pageSizeOptions = ref<any>(['10', '20', '30']);
//注册modal
const [registerModal, { openModal }] = useModal();
const [registerSettingModal, { openModal: openAppModal }] = useModal();
const [registerAiAppSendModal, { openModal: openAiAppSendModal }] = useModal();
const { createMessage } = useMessage();
//查询参数
const queryParam = reactive<any>({});
//查询区域label宽度
const labelCol = reactive({
xs: 24,
sm: 4,
xl: 6,
xxl: 6,
});
//查询区域组件宽度
const wrapperCol = reactive({
xs: 24,
sm: 20,
});
//表单的ref
const formRef = ref();
reload();
/**
* 加载数据
*/
function reload() {
let params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
column: 'createTime',
order: 'desc',
};
Object.assign(params, queryParam);
appList(params).then((res) => {
if (res.success) {
knowledgeAppDataList.value = res.result.records;
total.value = res.result.total;
} else {
knowledgeAppDataList.value = [];
total.value = 0;
}
});
}
/**
* 创建应用
*/
function handleCreateApp() {
openModal(true, {});
}
/**
* 分页改变事件
* @param page
* @param current
*/
function handlePageChange(page, current) {
pageNo.value = page;
pageSize.value = current;
reload();
}
/**
* 成功
*/
function handleSuccess(id) {
reload();
//打开编辑弹窗
openAppModal(true, {
isUpdate: false,
id: id,
});
}
/**
* 获取图片
* @param url
*/
function getImage(url) {
return url ? getFileAccessHttpUrl(url) : defaultImg;
}
/**
* 编辑
* @param item
*/
function handleEditClick(item) {
console.log('item:::', item);
openAppModal(true, {
isUpdate: true,
...item,
});
}
/**
* 演示
*/
function handleViewClick(id) {
window.open('/ai/app/chat/' + id , '_blank');
}
/**
* 删除
*/
function handleDeleteClick(item) {
deleteApp({ id: item.id, name: item.name }, reload);
}
/**
* 发布点击事件
* @param item 数据
* @param type 类别
*/
function handleSendClick(item,type) {
openAiAppSendModal(true,{
type: type,
data: item
})
}
/**
* 重置
*/
function searchReset() {
formRef.value.resetFields();
queryParam.name = '';
//刷新数据
reload();
}
return {
handleCreateApp,
knowledgeAppDataList,
pageNo,
pageSize,
total,
pageSizeOptions,
handlePageChange,
cardBodyStyle: { textAlign: 'left', width: '100%' },
registerModal,
handleSuccess,
getImage,
handleEditClick,
handleViewClick,
handleDeleteClick,
registerSettingModal,
reload,
queryParam,
labelCol,
wrapperCol,
handleSendClick,
registerAiAppSendModal,
searchReset,
formRef,
};
},
};
</script>
<style scoped lang="less">
.knowledge {
display: grid;
height: 100%;
background: #f7f8fc;
padding: 24px;
overflow-y: auto;
}
.add-knowledge-card {
border-radius: 10px;
margin-bottom: 20px;
display: inline-flex;
font-size: 16px;
height: 152px;
width: calc(100% - 20px);
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
.add-knowledge-card-icon {
padding: 8px;
color: #1f2329;
background-color: #f5f6f7;
margin-right: 12px;
}
}
.knowledge-card {
border-radius: 10px;
margin-right: 20px;
margin-bottom: 20px;
height: 152px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
.header-img {
width: 40px;
height: 40px;
border-radius: 0.5rem;
}
.header-text {
margin-left: 5px;
position: relative;
font-size: 14px;
display: grid;
width: calc(100% - 100px);
.header-name {
font-weight: bold;
color: #354052;
}
.header-create {
font-size: 12px;
color: #646a73;
}
}
.header-tag {
position: absolute;
right: 4px;
top: 6px;
}
}
.add-knowledge-card,
.knowledge-card {
transition: box-shadow 0.3s ease;
}
.add-knowledge-card:hover,
.knowledge-card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.knowledge-row {
overflow-y: auto;
}
.add-knowledge-doc {
margin-top: 6px;
color: #6f6f83;
font-size: 13px;
width: 100%;
cursor: pointer;
display: flex;
span {
margin-left: 4px;
line-height: 28px;
}
}
.add-knowledge-doc:hover {
background: #c8ceda33;
}
.card-description {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
height: 4.5em;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
margin-top: 10px;
text-align: left;
font-size: 12px;
color: #676f83;
}
.card-footer {
position: absolute;
bottom: 8px;
left: 0;
min-height: 30px;
padding: 0 16px;
width: 100%;
align-items: center;
display: flex;
}
.card-footer-icon {
font-size: 14px;
height: 24px;
padding: 0 7px;
border-radius: 4px;
text-align: center;
align-content: center;
float: left;
width: 36px;
}
.card-footer-icon:hover {
color: #000000;
background-color: rgba(0, 0, 0, 0.05);
border: none;
}
.operation {
position: relative;
top: 2px;
}
.list-footer {
text-align: right;
margin-top: 5px;
}
:deep(.ant-card .ant-card-body) {
padding: 16px;
}
.ellipsis{
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
}
.jeecg-basic-table-form-container {
padding: 0;
:deep(.ant-form) {
background-color: transparent;
}
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
</style>
<style lang="less">
.airag-knowledge-doc .scroll-container {
padding: 0 !important;
}
</style>

View File

@ -0,0 +1,372 @@
<template>
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
<template v-if="dataSource">
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div class="content">
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
</div>
<div class="toggle-btn" @click="handleToggle">
<span class="icon">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="currentColor"
></path>
</svg>
</span>
</div>
</div>
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
<chat
url="/airag/chat/send"
v-if="uuid && chatVisible"
:uuid="uuid"
:historyData="chatData"
type="view"
@save="handleSave"
:formState="appData"
:prologue="prologue"
:presetQuestion="presetQuestion"
@reload-message-title="reloadMessageTitle"
:chatTitle="chatTitle"
:quickCommandData="quickCommandData"
></chat>
</div>
</template>
<Spin v-else :spinning="true"></Spin>
</div>
</template>
<script setup lang="ts">
import slide from './slide.vue';
import chat from './chat.vue';
import { Spin } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
import { useRouter } from 'vue-router';
const router = useRouter();
const userId = useUserStore().getUserInfo?.id;
const localKey = JEECG_CHAT_KEY + userId;
let timer: any = null;
let unwatch01: any = null;
const dataSource = ref<any>({});
const uuid = ref<string>('');
const chatData = ref<any>([]);
const expand = ref<any>(true);
const chatVisible = ref(true);
const chatContainerRef = ref<any>(null);
const chatContainerStyle = ref({});
//左侧聊天信息
const chatTitle = ref<string>('');
//左侧聊天点击的坐标
const chatActiveKey = ref<number>(0);
//预置开场白
const presetQuestion = ref<string>('');
const handleToggle = () => {
expand.value = !expand.value;
};
//应用id
const appId = ref<string>('');
//应用数据
const appData = ref<any>({});
//开场白
const prologue = ref<string>('');
//快捷指令
const quickCommandData = ref<any>([]);
const priming = () => {
dataSource.value = {
active: '1002',
usingContext: true,
history: [{ id: '1002', title: '新建聊天', isEdit: false, disabled: true }],
};
chatTitle.value = '新建聊天';
chatActiveKey.value = 0;
};
const handleSave = () => {
// 删除标签或清空内容之后的保存
//save(dataSource.value);
setTimeout(() => {
// 删除标签或清空内容也会触发watch保存此时不需watch保存需清除
//clearTimeout(timer);
}, 50);
};
// 监听dataSource变化执行操作
const execute = () => {
unwatch01 = watch(
() => dataSource.value.active,
(value) => {
if (value) {
if (value == '1002') {
uuid.value = '1002';
chatData.value = [];
chatTitle.value = "新建聊天";
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
return;
}
//update-begin---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天删除会话后聊天切换到新的会话但是聊天标题没有变---
let values = dataSource.value.history.filter((item) => item.id === value);
if(values && values.length>0){
chatTitle.value = values[0]?.title
}
//update-end---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天删除会话后聊天切换到新的会话但是聊天标题没有变---
//根据选中的id查询聊天内容
let params = { conversationId: value };
uuid.value = value;
defHttp.get({ url: '/airag/chat/messages', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
chatData.value = res.result;
} else {
chatData.value = [];
}
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
});
}else{
chatData.value = [];
chatTitle.value = "";
}
},
{ immediate: true }
);
};
/**
* 初始化聊天信息
* @param appId
*/
function initChartData(appId = '') {
defHttp
.get(
{
url: '/airag/chat/conversations',
params: { appId: appId },
},
{ isTransformResponse: false }
)
.then((res) => {
if (res.success && res.result && res.result.length > 0) {
dataSource.value.history = res.result;
dataSource.value.active = res.result[0].id;
chatTitle.value = res.result[0].title;
chatActiveKey.value = 0;
} else {
priming();
}
!unwatch01 && execute();
})
.catch(() => {
priming();
});
}
onMounted(() => {
let params: any = router.currentRoute.value.params;
if (params.appId) {
appId.value = params.appId;
getApplicationData(params.appId);
initChartData(params.appId);
} else {
initChartData();
quickCommandData.value = [
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
{ name: 'JEECG有哪些优势', descr: "JEECG有哪些优势" },
{ name: 'JEECG可以做哪些事情', descr: "JEECG可以做哪些事情" },];
}
});
onUnmounted(() => {
chatData.value = [];
chatTitle.value = "";
prologue.value = ""
presetQuestion.value = "";
quickCommandData.value = [];
})
/**
* 获取应用id
*
* @param appId
*/
async function getApplicationData(appId) {
await defHttp
.get(
{
url: '/airag/app/queryById',
params: { id: appId },
},
{ isTransformResponse: false }
)
.then((res) => {
if (res.success) {
appData.value = res.result;
if (res.result && res.result.prologue) {
prologue.value = res.result.prologue;
}
if (res.result && res.result.quickCommand) {
quickCommandData.value = JSON.parse(res.result.quickCommand);
}
if (res.result && res.result.presetQuestion) {
presetQuestion.value = res.result.presetQuestion;
}
} else {
appData.value = {};
}
});
}
/**
* 左侧消息列表点击事件
* @param title
* @param index
*/
function handleChatClick(title, index) {
chatTitle.value = title;
chatActiveKey.value = index;
}
/**
* 重新加载标题消息
* @param text
*/
function reloadMessageTitle(text) {
let title = dataSource.value.history[chatActiveKey.value].title;
if(title === '新建聊天'){
dataSource.value.history[chatActiveKey.value].title = text;
dataSource.value.history[chatActiveKey.value]['disabled'] = false;
}
}
/**
* 初始化聊天用于icon点击
*/
function initChat(value) {
appId.value = value;
getApplicationData(value);
initChartData(value);
}
defineExpose({
initChat
})
onUnmounted(() => {
unwatch01 && unwatch01();
});
watch(
() => chatContainerRef.value,
() => {
if(chatContainerRef.value.offsetHeight){
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight} px` };
}
}
);
</script>
<style scoped lang="less">
@width: 260px;
.chat-container {
height: 100%;
width: 100%;
position: relative;
background: white;
display: flex;
overflow: hidden;
z-index: 999;
border: 1px solid #eeeeee;
:deep(.ant-spin) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.leftArea {
width: @width;
transition: 0.3s left;
position: absolute;
left: 0;
height: 100%;
.content {
width: 100%;
height: 100%;
overflow: hidden;
}
&.shrink {
left: -@width;
.toggle-btn {
.icon {
transform: rotate(0deg);
}
}
}
.toggle-btn {
transition:
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
width: 24px;
height: 24px;
position: absolute;
top: 50%;
right: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: rgb(51, 54, 57);
border: 1px solid rgb(239, 239, 245);
background-color: #fff;
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
transform: translateX(50%) translateY(-50%);
z-index: 1;
}
.icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: rotate(180deg);
font-size: 18px;
height: 18px;
svg {
height: 1em;
width: 1em;
vertical-align: top;
}
}
}
.rightArea {
margin-left: @width;
transition: 0.3s margin-left;
&.shrink {
margin-left: 0;
}
flex: 1;
min-width: 0;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="footer">
<div v-if="!showChat" class="footer-icon" @click="chatClick">
<Icon icon="ant-design:comment-outlined" size="22"></Icon>
</div>
<div v-if="showChat" class="footer-close-icon" @click="chatClick">
<Icon icon="ant-design:close-outlined" size="20"></Icon>
</div>
<div v-if="showChat" class="ai-chat">
<AiChat ref="aiChatRef"></AiChat>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import AiChat from './AiChat.vue';
import { useRouter } from 'vue-router';
//aiChat<61><74>ref
const aiChatRef = ref();
//Ӧ<><D3A6>id
const appId = ref<string>('');
//<2F>Ƿ<EFBFBD><C7B7><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD>
const showChat = ref<any>(false);
const router = useRouter();
//<2F>ж<EFBFBD><D0B6>Ƿ<EFBFBD>Ϊ<EFBFBD><CEAA>ʼ<EFBFBD><CABC>
const isInit = ref<boolean>(false);
/**
* chatͼ<74><CDBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
*/
function chatClick() {
showChat.value = !showChat.value;
if(showChat.value && !isInit.value){
setTimeout(()=>{
isInit.value = true;
aiChatRef.value.initChat(appId.value);
},100)
}
}
onMounted(() => {
let params: any = router.currentRoute.value.params;
appId.value = params?.appId;
isInit.value = false;
});
</script>
<style scoped lang="less">
.footer {
position: fixed;
bottom: 16px;
right: 16px;
left: unset;
top: unset;
.footer-icon {
cursor: pointer;
background-color: #155eef;
color: white;
border-radius: 100%;
padding: 20px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
}
.footer-close-icon {
color: #0a3069;
height: 48px;
position: absolute;
right: 10px;
top: 20px;
cursor: pointer;
z-index: 9999;
}
.ai-chat {
border: 1px solid #eeeeee;
width: calc(100vh - 20px);
height: calc(100vh - 200px);
}
}
</style>

View File

@ -0,0 +1,919 @@
<template>
<div class="chatWrap">
<div class="content">
<div class="header-title" v-if="type === 'view' && headerTitle">
{{headerTitle}}
</div>
<div class="main">
<div id="scrollRef" ref="scrollRef" class="scrollArea">
<template v-if="chatData.length>0">
<div class="chatContentArea">
<chatMessage
v-for="(item, index) of chatData"
:key="index"
:date-time="item.dateTime || item.datetime"
:text="item.content"
:inversion="item.inversion || item.role"
:error="item.error"
:loading="item.loading"
:appData="appData"
:presetQuestion="item.presetQuestion"
:images = "item.images"
@send="handleOutQuestion"
></chatMessage>
</div>
</template>
</div>
</div>
<div class="footer">
<div class="topArea">
<presetQuestion @outQuestion="handleOutQuestion" :quickCommandData="quickCommandData"></presetQuestion>
</div>
<div class="bottomArea">
<a-button type="text" class="delBtn" @click="handleDelSession()">
<svg
t="1706504908534"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1584"
width="18"
height="18"
>
<path
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
fill="currentColor"
p-id="1585"
></path>
</svg>
</a-button>
<a-button v-if="type === 'view'" type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
></path>
</svg>
</a-button>
<div class="chat-textarea" :class="textareaActive?'textarea-active':''">
<div class="textarea-top" v-if="uploadUrlList && uploadUrlList.length>0">
<div v-for="(item,index) in uploadUrlList" class="top-image" :key="index">
<img :src="getImage(item)" @click="handlePreview(item)"/>
<div class="upload-icon" @click="deleteImage(index)">
<Icon icon="ant-design:close-outlined" size="12px"></Icon>
</div>
</div>
</div>
<div class="textarea-bottom">
<a-textarea
ref="inputRef"
v-model:value="prompt"
:autoSize="{ minRows: 1, maxRows: 6 }"
:placeholder="placeholder"
@pressEnter="handleEnter"
@focus="textareaActive = true"
@blur="textareaActive = false"
autofocus
:readonly="loading"
style="border-color: #ffffff !important;box-shadow:none"
>
</a-textarea>
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
<svg
t="1706148514627"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5214"
width="18"
height="18"
>
<path
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
fill="currentColor"
p-id="5215"
></path>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
</svg>
</a-button>
<a-upload
accept=".jpg,.jpeg,.png"
v-if="!loading"
name="file"
v-model:file-list="fileInfoList"
:showUploadList="false"
:headers="headers"
:beforeUpload="beforeUpload"
@change="handleChange"
:multiple="true"
:action="uploadUrl"
:max-count="3"
>
<a-tooltip title="图片上传支持jpg/jpeg/png">
<a-button class="sendBtn" type="text">
<Icon icon="ant-design:picture-outlined" style="color: rgba(15,21,40,0.8)"></Icon>
</a-button>
</a-tooltip>
</a-upload>
<a-divider v-if="!loading" type="vertical" style="border-color:#38374314"></a-divider>
<a-button
@click="
() => {
handleSubmit();
}
"
:disabled="!prompt"
class="sendBtn"
type="text"
v-if="!loading"
>
<svg
t="1706147858151"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4237"
width="1em"
height="1em"
>
<path
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
p-id="4238"
fill="currentColor"
></path>
</svg>
</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, watch } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
import { useScroll } from './js/useScroll';
import chatMessage from './chatMessage.vue';
import presetQuestion from './presetQuestion.vue';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { message, Modal, Tabs } from 'ant-design-vue';
import './style/github-markdown.less';
import './style/highlight.less';
import './style/github-markdown.less';
import dayjs from 'dayjs';
import { defHttp } from '@/utils/http/axios';
import { cloneDeep } from "lodash-es";
import {getFileAccessHttpUrl, getHeaders} from "@/utils/common/compUtils";
import { uploadUrl } from '/@/api/common/api';
import { createImgPreview } from "@/components/Preview";
message.config({
prefixCls: 'ai-chat-message',
});
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
const emit = defineEmits(['save','reload-message-title']);
const { scrollRef, scrollToBottom } = useScroll();
const prompt = ref<string>('');
const loading = ref<boolean>(false);
const inputRef = ref<Ref | null>(null);
const headerTitle = ref<string>(props.chatTitle);
//聊天数据
const chatData = ref<any>([]);
//应用数据
const appData = ref<any>({});
const usingContext = ref<any>(true);
const uuid = ref<string>(props.uuid);
const topicId = ref<string>('');
//请求id
const requestId = ref<string>('');
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
const placeholder = computed(() => {
return '来说点什么吧...Shift + Enter = 换行)';
});
//token
const headers = getHeaders();
//文本域点击事件
const textareaActive = ref<boolean>(false);
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
function handleSubmit() {
let message = prompt.value;
if (!message || message.trim() === '') return;
prompt.value = '';
onConversation(message);
}
const handleOutQuestion = (message) => {
onConversation(message);
};
async function onConversation(message) {
if(!props.type && props.type != 'view'){
if(appData.value.type && appData.value.type == 'chatSimple' && !appData.value.modelId) {
messageTip("请选择AI模型");
return;
}
if(appData.value.type && appData.value.type == 'chatFLow' && !appData.value.flowId) {
messageTip("请选择关联流程");
return;
}
if(!appData.value.name) {
messageTip("请填写应用名称");
return;
}
}
if (loading.value) return;
loading.value = true;
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: message,
images:uploadUrlList.value?uploadUrlList.value:[],
inversion: 'user',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
});
scrollToBottom();
let options: any = {};
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
if (lastContext && usingContext.value) {
options = { ...lastContext };
}
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: '思考中...',
loading: true,
inversion: 'ai',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
//发送消息
sendMessage(message,options);
}
onUnmounted(() => {
updateChatSome(uuid.value, chatData.value.length - 1, { loading: false });
});
const addChat = (uuid, data) => {
chatData.value.push({ ...data });
};
const updateChat = async (uuid, index, data) => {
chatData.value.splice(index, 1, data);
await scrollToBottom();
};
/**
* 顶置开场白
* @param txt
*/
const topChat = (txt) => {
let data = {
content: txt,
key: 'prologue',
loading: false,
dateTime: dayjs().format('YYYY/MM/DD HH:mm:ss'),
inversion: 'ai',
presetQuestion: props.presetQuestion ? JSON.parse(props.presetQuestion) : "",
};
if (chatData.value && chatData.value.length > 0) {
let key = chatData.value[0].key;
if (key === 'prologue') {
chatData.value[0] = { ...data };
return;
}
}
chatData.value.unshift({ ...data });
};
const updateChatSome = (uuid, index, data) => {
chatData.value[index] = { ...chatData.value[index], ...data };
};
const updateChatFail = (uuid, index, data) => {
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: data,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: null,
requestOptions: null,
});
scrollToBottom();
};
/**
* 清空会话
* @param id
*/
function handleDelSession (){
Modal.confirm({
title: '清空会话',
icon: createVNode(ExclamationCircleOutlined),
content: '是否清空会话?',
closable: true,
okText: '确定',
cancelText: '取消',
wrapClassName:'ai-chat-modal',
async onOk() {
try {
defHttp.get({
url: '/airag/chat/messages/clear/' + uuid.value,
},{ isTransformResponse: false }).then((res) => {
if(res.success){
chatData.value = [];
topicId.value = "";
if(props.prologue){
topChat(props.prologue);
}
}
})
} catch {
return console.log('Oops errors!');
}
},
});
};
// 停止响应
const handleStop = () => {
console.log('ai 聊天:::---停止响应');
if (loading.value) {
loading.value = false;
}
updateChatSome(uuid, chatData.value.length - 1, { loading: false });
};
handleStop();
/**
* 停止消息
*/
function handleStopChat() {
if(requestId.value){
//调用后端接口停止响应
defHttp.get({
url: '/airag/chat/stop/' + requestId.value,
},{ isTransformResponse: false });
}
handleStop();
}
/**
* 读取文本
* @param message
* @param options
*/
async function sendMessage(message, options) {
let param = {};
if (!props.type && props.type != 'view') {
param = {
content: message,
images: uploadUrlList.value?uploadUrlList.value:[],
topicId: topicId.value,
app: appData.value,
responseMode: 'streaming',
};
}else{
param = {
content: message,
topicId: usingContext.value?topicId.value:'',
images: uploadUrlList.value?uploadUrlList.value:[],
appId: appData.value.id,
responseMode: 'streaming',
conversationId: uuid.value === "1002"?'':uuid.value
};
if(headerTitle.value == '新建聊天'){
headerTitle.value = message.length>5?message.substring(0,5):message
}
emit("reload-message-title",message.length>5?message.substring(0,5):message)
}
uploadUrlList.value = [];
fileInfoList.value = [];
const readableStream = await defHttp.post(
{
url: props.url,
params: param,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000,
},
{
isTransformResponse: false,
}
).catch((e)=>{
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
handleStop();
return;
});
const reader = readableStream.getReader();
const decoder = new TextDecoder('UTF-8');
let conversationId = '';
let buffer = '';
let text = ''; // 按 SSE 协议分割消息
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
let result = decoder.decode(value, { stream: true });
result = buffer + result;
const lines = result.split('\n\n');
for (const line of lines) {
if (line.startsWith('data:')) {
const content = line.replace('data:', '').trim();
if(!content){
continue;
}
if(!content.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
try {
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
let parse = JSON.parse(content);
await renderText(parse,conversationId,text,options).then((res)=>{
text = res.returnText;
conversationId = res.conversationId;
});
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
} catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
}else{
if(!line){
continue;
}
if(!line.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
try {
let parse = JSON.parse(line);
await renderText(parse, conversationId, text, options).then((res) => {
text = res.returnText;
conversationId = res.conversationId;
});
}catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态内容不能加载出来得刷新才能看到全部回答---
}
}
}
}
// 是否使用上下文
const handleUsingContext = () => {
usingContext.value = !usingContext.value;
if (usingContext.value) {
message.success("当前模式下, 发送消息会携带之前的聊天记录");
} else {
message.warning("当前模式下, 发送消息不会携带之前的聊天记录");
}
};
/**
* 提示
* @param value
*/
function messageTip(value) {
message.warning(value);
}
/**
* 渲染文本
* @param item
* @param conversationId
* @param text
* @param options
*/
async function renderText(item,conversationId,text,options) {
let returnText = "";
if (item.event == 'MESSAGE') {
text = text + item.data.message;
returnText = text;
//更新聊天信息
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: text,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
requestOptions: { prompt: message, options: { ...options } },
});
}
if (item.event == 'MESSAGE_END') {
topicId.value = item.topicId;
conversationId = item.conversationId;
uuid.value = item.conversationId;
requestId.value = item.requestId;
handleStop();
}
if (item.event == 'FLOW_FINISHED') {
//update-begin---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程执行失败了但是没提示---
if(item.data && !item.data.success){
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
handleStop();
return "";
}
//update-end---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程执行失败了但是没提示---
topicId.value = item.topicId;
conversationId = item.conversationId;
uuid.value = item.conversationId;
requestId.value = item.requestId;
handleStop();
}
if (item.event == 'ERROR') {
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
handleStop();
return "";
}
//update-begin---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
if(item.event === "NODE_STARTED"){
let aiText = "";
if(item.data.type === 'llm'){
aiText = "正在构建响应内容";
}
if(item.data.type === 'knowledge'){
aiText = "正在对知识库进行深度检索";
}
if(item.data.type === 'classifier'){
aiText = "正在分类";
}
if(item.data.type === 'code'){
aiText = "正在实施代码运行操作";
}
if(item.data.type === 'subflow'){
aiText = "正在运行子流程";
}
if(item.data.type === 'enhanceJava'){
aiText = "正在执行java增强";
}
if(item.data.type === 'http'){
aiText = "正在发送http请求";
}
//更新聊天信息
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: aiText,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
}
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
return { returnText, conversationId };
}
//上传文件列表集合
const uploadUrlList = ref<any>([]);
//文件集合
const fileInfoList = ref<any>([]);
/**
* 文件上传回调事件
* @param info
*/
function handleChange(info) {
let { fileList, file } = info;
fileInfoList.value = fileList;
if (file.status === 'error') {
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
}
if (file.status === 'done') {
uploadUrlList.value.push(file.response.message);
}
}
/**
* 获取图片地址
*
* @param url
*/
function getImage(url) {
return getFileAccessHttpUrl(url);
}
/**
* 上传前事件
*/
function beforeUpload(file) {
var fileType = file.type;
if (fileType === 'image') {
if (fileType.indexOf('image') < 0) {
message.warning('请上传图片');
return false;
}
}
return true;
}
/**
* 删除图片
*/
function deleteImage(index) {
uploadUrlList.value.splice(index,1);
fileInfoList.value.splice(index,1);
}
/**
* 图片预览
* @param url
*/
function handlePreview(url){
const onImgLoad = ({ index, url, dom }) => {
console.log(`${index + 1}张图片已加载URL为${url}`, dom);
};
let imageList = [getImage(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
//监听开场白
watch(
() => props.prologue,
(val) => {
try {
if (val) {
topChat(val);
}
} catch (e) {}
}
);
//监听开场白预制问题
watch(
() => props.presetQuestion,
(val) => {
topChat(props.prologue);
}
);
//监听应用信息
watch(
() => props.formState,
(val) => {
try {
if (val) {
appData.value = val;
}
} catch (e) {}
},
{ deep: true, immediate: true }
);
//监听历史信息
watch(
() => props.historyData,
(val) => {
try {
//update-begin---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
if (val && val.length > 0) {
chatData.value = cloneDeep(val);
if(chatData.value[0]){
topicId.value = chatData.value[0].topicId
}
}else{
chatData.value = [];
headerTitle.value = props.chatTitle;
}
if(props.prologue && props.chatTitle){
topChat(props.prologue)
}
} catch (e) {
console.log(e)
}
//update-end---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
},
{ deep: true, immediate: true }
);
onMounted(() => {
scrollToBottom();
uploadUrlList.value = [];
fileInfoList.value = [];
});
</script>
<style lang="less" scoped>
.chatWrap {
width: 100%;
height: 100%;
padding: 20px;
.content {
height: 100%;
width: 100%;
background: #fff;
display: flex;
flex-direction: column;
}
}
.main {
flex: 1;
min-height: 0;
.scrollArea {
overflow-y: auto;
height: 100%;
}
.chatContentArea {
padding: 10px;
}
}
.emptyArea {
display: flex;
justify-content: center;
align-items: center;
color: #d4d4d4;
}
.stopArea {
display: flex;
justify-content: center;
padding: 10px 0;
}
.footer {
display: flex;
flex-direction: column;
padding: 6px 16px;
.topArea {
padding-left: 6%;
margin-bottom: 6px;
}
.bottomArea {
display: flex;
align-items: center;
.ant-input {
margin: 0 8px;
}
.ant-input,
.ant-btn {
height: 36px;
}
textarea.ant-input {
padding-top: 6px;
padding-bottom: 6px;
}
.contextBtn,
.delBtn {
padding: 0;
width: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.delBtn {
margin-right: 8px;
}
.contextBtn {
color: #a8071a;
&.enabled {
color: @primary-color;
}
font-size: 18px;
}
.sendBtn {
font-size: 18px;
width: 36px;
display: flex;
padding: 8px;
align-items: center;
}
.stopBtn {
width: 32px;
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
}
}
}
:deep(.chatgpt .markdown-body) {
background-color: #f4f6f8;
}
:deep(.ant-message) {
top: 50% !important;
}
.header-title{
color: #101828;
font-size: 16px;
font-weight: 400;
padding-bottom: 8px;
margin-left: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-textarea{
display: flex;
align-items: center;
width: 100%;
border-radius: 15px;
border-style: solid;
border-width: 1px;
flex-direction: column;
transition: width 0.3s;
border-color: rgba(68,83,130,0.2);
.textarea-top{
border-bottom: 1px solid #f0f0f5;
padding: 12px 28px;
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 10px;
.top-image{
display: flex;
img{
border-radius: 8px;
cursor: pointer;
height: 60px;
position: relative;
width: 60px;
}
}
}
.textarea-bottom{
display: flex;
flex-direction: row;
align-items: center;
flex: 1 1;
min-height: 48px;
position: relative;
padding: 8px 8px 8px 10px;
width: 100%;
}
}
.chat-textarea:hover{
border-color: rgba(59,130,246,0.5)
}
.textarea-active{
border-color: rgba(59,130,246,0.5) !important;
}
:deep(.ant-divider-vertical){
margin: 0 2px;
}
.upload-icon{
cursor: pointer;
position: absolute;
background-color: #1D1C23;
color: white;
border-radius: 50%;
padding: 4px;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-left: 44px;
margin-top: -4px;
}
.top-image:hover{
.upload-icon{
display: flex;
}
}
</style>
<style lang="less">
.ai-chat-modal{
z-index: 9999 !important;
}
.ai-chat-message{
z-index: 9999 !important;
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
<div class="avatar">
<img v-if="inversion === 'user'" :src="avatar()" />
<img v-else :src="getAiImg()">
</div>
<div class="content">
<p class="date">
<span v-if="inversion === 'ai'" style="margin-right: 10px">{{appData.name || 'AI助手'}}</span>
<span>{{ dateTime }}</span>
</p>
<div v-if="inversion === 'user' && images && images.length>0" class="images">
<div v-for="(item,index) in images" :key="index" class="image" @click="handlePreview(item)">
<img :src="getImageUrl(item)"/>
</div>
</div>
<div class="msgArea">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
</div>
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
<span>{{item.descr}}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import chatText from './chatText.vue';
import defaultAvatar from '/@/assets/images/ai/avatar.jpg';
import { useUserStore } from '/@/store/modules/user';
import defaultImg from '../img/ailogo.png';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images']);
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { createImgPreview } from "@/components/Preview";
const { userInfo } = useUserStore();
const avatar = () => {
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
};
const emit = defineEmits(['send']);
const getAiImg = () => {
return getFileAccessHttpUrl(props.appData?.icon) || defaultImg;
};
/**
* 预设问题点击事件
*
*/
function presetQuestionClick(descr) {
emit("send",descr)
}
/**
* 获取图片
*
* @param item
*/
function getImageUrl(item) {
let url = item;
if(item.hasOwnProperty('url')){
url = item.url;
}
if(item.hasOwnProperty('base64Data') && item.base64Data){
return item.base64Data;
}
return getFileAccessHttpUrl(url);
}
/**
* 图片预览
* @param url
*/
function handlePreview(url){
const onImgLoad = ({ index, url, dom }) => {
console.log(`${index + 1}张图片已加载URL为${url}`, dom);
};
let imageList = [getImageUrl(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
</script>
<style lang="less" scoped>
.chat {
display: flex;
margin-bottom: 1.5rem;
&.self {
flex-direction: row-reverse;
.avatar {
margin-right: 0;
margin-left: 10px;
}
.msgArea {
flex-direction: row-reverse;
}
.date {
text-align: right;
}
}
}
.avatar {
flex: none;
margin-right: 10px;
img {
width: 34px;
height: 34px;
border-radius: 50%;
overflow: hidden;
}
svg {
font-size: 28px;
}
}
.content {
.date {
color: #b4bbc4;
font-size: 0.75rem;
margin-bottom: 10px;
}
.msgArea {
display: flex;
}
}
.question{
margin-top: 10px;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
background-color: #ffffff;
font-size: 0.875rem;
line-height: 1.25rem;
cursor: pointer;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.images{
margin-bottom: 10px;
flex-wrap: wrap;
display: flex;
gap: 10px;
.image{
width: 120px;
height: 80px;
cursor: pointer;
img{
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="textWrap" :class="[inversion ? 'self' : 'chatgpt']" ref="textRef">
<div v-if="!inversion">
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
<div v-if="inversion != 'user'">
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
</div>
<div v-else class="msg" v-text="text" />
@ -13,6 +13,9 @@
import mdKatex from '@traptitech/markdown-it-katex';
import mila from 'markdown-it-link-attributes';
import hljs from 'highlight.js';
import './style/github-markdown.less';
import './style/highlight.less';
import './style/style.less';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
const textRef = ref();
@ -34,7 +37,7 @@
const text = computed(() => {
const value = props.text ?? '';
if (!props.inversion) return mdi.render(value);
if (props.inversion != 'user') return mdi.render(value);
return value;
});

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