Compare commits

...

2 Commits

Author SHA1 Message Date
xueyinfei c7fe707424 Merge remote-tracking branch 'origin/master' 2 months ago
xueyinfei b789d32e0c 智能问答 2 months ago
  1. 147
      vue-fastapi-backend/module_admin/controller/data_asset_controller.py
  2. 166
      vue-fastapi-backend/module_admin/dao/data_asset_dao.py
  3. 12
      vue-fastapi-backend/module_admin/dao/data_ast_content_dao.py
  4. 56
      vue-fastapi-backend/module_admin/entity/do/data_ast_content_do.py
  5. 96
      vue-fastapi-backend/module_admin/entity/vo/data_asset_vo.py
  6. 176
      vue-fastapi-backend/module_admin/service/data_asset_service.py
  7. 2
      vue-fastapi-backend/module_admin/service/data_ast_content_service.py
  8. 2
      vue-fastapi-backend/server.py
  9. 1
      vue-fastapi-frontend/package.json
  10. 7
      vue-fastapi-frontend/src/api/aichat/aichat.js
  11. 25
      vue-fastapi-frontend/src/api/dataAsset/assetDetail.js
  12. 6
      vue-fastapi-frontend/src/components/codemirror/SQLCodeMirror.vue
  13. 2
      vue-fastapi-frontend/src/utils/request.js
  14. 7
      vue-fastapi-frontend/src/views/aichat/MdRenderer.vue
  15. 2
      vue-fastapi-frontend/src/views/aichat/aichat.vue
  16. 566
      vue-fastapi-frontend/src/views/dataAsset/assetDetail/index.vue
  17. 66
      vue-fastapi-frontend/src/views/dataint/dataquery/DataQueryButton.vue
  18. 514
      vue-fastapi-frontend/src/views/dataint/dataquery/dataquerychat.vue
  19. 400
      vue-fastapi-frontend/src/views/dataint/dataquery/index.vue
  20. 5
      vue-fastapi-frontend/src/views/meta/metaInfo/index.vue
  21. 6
      vue-fastapi-frontend/vite.config.js

147
vue-fastapi-backend/module_admin/controller/data_asset_controller.py

@ -0,0 +1,147 @@
from fastapi import APIRouter, Depends, Form, Request
from sqlalchemy.ext.asyncio import AsyncSession
from config.get_db import get_db
from module_admin.aspect.interface_auth import CheckUserInterfaceAuth
from module_admin.service.login_service import LoginService
from module_admin.service.data_asset_service import DataAssetService
from module_admin.entity.vo.data_asset_vo import DataAssetBatchModel, DataAssetPageQueryModel, DataAssetResultModel, DataAssetSearchModel
from utils.log_util import logger
from utils.page_util import PageResponseModel
from utils.response_util import ResponseUtil
from typing import List
from config.enums import BusinessType
from utils.common_util import bytes2file_response
from module_admin.annotation.log_annotation import Log
dataAssetController = APIRouter(prefix='/system/dataAsset')
@dataAssetController.get('/list', response_model=PageResponseModel)
async def get_data_asset_list(
request: Request,
data_asset_page_query: DataAssetPageQueryModel = Depends(DataAssetPageQueryModel.as_query),
query_db: AsyncSession = Depends(get_db),
):
"""
获取数据资产信息列表
:param request: 请求对象
:param data_asset_page_query: 查询参数
:param query_db: 数据库会话
:return: 数据资产信息列表
"""
# 获取分页数据
data_asset_page_query_result = await DataAssetService.get_data_asset_list_services(
query_db, data_asset_page_query, is_page=True
)
logger.info('获取数据资产信息列表成功')
return ResponseUtil.success(model_content=data_asset_page_query_result)
@dataAssetController.post('/batch')
async def batch_process_data_asset(
request: Request,
batch_data: DataAssetBatchModel,
query_db: AsyncSession = Depends(get_db),
):
"""
批量处理数据资产信息新增/修改/删除
:param request: 请求对象
:param batch_data: 批量处理数据
:param query_db: 数据库会话
:return: 处理结果
"""
try:
# 批量处理数据资产
result = await DataAssetService.batch_process_data_asset_services(query_db, batch_data)
# 记录处理结果
logger.info(f'批量处理数据资产成功,成功: {result.success_count},失败: {result.failed_count}')
# 返回处理结果
return ResponseUtil.success(data=result)
except Exception as e:
logger.error(f'批量处理数据资产失败: {str(e)}')
return ResponseUtil.error(msg=f"处理失败: {str(e)}")
@dataAssetController.get('/sources', response_model=List[str])
async def get_data_asset_sources(
request: Request,
query_db: AsyncSession = Depends(get_db),
):
"""
获取所有数据资产来源列表
:param request: 请求对象
:param query_db: 数据库会话
:return: 数据资产来源列表
"""
sources = await DataAssetService.get_data_asset_sources_services(query_db)
logger.info('获取数据资产来源列表成功')
return ResponseUtil.success(data=sources)
@dataAssetController.get('/search', response_model=PageResponseModel)
async def search_data_assets(
request: Request,
search_params: DataAssetSearchModel = Depends(DataAssetSearchModel.as_query),
query_db: AsyncSession = Depends(get_db),
):
"""
综合查询数据资产信息
:param request: 请求对象
:param search_params: 查询参数
:param query_db: 数据库会话
:return: 查询结果
"""
# 将查询参数转换为字典,剔除None值
params_dict = {k: v for k, v in search_params.model_dump().items()
if v is not None and k not in ['page_num', 'page_size']}
# 执行查询
search_result = await DataAssetService.search_data_assets_services(
query_db,
params_dict,
search_params.page_num,
search_params.page_size
)
logger.info('综合查询数据资产信息成功')
return ResponseUtil.success(model_content=search_result)
# @dataAssetController.get('/search', response_model=PageResponseModel)
# async def search_data_assets(
# request: Request,
# search_params: DataAssetSearchModel = Depends(DataAssetSearchModel.as_query),
# query_db: AsyncSession = Depends(get_db),
# ):
@dataAssetController.post("/export")
@Log(title="数据资产", business_type=BusinessType.EXPORT)
async def export_data_assets(
request: Request,
data_asset_query: DataAssetPageQueryModel = Form(),
query_db: AsyncSession = Depends(get_db),
):
"""
导出数据资产信息
:param request: 请求对象
:param data_asset_query: 查询参数
:param query_db: 数据库会话
:return: Excel文件流
"""
data_assets = await DataAssetService.get_data_asset_list_services(
query_db, data_asset_query, is_page=False
)
excel_data = await DataAssetService.export_data_asset_list_services(data_assets)
logger.info('导出成功')
return ResponseUtil.streaming(data=bytes2file_response(excel_data))

166
vue-fastapi-backend/module_admin/dao/data_asset_dao.py

@ -0,0 +1,166 @@
from sqlalchemy import select, update, delete, insert
from sqlalchemy.ext.asyncio import AsyncSession
from module_admin.entity.do.data_ast_content_do import DataAssetInfo
from module_admin.entity.vo.data_asset_vo import DataAssetItemModel, DataAssetPageQueryModel
from utils.page_util import PageUtil
from typing import List, Dict, Any
class DataAssetDao:
"""
数据资产信息模块数据库操作层
"""
@classmethod
async def get_data_asset_list(cls, db: AsyncSession, query_object: DataAssetPageQueryModel, is_page: bool = False):
"""
根据查询参数获取数据资产信息列表
:param db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 数据资产信息列表对象
"""
query = (
select(DataAssetInfo)
.where(
DataAssetInfo.ast_no == query_object.ast_no if query_object.ast_no else True,
DataAssetInfo.data_ast_eng_name.like(f'%{query_object.data_ast_eng_name}%') if query_object.data_ast_eng_name else True,
DataAssetInfo.data_ast_cn_name.like(f'%{query_object.data_ast_cn_name}%') if query_object.data_ast_cn_name else True,
DataAssetInfo.data_ast_type == query_object.data_ast_type if query_object.data_ast_type else True,
DataAssetInfo.data_ast_stat == query_object.data_ast_stat if query_object.data_ast_stat else True,
DataAssetInfo.data_ast_src == query_object.data_ast_src if query_object.data_ast_src else True,
)
.order_by(DataAssetInfo.data_ast_estb_time.desc())
)
data_asset_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page)
return data_asset_list
@classmethod
async def get_data_asset_by_ast_no(cls, db: AsyncSession, ast_no: str):
"""
根据资产编号获取数据资产信息
:param db: orm对象
:param ast_no: 资产编号
:return: 数据资产信息对象
"""
data_asset = (
await db.execute(select(DataAssetInfo).where(DataAssetInfo.ast_no == ast_no))
).scalars().first()
return data_asset
@classmethod
async def insert_data_asset(cls, db: AsyncSession, data_asset: Dict[str, Any]):
"""
插入数据资产信息
:param db: orm对象
:param data_asset: 数据资产信息字典
:return: 插入的数据资产对象
"""
db_data_asset = DataAssetInfo(**data_asset)
db.add(db_data_asset)
await db.flush()
return db_data_asset
@classmethod
async def update_data_asset(cls, db: AsyncSession, ast_no: str, data_asset: Dict[str, Any]):
"""
更新数据资产信息
:param db: orm对象
:param ast_no: 资产编号
:param data_asset: 数据资产信息字典
:return: 更新结果
"""
result = await db.execute(
update(DataAssetInfo)
.where(DataAssetInfo.ast_no == ast_no)
.values(**data_asset)
)
return result.rowcount
@classmethod
async def delete_data_asset(cls, db: AsyncSession, ast_no: str):
"""
删除数据资产信息
:param db: orm对象
:param ast_no: 资产编号
:return: 删除结果
"""
result = await db.execute(
delete(DataAssetInfo)
.where(DataAssetInfo.ast_no == ast_no)
)
return result.rowcount
@classmethod
async def get_data_asset_sources(cls, db: AsyncSession):
"""
获取所有数据资产来源列表
:param db: orm对象
:return: 数据资产来源列表
"""
# 使用distinct查询不重复的数据资产来源
result = await db.execute(
select(DataAssetInfo.data_ast_src)
.distinct()
.where(DataAssetInfo.data_ast_src.is_not(None))
.order_by(DataAssetInfo.data_ast_src)
)
sources = result.scalars().all()
return sources
@classmethod
async def search_data_assets(cls, db: AsyncSession, search_params: dict, is_page: bool = False, page_num: int = 1, page_size: int = 10):
"""
综合查询数据资产信息
:param db: orm对象
:param search_params: 查询参数字典
:param is_page: 是否分页
:param page_num: 页码
:param page_size: 每页大小
:return: 查询结果
"""
query = select(DataAssetInfo)
# 构建查询条件
conditions = []
# 名称查询(支持英文名称和中文名称)
if search_params.get('name'):
name_keyword = f"%{search_params['name']}%"
conditions.append(
(DataAssetInfo.data_ast_eng_name.like(name_keyword)) |
(DataAssetInfo.data_ast_cn_name.like(name_keyword))
)
# 数据资产类型
if search_params.get('data_ast_type'):
conditions.append(DataAssetInfo.data_ast_type == search_params['data_ast_type'])
# 资产应用场景
if search_params.get('data_ast_screen'):
conditions.append(DataAssetInfo.data_ast_screen == search_params['data_ast_screen'])
# 数据资产标签
if search_params.get('data_ast_clas'):
conditions.append(DataAssetInfo.data_ast_clas.like(f"%{search_params['data_ast_clas']}%"))
# 数据资产来源
if search_params.get('data_ast_src'):
conditions.append(DataAssetInfo.data_ast_src == search_params['data_ast_src'])
# 将所有条件添加到查询
for condition in conditions:
query = query.where(condition)
# 排序
query = query.order_by(DataAssetInfo.data_ast_estb_time.desc())
# 分页查询
result = await PageUtil.paginate(db, query, page_num, page_size, is_page)
return result

12
vue-fastapi-backend/module_admin/dao/data_ast_content_dao.py

@ -4,7 +4,7 @@ from sqlalchemy.orm import aliased
from sqlalchemy import delete, func, not_, select, update, or_, and_, desc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError
from module_admin.entity.do.data_ast_content_do import DataAstContent,DataAstContentRela,DataAstInfo,DataAstBookmarkRela,DataAstIndx
from module_admin.entity.do.data_ast_content_do import DataAstContent,DataAstContentRela,DataAssetInfo,DataAstBookmarkRela,DataAstIndx
from module_admin.entity.do.user_do import SysUser
from module_admin.entity.vo.data_ast_content_vo import DataCatalogPageQueryModel, DeleteDataCatalogModel,DataCatalogChild,DataAstBookmarkRelaRequest,DataAstIndxRequest,DataAstIndxResponse
from utils.page_util import PageUtil
@ -151,7 +151,7 @@ class DataCatalogDAO:
async def get_catalog_list(cls, db: AsyncSession, query_object: DataCatalogPageQueryModel, user_id: int, user_name: str, is_page: bool = False):
# 创建别名对象
t1 = aliased(DataAstContentRela, name='t1')
t2 = aliased(DataAstInfo, name='t2')
t2 = aliased(DataAssetInfo, name='t2')
t3 = aliased(DataAstBookmarkRela, name='t3')
# 构建子查询1(对应subquery_t1)
@ -447,7 +447,7 @@ class DataCatalogDAO:
:return: 去重后的数据资产树数据
"""
# 创建别名对象
a = aliased(DataAstInfo, name='a')
a = aliased(DataAssetInfo, name='a')
b = aliased(DataAstContentRela, name='b')
# 构建查询
@ -714,7 +714,7 @@ class DataCatalogDAO:
"""
# 创建别名对象
t1 = aliased(DataAstContentRela, name='t1')
t2 = aliased(DataAstInfo, name='t2')
t2 = aliased(DataAssetInfo, name='t2')
t3 = aliased(DataAstBookmarkRela, name='t3')
@ -756,10 +756,10 @@ class DataCatalogDAO:
# 获取通过资产序号,获取资产编号
para_ast_no = (
select(
DataAstInfo.ast_no
DataAssetInfo.ast_no
)
.where(
DataAstInfo.data_ast_no == catalog['data_ast_no']
DataAssetInfo.data_ast_no == catalog['data_ast_no']
)
)

56
vue-fastapi-backend/module_admin/entity/do/data_ast_content_do.py

@ -1,6 +1,9 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String, Text, DateTime, ForeignKey, Date, Double
from config.database import Base
from sqlalchemy.sql import func
# 定义数据资产目录表
class DataAstContent(Base):
@ -34,25 +37,46 @@ class DataAstContentRela(Base):
class DataAstInfo(Base):
__tablename__ = "t_data_ast_info"
data_ast_no = Column(Integer, primary_key=True, autoincrement=True, comment='数据资产编号')
data_ast_eng_name = Column(String(255), nullable=False, comment='数据资产英文名称')
data_ast_cn_name = Column(String(255), nullable=True, comment='数据资产中文名称')
data_ast_type = Column(String(50), nullable=True, comment='数据资产类型')
data_ast_stat = Column(String(50), nullable=True, comment='数据资产状态')
data_ast_desc = Column(Text, nullable=True, comment='数据资产描述/说明')
data_ast_clas = Column(String(255), nullable=True, comment='数据资产标签')
data_ast_cont = Column(Text, nullable=True, comment='数据资产内容')
data_ast_faq = Column(Text, nullable=True, comment='数据资产常见问题')
data_ast_estb_time = Column(DateTime, default=datetime.now, comment='数据资产建立时间')
data_ast_upd_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment='数据资产更新时间')
data_ast_src = Column(String(255), nullable=True, comment='数据资产来源')
# class DataAstInfo(Base):
# __tablename__ = "t_data_ast_info"
# data_ast_no = Column(Integer, primary_key=True, autoincrement=True, comment='数据资产编号')
# data_ast_eng_name = Column(String(255), nullable=False, comment='数据资产英文名称')
# data_ast_cn_name = Column(String(255), nullable=True, comment='数据资产中文名称')
# data_ast_type = Column(String(50), nullable=True, comment='数据资产类型')
# data_ast_stat = Column(String(50), nullable=True, comment='数据资产状态')
# data_ast_desc = Column(Text, nullable=True, comment='数据资产描述/说明')
# data_ast_clas = Column(String(255), nullable=True, comment='数据资产标签')
# data_ast_cont = Column(Text, nullable=True, comment='数据资产内容')
# data_ast_faq = Column(Text, nullable=True, comment='数据资产常见问题')
# data_ast_estb_time = Column(DateTime, default=datetime.now, comment='数据资产建立时间')
# data_ast_upd_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment='数据资产更新时间')
# data_ast_src = Column(String(255), nullable=True, comment='数据资产来源')
# ast_no = Column(Integer, nullable=True, comment='资产编号')
class DataAssetInfo(Base):
"""
数据资产信息表
"""
__tablename__ = 't_data_ast_info'
data_ast_no = Column(Integer, primary_key=True, autoincrement=True, comment='资产序号')
data_ast_eng_name = Column(String(255), nullable=False, comment='表英文名称')
data_ast_cn_name = Column(String(255), nullable=True, comment='表中文名称')
data_ast_type = Column(String(50), nullable=True, comment='表类型(表、报表、数据应用等)')
data_ast_stat = Column(String(50), nullable=True, comment='表状态(有效/废弃)')
data_ast_desc = Column(Text, nullable=True, comment='资产描述/说明')
data_ast_screen = Column(String(255), nullable=True, comment='资产应用场景')
data_ast_scren_clas = Column(String(255), nullable=True, comment='应用场景分类')
data_ast_clas = Column(String(255), nullable=True, comment='资产标签')
data_ast_cont = Column(Text, nullable=True, comment='资产内容')
data_ast_faq = Column(Text, nullable=True, comment='资产常见问题')
data_ast_estb_time = Column(DateTime, nullable=True, server_default=func.now(), comment='资产建立时间')
data_ast_upd_time = Column(DateTime, nullable=True, server_default=func.now(), onupdate=func.now(), comment='资产更新时间')
data_ast_src = Column(String(255), nullable=True, comment='资产来源')
ast_no = Column(Integer, nullable=True, comment='资产编号')
class DataAstBookmarkRela(Base):
__tablename__ = "t_data_ast_bookmark_rela"

96
vue-fastapi-backend/module_admin/entity/vo/data_asset_vo.py

@ -0,0 +1,96 @@
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Optional
from module_admin.annotation.pydantic_annotation import as_query
class DataAssetItemModel(BaseModel):
"""
数据资产信息项模型
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
ast_no: int = Field(default=None, alias="astNo", description='数据资产编号(必填)')
data_ast_eng_name: str = Field(default=None, alias="dataAstEngName", description='数据资产英文名称(必填)')
data_ast_cn_name: str = Field(default=None, alias="dataAstCnName", description='数据资产中文名称')
data_ast_type: str = Field(default=None, alias="dataAstType", description='数据资产类型(表/报表/数据应用)(必填)')
data_ast_stat: str = Field(default=None, alias="dataAstStat", description='数据资产状态(有效/废弃)(必填)')
data_ast_desc: str = Field(default=None, alias="dataAstDesc", description='数据资产描述')
data_ast_screen: str = Field(default=None, alias="dataAstScreen", description='资产应用场景(API/智能问答等)')
data_ast_scren_clas: str = Field(default=None, alias="dataAstScrenClas", description='应用场景分类')
data_ast_clas: str = Field(default=None, alias="dataAstClas", description='数据资产标签')
data_ast_cont: str = Field(default=None, alias="dataAstCont", description='数据资产内容')
data_ast_faq: str = Field(default=None, alias="dataAstFaq", description='数据资产常见问题')
data_ast_src: str = Field(default=None, alias="dataAstSrc", description='数据资产来源(必填)')
version_no: str = Field(default="1", alias="versionNo", description='版本号(默认最新)')
ctrl_flag: str = Field(default=None, alias="ctrlFlag", description='操作类型 1:插入 2:删除 3:更新')
class DataAssetBatchModel(BaseModel):
"""
数据资产批量操作模型
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
data_assets: List[DataAssetItemModel] = Field(default=[], alias="dataAssets", description='数据资产列表')
class DataAssetResultModel(BaseModel):
"""
数据资产操作结果模型
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
success_count: int = Field(default=0, alias="successCount", description='成功数量')
failed_count: int = Field(default=0, alias="failedCount", description='失败数量')
success_items: List[str] = Field(default=[], alias="successItems", description='成功项')
failed_items: List[dict] = Field(default=[], alias="failedItems", description='失败项及原因')
class DataAssetQueryModel(BaseModel):
"""
数据资产查询模型
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
ast_no: Optional[str] = Field(default=None, alias="astNo", description='资产编号')
data_ast_eng_name: Optional[str] = Field(default=None, alias="dataAstEngName", description='资产英文名称')
data_ast_cn_name: Optional[str] = Field(default=None, alias="dataAstCnName", description='资产中文名称')
data_ast_type: Optional[str] = Field(default=None, alias="dataAstType", description='资产类型')
data_ast_stat: Optional[str] = Field(default=None, alias="dataAstStat", description='资产状态')
data_ast_src: Optional[str] = Field(default=None, alias="dataAstSrc", description='资产来源')
begin_time: Optional[str] = Field(default=None, alias="beginTime", description='开始时间')
end_time: Optional[str] = Field(default=None, alias="endTime", description='结束时间')
@as_query
class DataAssetPageQueryModel(DataAssetQueryModel):
"""
数据资产分页查询模型
"""
page_num: int = Field(default=1, alias="pageNum", description='当前页码')
page_size: int = Field(default=10, alias="pageSize", description='每页记录数')
@as_query
class DataAssetSearchModel(BaseModel):
"""
数据资产综合查询模型
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
name: Optional[str] = Field(default=None, alias="name", description='资产名称(英文名或中文名)')
data_ast_type: Optional[str] = Field(default=None, alias="dataAstType", description='资产类型')
data_ast_screen: Optional[str] = Field(default=None, alias="dataAstScreen", description='资产应用场景')
data_ast_clas: Optional[str] = Field(default=None, alias="dataAstClas", description='资产标签')
data_ast_src: Optional[str] = Field(default=None, alias="dataAstSrc", description='资产来源')
page_num: int = Field(default=1, alias="pageNum", description='当前页码')
page_size: int = Field(default=10, alias="pageSize", description='每页记录数')

176
vue-fastapi-backend/module_admin/service/data_asset_service.py

@ -0,0 +1,176 @@
from sqlalchemy.ext.asyncio import AsyncSession
from module_admin.dao.data_asset_dao import DataAssetDao
from module_admin.entity.vo.data_asset_vo import DataAssetItemModel, DataAssetBatchModel, DataAssetResultModel, DataAssetPageQueryModel
from exceptions.exception import ServiceException
from typing import Dict, Any, List
from utils.common_util import export_list2excel
class DataAssetService:
"""
数据资产信息模块服务层
"""
@classmethod
async def get_data_asset_list_services(
cls, query_db: AsyncSession, query_object: DataAssetPageQueryModel, is_page: bool = False
):
"""
获取数据资产信息列表service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 数据资产信息列表对象
"""
data_asset_list_result = await DataAssetDao.get_data_asset_list(query_db, query_object, is_page)
return data_asset_list_result
@classmethod
async def batch_process_data_asset_services(
cls, query_db: AsyncSession, batch_object: DataAssetBatchModel
):
"""
批量处理数据资产信息service
:param query_db: orm对象
:param batch_object: 批量处理参数对象
:return: 处理结果对象
"""
result = DataAssetResultModel()
for item in batch_object.data_assets:
try:
# 转换对象为字典,并移除ctrl_flag,version_no字段
data_dict = item.model_dump()
ctrl_flag = data_dict.pop("ctrl_flag")
version_no = data_dict.pop("version_no")
# 检查数据资产是否存在
existing_asset = await DataAssetDao.get_data_asset_by_ast_no(query_db, item.ast_no)
# 根据ctrl_flag进行不同的操作
if ctrl_flag == "1": # 插入
if existing_asset:
raise ServiceException(f"数据资产编号 {item.ast_no} 已存在,无法插入")
await DataAssetDao.insert_data_asset(query_db, data_dict)
result.success_count += 1
result.success_items.append(item.ast_no)
elif ctrl_flag == "2": # 删除
if not existing_asset:
raise ServiceException(f"数据资产编号 {item.ast_no} 不存在,无法删除")
await DataAssetDao.delete_data_asset(query_db, item.ast_no)
result.success_count += 1
result.success_items.append(item.ast_no)
elif ctrl_flag == "3": # 更新
if not existing_asset:
raise ServiceException(f"数据资产编号 {item.ast_no} 不存在,无法更新")
await DataAssetDao.update_data_asset(query_db, item.ast_no, data_dict)
result.success_count += 1
result.success_items.append(item.ast_no)
else:
raise ServiceException(f"未知的操作类型: {ctrl_flag}")
except Exception as e:
result.failed_count += 1
result.failed_items.append({"ast_no": item.ast_no, "error": str(e)})
# 提交事务
await query_db.commit()
return result
@classmethod
async def get_data_asset_sources_services(cls, query_db: AsyncSession):
"""
获取所有数据资产来源service
:param query_db: orm对象
:return: 数据资产来源列表
"""
sources = await DataAssetDao.get_data_asset_sources(query_db)
# 构造父节点
formatted_data = [
{
"name": "表数据资产",
"children": [
{
"id": str(index), # 转换为字符串类型id
"name": source # 原始来源名称
}
for index, source in enumerate(sources, 1) # 从1开始编号
]
},
{
"name": "接口数据资产",
"children": []
},
{
"name": "数据治理资产",
"children": []
}
]
return formatted_data
@classmethod
async def search_data_assets_services(
cls, query_db: AsyncSession, search_params: dict, page_num: int = 1, page_size: int = 10, is_page: bool = True
):
"""
综合查询数据资产信息service
:param query_db: orm对象
:param search_params: 查询参数字典
:param page_num: 页码
:param page_size: 每页大小
:param is_page: 是否分页
:return: 查询结果
"""
search_result = await DataAssetDao.search_data_assets(
query_db, search_params, is_page, page_num, page_size
)
return search_result
@staticmethod
async def export_data_asset_list_services(data_assets_list: list):
mapping_dict = {
"astNo": "资产编号",
"dataAstEngName": "表英文名称",
"dataAstCnName": "表中文名称",
"dataAstType": "表类型",
"dataAstStat": "表状态",
"dataAstDesc": "表描述",
"dataAstScreen": "资产应用场景",
"dataAstScrenClas": "应用场景分类",
"dataAstClas": "资产标签",
"dataAstCont": "资产内容",
"dataAstFaq": "资产常见问题",
"dataAstSrc": "资产来源",
"dataAstEstbTime": "资产建立时间",
"dataAstUpdTime": "资产更新时间"
}
# 1. 获取映射关系的键顺序(确保列顺序一致)
keys_order = list(mapping_dict.keys())
new_data = []
for item in data_assets_list:
item_dict = item.__dict__ if not isinstance(item, dict) else item
cleaned_item = {k: (v if v is not None else "") for k, v in item_dict.items()}
# 处理状态字段
cleaned_item["dataAstStat"] = "有效" if cleaned_item.get("dataAstStat") == "有效" else "废弃"
# 按mapping_dict的键顺序构建数据
mapped_item = {
mapping_dict[key]: cleaned_item.get(key, "")
for key in keys_order
}
new_data.append(mapped_item)
# 直接调用export_list2excel,只传入数据参数
return export_list2excel(new_data)

2
vue-fastapi-backend/module_admin/service/data_ast_content_service.py

@ -669,7 +669,7 @@ class DataCatalogService:
indx_list_dict = await DataCatalogDAO.get_data_ast_indx_list(query_db, query_object)
indx_names = [item["indx_name"] for item in indx_list_dict]
indx_vals = [item["indx_val"] for item in indx_list_dict]
CurrentConfig.ONLINE_HOST = "/assets/js/echarts.min.js"
CurrentConfig.ONLINE_HOST = "/assets/js/"
# 创建独立图表
pie = (

2
vue-fastapi-backend/server.py

@ -34,6 +34,7 @@ from module_admin.controller.fccbd_controller import fccbdController
from module_admin.controller.cdplb_controller import cdplbController
from module_admin.controller.sscf_controller import sscfController
from module_admin.controller.vecset_controller import vecsetController
from module_admin.controller.data_asset_controller import dataAssetController
from sub_applications.handle import handle_sub_applications
from utils.common_util import worship
from utils.log_util import logger
@ -101,6 +102,7 @@ controller_list = [
{'router': cdplbController, 'tags': ['智能数据-词典批量补充']},
{'router': sscfController, 'tags': ['智能数据-短句配置']},
{'router': vecsetController, 'tags': ['智能数据-全句配置']},
{'router': dataAssetController, 'tags': ['系统管理-数据资产详情']},
]
for controller in controller_list:

1
vue-fastapi-frontend/package.json

@ -29,6 +29,7 @@
"@vueuse/core": "10.11.0",
"ant-design-vue": "^4.1.1",
"axios": "0.28.1",
"codemirror": "^5.65.19",
"codemirror-editor-vue3": "^2.8.0",
"echarts": "5.5.1",
"element-plus": "2.8.0",

7
vue-fastapi-frontend/src/api/aichat/aichat.js

@ -48,3 +48,10 @@ export async function addChat(data) {
data: data
})
}
export async function postDataQuery(data) {
return request({
url: '/dataquery-api/chat/nl2sql_client_chat',
method: 'post',
data: data
})
}

25
vue-fastapi-frontend/src/api/dataAsset/assetDetail.js

@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getSearch(params) {
return request({
url: '/default-api/system/dataAsset/search',
method: 'get',
params,
})
}
export function batch(data) {
return request({
url: '/default-api/system/dataAsset/batch',
method: 'post',
data,
})
}
export function deptTreeSelect() {
return request({
url: '/default-api/system/dataAsset/sources',
method: 'get'
})
}

6
vue-fastapi-frontend/src/components/codemirror/SQLCodeMirror.vue

@ -59,11 +59,15 @@ const sqlOptions = {
lint: true, // json
}
const value = ref("")
watch(() => props.data,
watch(() => value.value,
(val) =>{
value.value = sqlFormatter.format(val)
}
)
onMounted(()=>{
value.value = props.data
}
)
</script>

2
vue-fastapi-frontend/src/utils/request.js

@ -17,7 +17,7 @@ const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
// baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000
timeout: 100000
})
// request拦截器

7
vue-fastapi-frontend/src/views/aichat/MdRenderer.vue

@ -22,6 +22,7 @@
</template>
<script setup>
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
import markdown from './markdown.vue'
import antvg6 from './antv-g6.vue'
import chatTable from './chatTable.vue'
@ -42,13 +43,13 @@ function fullscreenG6(data){
function downLoadTable(data){
const worksheet = XLSX.utils.json_to_sheet(data);
// 簿簿
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// Excel
XLSX.writeFile(workbook, '导出数据.xlsx');
const excelBuffer = XLSX.write(workbook,{bookType:'xlsx',type:'array'});
const excel = new Blob([excelBuffer], { type: 'application/octet-stream' });
saveAs(excel, '导出文件.xlsx');
}
</script>
<style lang="scss" scoped>

2
vue-fastapi-frontend/src/views/aichat/aichat.vue

@ -409,7 +409,7 @@ function formatDate(date) {
function downloadFile(file,bucket,sessionId){
let data = {file,bucket,sessionId}
proxy.download("aichat/file/download", {
proxy.download("/default-api/aichat/file/download", {
...data,
}, file);
}

566
vue-fastapi-frontend/src/views/dataAsset/assetDetail/index.vue

@ -3,82 +3,57 @@
<el-row :gutter="16">
<el-col :span="5">
<el-card shadow="never">
<el-input
v-model="filterText"
style="width: 100%"
placeholder="搜索系统名称"
>
<el-input v-model="filterText" style="width: 100%" placeholder="搜索系统名称">
<template #prefix>
<el-icon><Search /></el-icon>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-tree
style="margin-top: 10px"
ref="treeRef"
node-key="id"
:default-expand-all="true"
:highlight-current="true"
:expand-on-click-node="false"
:data="treeData"
:props="defaultProps"
:filter-node-method="filterNode"
:current-node-key="currentNode.id"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<el-tree style="margin-top: 10px" ref="treeRef" node-key="id" :default-expand-all="true"
:highlight-current="true" :expand-on-click-node="false" :data="treeData" :props="defaultProps"
:filter-node-method="filterNode" :current-node-key="currentNode.id" @node-click="handleNodeClick">
<!-- <template #default="{ node, data }">
<el-space :size="2">
<el-icon v-if="!data.isLeaf"><Folder /></el-icon>
<el-icon v-else><Document /></el-icon>
<el-icon v-if="!data.isLeaf">
<Folder />
</el-icon>
<el-icon v-else>
<Document />
</el-icon>
<span>{{ node.label }}</span>
</el-space>
</template>
</template> -->
</el-tree>
</el-card>
</el-col>
<el-col :span="19">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
label-width="68px"
>
<el-form-item label="名称">
<el-input
v-model="queryParams.name"
placeholder="请输入名称"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入名称" clearable style="width: 160px"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="类型" prop="dataAstType">
<el-select v-model="queryParams.dataAstType" placeholder="请选择" clearable style="width: 160px">
<el-option v-for="(item, index) in dataAstType_list" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-input
v-model="queryParams.name"
placeholder="请输入类型"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
<el-form-item label="标签" prop="dataAstClas">
<el-input v-model="queryParams.dataAstClas" placeholder="请输入" clearable style="width: 160px"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="标签">
<el-input
v-model="queryParams.name"
placeholder="请输入标签"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
<el-form-item label="应用场景" prop="dataAstScreen">
<el-select v-model="queryParams.dataAstScreen" placeholder="请选择" clearable style="width: 160px">
<el-option v-for="(item, index) in dataAstScreen_list" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"
>搜索</el-button
>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button
type="primary"
plain
@ -87,219 +62,173 @@
v-hasPermi="['dataAsset:assetDetail:add']"
>新增</el-button
>
</el-col>
</el-col> -->
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['dataAsset:assetDetail:edit']"
>修改</el-button
>
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
v-hasPermi="['dataAsset:assetDetail:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['dataAsset:assetDetail:remove']"
>删除</el-button
>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
v-hasPermi="['dataAsset:assetDetail:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['dataAsset:assetDetail:export']"
>导出</el-button
>
<el-button type="warning" plain icon="Download" @click="handleExport"
v-hasPermi="['dataAsset:assetDetail:export']">导出</el-button>
</el-col>
<right-toolbar
v-model:showSearch="showSearch"
@queryTable="getList"
:columns="columns"
></right-toolbar>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleSelectionChange"
>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column
label="编号"
align="center"
key=""
prop=""
v-if="columns[0].visible"
/>
<el-table-column
label="来源系统"
align="center"
key=""
prop=""
v-if="columns[1].visible"
/>
<el-table-column
label="英文名称"
align="center"
key=""
prop=""
v-if="columns[2].visible"
/>
<el-table-column
label="中文名称"
align="center"
key=""
prop=""
v-if="columns[3].visible"
/>
<el-table-column
label="类型"
align="center"
key=""
prop=""
v-if="columns[4].visible"
/>
<el-table-column
label="描述"
align="center"
key=""
prop=""
v-if="columns[5].visible"
/>
<el-table-column
label="标签"
align="center"
key=""
prop=""
v-if="columns[6].visible"
/>
<el-table-column
label="负责人"
align="center"
key=""
prop=""
v-if="columns[7].visible"
/>
<el-table-column
label="建立时间"
align="center"
key=""
prop=""
v-if="columns[8].visible"
/>
<el-table-column
label="更新时间"
align="center"
key=""
prop=""
v-if="columns[9].visible"
/>
<el-table-column
label="操作"
align="center"
width="100"
class-name="small-padding fixed-width"
>
<el-table-column label="资产编号" align="center" key="" prop="astNo" v-if="columns[0].visible" />
<el-table-column label="表来源系统" align="center" width="150" key="" prop="dataAstSrc" v-if="columns[1].visible" />
<el-table-column label="表英文名称" align="center" width="200" key="" prop="dataAstEngName" v-if="columns[2].visible" />
<el-table-column label="表中文名称" align="center" width="200" key="" prop="dataAstCnName" v-if="columns[3].visible" />
<el-table-column label="表类型" align="center" width="100" key="" prop="dataAstType" v-if="columns[4].visible" />
<el-table-column label="资产标签" align="center" width="150" key="" prop="dataAstClas" v-if="columns[5].visible" />
<el-table-column label="资产描述" align="center" width="150" key="" prop="dataAstDesc" v-if="columns[6].visible" />
<el-table-column label="资产应用场景" align="center" width="150" key="" prop="dataAstScreen" v-if="columns[7].visible" />
<el-table-column label="应用场景分类" align="center" width="150" key="" prop="dataAstScrenClas" v-if="columns[8].visible" />
<el-table-column label="常见问题" align="center" width="150" key="" prop="dataAstFaq" v-if="columns[9].visible" />
<el-table-column label="负责人" align="center" width="150" key="" prop="dataAstCont" v-if="columns[10].visible" />
<el-table-column label="建立时间" align="center" width="200" key="" prop="dataAstEstbTime" v-if="columns[11].visible" />
<el-table-column label="更新时间" align="center" width="200" key="" prop="dataAstUpdTime" v-if="columns[12].visible" />
<el-table-column label="操作" align="center" width="100" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['dataAsset:assetDetail:edit']"
></el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-hasPermi="['dataAsset:assetDetail:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['dataAsset:assetDetail:remove']"
></el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['dataAsset:assetDetail:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-col>
</el-row>
<!-- -->
<el-dialog title="编辑资产" v-model="open" width="600px" append-to-body>
<el-form :model="form" :rules="rules" ref="assetRef" label-width="120px">
<el-form-item label="资产编号" prop="astNo">
<el-input v-model="form.astNo" placeholder="请输入" />
</el-form-item>
<el-form-item label="资产英文名称" prop="dataAstEngName">
<el-input v-model="form.dataAstEngName" placeholder="请输入" />
</el-form-item>
<el-form-item label="资产中文名称" prop="dataAstCnName">
<el-input v-model="form.dataAstCnName" placeholder="请输入" />
</el-form-item>
<el-form-item label="资产类型" prop="dataAstType">
<el-select v-model="form.dataAstType" placeholder="请选择" clearable style="width: 100%">
<el-option label="表" value="表" />
<el-option label="报表" value="报表" />
</el-select>
</el-form-item>
<el-form-item label="资产状态" prop="dataAstStat">
<el-select v-model="form.dataAstStat" placeholder="请选择" clearable style="width: 100%">
<el-option label="有效" value="1" />
<el-option label="作废" value="0" />
</el-select>
</el-form-item>
<el-form-item label="资产应用场景" prop="dataAstScreen">
<el-select v-model="form.dataAstScreen" placeholder="请选择" clearable style="width: 100%">
<el-option label="智能助手" value="智能助手" />
<el-option label="API" value="API" />
</el-select>
</el-form-item>
<el-form-item label="应用场景分类" prop="dataAstScrenClas">
<el-select v-model="form.dataAstScrenClas" placeholder="请选择" clearable style="width: 100%">
<el-option v-for="(item, index) in dataAstScrenClas_list" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="资产标签" prop="dataAstClas">
<el-input v-model="form.dataAstClas" placeholder="请输入" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="资产描述" prop="dataAstDesc">
<el-input v-model="form.dataAstDesc" placeholder="请输入" />
</el-form-item>
<el-form-item label="资产内容" prop="dataAstCont">
<el-input v-model="form.dataAstCont" placeholder="请输入" />
</el-form-item>
<el-form-item label="资产常见问题" prop="dataAstFaq">
<el-input v-model="form.dataAstFaq" placeholder="请输入" />
</el-form-item>
<el-form-item label="数据资产来源" prop="dataAstSrc">
<el-tree-select v-model="form.dataAstSrc" :data="treeDataChildren"
:props="{ value: 'name', label: 'name', children: 'children' }" value-key="id" placeholder="请选择"
check-strictly />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="AssetDetail">
import { getSearch, batch, deptTreeSelect } from "@/api/dataAsset/assetDetail";
const { proxy } = getCurrentInstance()
const defaultProps = {
children: 'children',
label: 'name',
}
const currentNode = ref({})
const treeData = ref([
{
catalogId: '-1',
name: '系统',
isLeaf: 0,
children: [
{
parentId: '-1',
id: '1',
name: 'wind',
isLeaf: 1,
},
{
parentId: '-1',
id: '2',
name: 'O32',
isLeaf: 1,
},
],
},
])
const treeData = ref([])
const treeDataChildren = ref([]);
const handleNodeClick = (data) => {
currentNode.value = data
/** 查询部门下拉树结构 */
function getDeptTree() {
deptTreeSelect().then(response => {
treeData.value = response.data;
if (response.data && response.data[0] && response.data[0].children) {
treeDataChildren.value = response.data[0].children
}
});
};
const filterText = ref(undefined)
const treeRef = ref(null)
watch(filterText, (val) => {
treeRef.value.filter(val)
})
/** 节点单击事件 */
function handleNodeClick(data) {
queryParams.value.dataAstSrc = data.name;
handleQuery();
};
const filterText = ref('')
/** 根据名称筛选部门树 */
watch(filterText, val => {
proxy.$refs["treeRef"].filter(val);
});
/** 通过条件过滤节点 */
const filterNode = (value, data) => {
if (!value) {
return true
}
return data.catalogName.includes(value)
}
if (!value) return true;
return data.name.indexOf(value) !== -1;
};
const columns = ref([
{ key: 0, label: `编号`, visible: true },
{ key: 0, label: `资产编号`, visible: true },
{ key: 1, label: `来源系统`, visible: true },
{ key: 2, label: `英文名称`, visible: true },
{ key: 3, label: `中文名称`, visible: true },
{ key: 4, label: `类型`, visible: true },
{ key: 5, label: `描述`, visible: true },
{ key: 6, label: `标签`, visible: true },
{ key: 7, label: `负责人`, visible: true },
{ key: 8, label: `建立时间`, visible: true },
{ key: 9, label: `更新时间`, visible: true },
{ key: 5, label: `资产标签`, visible: true },
{ key: 6, label: `资产描述`, visible: true },
{ key: 7, label: `资产应用场景`, visible: true },
{ key: 8, label: `应用场景分类`, visible: true },
{ key: 9, label: `常见问题`, visible: true },
{ key: 10, label: `负责人`, visible: true },
{ key: 11, label: `建立时间`, visible: true },
{ key: 12, label: `更新时间`, visible: true },
])
const loading = ref(false)
const list = ref([])
@ -309,28 +238,191 @@ const queryParams = ref({
pageNum: 1,
pageSize: 10,
name: undefined,
dataAstType: undefined,
dataAstClas: undefined,
dataAstScreen: undefined,
dataAstSrc: undefined,
})
const getList = () => {}
const getList = () => {
loading.value = true;
getSearch(proxy.addDateRange(queryParams.value)).then(res => {
loading.value = false;
list.value = res.rows;
total.value = res.total;
});
}
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
const resetQuery = () => {
queryParams.value.dataAstSrc = undefined
proxy.resetForm('queryRef')
proxy.$refs.treeRef.setCurrentKey(null);
handleQuery()
}
const handleAdd = () => { }
const handleUpdate = (row) => {}
const handleDelete = (row) => {}
const handleExport = () => {}
const handleUpdate = (row) => {
open.value = true;
proxy.resetForm("assetRef");
form.value = row || ids.value[0];
}
const handleDelete = (row) => {
const asset = row ? [row] : ids.value;
let submitasset = [];
asset.map(item => {
item.ctrlFlag = "2"
submitasset.push(
{
"astNo": String(item.astNo),
"dataAstEngName": item.dataAstEngName,
"dataAstCnName": item.dataAstCnName,
"dataAstType": item.dataAstType,
"dataAstStat": item.dataAstStat,
"dataAstDesc": item.dataAstDesc,
"dataAstScreen": item.dataAstScreen,
"dataAstScrenClas": item.dataAstScrenClas,
"dataAstClas": item.dataAstClas?item.dataAstClas:"",
"dataAstCont": item.dataAstCont,
"dataAstFaq": item.dataAstFaq,
"dataAstSrc": item.dataAstSrc,
"versionNo": item.versionNo,
"ctrlFlag": item.ctrlFlag,
}
)
})
proxy.$modal.confirm('是否确认删除?').then(function () {
return batch({ dataAssets: submitasset });
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => { });
}
const handleExport = () => {
proxy.download("/system/dataAsset/export", {
...queryParams.value,
}, `dataAsset_${new Date().getTime()}.xlsx`);
}
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const handleSelectionChange = (selection) => {
ids.value = selection.map((item) => item.userId)
ids.value = selection
single.value = selection.length != 1
multiple.value = !selection.length
}
// const dataAstType_list = computed(() => {
// const unique = new Set();
// list.value.forEach(item => {
// if (item.dataAstType) {
// unique.add(item.dataAstType);
// }
// });
// return Array.from(unique);
// });
const dataAstType_list = computed(() => {
return ['表', '报表','其他应用'];
});
const dataAstClas_list = computed(() => {
const unique = new Set();
list.value.forEach(item => {
if (item.dataAstClas) {
unique.add(item.dataAstClas);
}
});
return Array.from(unique);
});
// const dataAstScreen_list = computed(() => {
// const unique = new Set();
// list.value.forEach(item => {
// if (item.dataAstScreen) {
// unique.add(item.dataAstScreen);
// }
// });
// return Array.from(unique);
// });
const dataAstScreen_list = computed(() => {
return ['智能助手', 'API'];
});
const dataAstStat_list = computed(() => {
const unique = new Set();
list.value.forEach(item => {
if (item.dataAstStat) {
unique.add(item.dataAstStat);
}
});
return Array.from(unique);
});
const dataAstScrenClas_list = computed(() => {
const unique = new Set();
list.value.forEach(item => {
if (item.dataAstScrenClas) {
unique.add(item.dataAstScrenClas);
}
});
return Array.from(unique);
});
const open = ref(false);
const data = reactive({
form: {},
rules: {
astNo: [{ required: true, message: "请输入", trigger: "blur" }],
dataAstEngName: [{ required: true, message: "请输入", trigger: "blur" }],
dataAstType: [{ required: true, message: "请选择", trigger: "blur" }],
dataAstStat: [{ required: true, message: "请选择", trigger: "blur" }],
}
});
const { form, rules } = toRefs(data);
function submitForm() {
proxy.$refs["assetRef"].validate(valid => {
if (valid) {
form.value.ctrlFlag = "3";
let submitform = {
"astNo": String(form.value.astNo),
"dataAstEngName": form.value.dataAstEngName,
"dataAstCnName": form.value.dataAstCnName,
"dataAstType": form.value.dataAstType,
"dataAstStat": form.value.dataAstStat,
"dataAstDesc": form.value.dataAstDesc,
"dataAstScreen": form.value.dataAstScreen?form.value.dataAstScreen:"",
"dataAstScrenClas": form.value.dataAstScrenClas?form.value.dataAstScrenClas:"",
"dataAstClas": form.value.dataAstClas?form.value.dataAstClas:"",
"dataAstCont": form.value.dataAstCont?form.value.dataAstCont:"",
"dataAstFaq": form.value.dataAstFaq?form.value.dataAstFaq:"",
"dataAstSrc": form.value.dataAstSrc,
"versionNo": form.value.versionNo,
"ctrlFlag": form.value.ctrlFlag,
}
batch({ dataAssets: [submitform] }).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
}
});
};
function cancel() {
open.value = false;
form.value = {};
proxy.resetForm("assetRef");
};
getDeptTree();
getList();
</script>

66
vue-fastapi-frontend/src/views/dataint/dataquery/DataQueryButton.vue

@ -0,0 +1,66 @@
<template>
<div>
<el-text type="info">
<span class="ml-4" style="margin-left: 4px">{{ datetimeFormat(data.time) }}</span>
</el-text>
</div>
<div v-if="data.type === 'answer'">
<span>
<el-tooltip effect="dark" content="编辑" placement="top" popper-class="operate-tooltip">
<el-button text @click="edit" style="padding: 0">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="下载" placement="top" popper-class="operate-tooltip">
<el-button text @click="download" style="padding: 0">
<el-icon><Download /></el-icon>
</el-button>
</el-tooltip>
</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import Clipboard from 'vue-clipboard3'
import { datetimeFormat } from '@/utils/time'
import {updateChat} from "@/api/aichat/aichat.js";
import {EditPen} from "@element-plus/icons-vue";
const { proxy } = getCurrentInstance();
const route = useRoute()
const {
params: { id }
} = route
const props = defineProps({
data: {
type: Object,
default: () => {}
},
index: Number
})
const thumbDownReason = ref("")
const visible = ref(false)
const emit = defineEmits([ 'edit', 'download'])
function edit() {
emit('edit')
}
function download(){
emit('download')
}
</script>
<style lang="scss">
.operate-tooltip {
z-index: 99999 !important;
}
</style>

514
vue-fastapi-frontend/src/views/dataint/dataquery/dataquerychat.vue

@ -15,18 +15,26 @@
<template v-for="(item, index) in chatList" :key="index">
<!-- 问题 -->
<div v-if="item.type === 'question'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
<div v-if="item.type === 'question'">
<div class="item-content mb-16 lighter" style="display:flex;justify-content:flex-end;gap:10px;margin-bottom: 16px;font-weight: 400">
<div>
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;">
{{ item.content }}
</div>
</div>
<div class="avatar">
<el-avatar style="width:30px;height: 30px;background: #3370FF">
<img src="@/assets/aichat/user-icon.svg" style="width: 30px" alt="" />
</el-avatar>
</div>
<div class="content">
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;">
{{ item.content }}
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-bottom: 16px;font-weight: 400">
<el-text type="info">
<span class="ml-4" style="margin-left: 4px">{{ datetimeFormat(item.time) }}</span>
</el-text>
</div>
</div>
<!-- 回答 -->
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
<div class="avatar">
@ -34,22 +42,107 @@
</div>
<div class="content">
<el-card shadow="always" class="dialog-card">
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
<el-form
:model="item"
ref="formRef"
label-width="125px">
<el-form-item label="原始问题:">
<el-input v-model="item.content.question" placeholder="请补充其它条件"></el-input>
</el-form-item>
<el-form-item v-if="item.content.query.length > 0" label="查询条件:">
<template v-for="filter in item.content.query">
<div v-if="filter.ct_type === 'mselect'" style="display:flex;width: 100%;margin-bottom: 10px">
<span style="width: 150px">{{filter.name + ": "}}</span>
<el-select v-model="filter.default_value" multiple style="width: 100%;" clearable>
<el-option v-for="opt in filter.options" :key="opt" :value="opt" :label="opt"></el-option>
</el-select>
</div>
<div v-if="filter.ct_type === 'datePicker'" style="display:flex;width: 100%;margin-bottom: 10px">
<span style="width: 150px">{{filter.name + ": "}}</span>
<el-date-picker
v-model="filter.default_value"
type="date"
placeholder="Pick a day"
/>
</div>
<div v-if="filter.ct_type === 'dateRangePicker'" style="display:flex;width: 100%;margin-bottom: 10px">
<span style="width: 150px">{{filter.name + ": "}}</span>
<el-date-picker
v-model="filter.dateRangeValue"
type="daterange"
range-separator="-"
start-placeholder="Start date"
end-placeholder="End date"
/>
</div>
<div v-if="filter.ct_type === 'radioGroup'" style="display:flex;width: 100%;margin-bottom: 10px">
<span style="width: 150px">{{filter.name + ": "}}</span>
<el-radio-group v-model="filter.default_value">
<el-radio :key="'radio'+opt" v-for="opt in filter.options" :value="opt">{{ opt }}</el-radio>
</el-radio-group>
</div>
<div v-if="filter.ct_type === 'input'" style="display:flex;width: 100%;margin-bottom: 10px">
<span style="width: 150px">{{filter.name + ": "}}</span>
<el-input style="width: calc(100% - 150px)" v-model="filter.default_value"/>
</div>
</template>
</el-form-item>
<el-form-item v-if="item.content.result.length > 0" label="输出结果:">
<span v-for="result in item.content.result">{{result.name}}</span>
</el-form-item>
<el-form-item label="数据结果:">
<template v-if="item.content.data">
<el-tabs type="border-card" v-model="item.content.tab" style="width: 100%" @tabChange="changeTab(item.content)">
<el-tab-pane label="数据结果" name="result">
<div style="max-height: calc(100vh - 400px);overflow: auto">
<el-table :data="item.content.data" style="height: 100%">
<el-table-column v-for="(val,key) in item.content.data[0]" :prop="key" :label="key"/>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="分析结果" name="analysis" v-if="item.content.html_content && item.content.html_content !== ''">
<iframe :srcdoc="item.content.html_content"
style="width: 100%;height: 300px;border: none;
overflow-y:hidden; overflow-x: auto"></iframe>
</el-tab-pane>
<el-tab-pane label="sql" name="sql">
<SQLCodeMirror ref="codemirror" v-if="item.content.tab === 'sql'" :data="item.content.sql.replace('\n',' ')"></SQLCodeMirror>
</el-tab-pane>
</el-tabs>
</template>
<template v-else>
<span>查询结果为空请重新提问或者点击编辑按钮辅助提问^_^</span>
</template>
</el-form-item>
</el-form>
</el-card>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<el-button
type="primary"
v-if="item.isStop && !item.isEnd"
@click="startChat(index)"
link
>重新生成
</el-button>
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link
>停止回答
</el-button>
<div class="flex-between" style="display: flex; justify-content: space-between; align-items: center;margin-top: 16px">
<DataQueryButton :data="item" @edit="edit(item)" @download="download(item)"></DataQueryButton>
</div>
</div>
</div>
<div v-if="item.type === 'answer_err'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
<div class="avatar">
<img src="@/assets/logo/logo2.png" height="30px" />
</div>
<div content="content" style="padding-left:40px">
<el-card shadow="always" class="dialog-card">
<span style="font-size: 14px">{{item.content}}</span>
</el-card>
<div class="flex-between" style="display: flex; justify-content: space-between; align-items: center;margin-top: 16px">
<DataQueryButton :data="item"></DataQueryButton>
</div>
</div>
</div>
<div v-if="item.type === 'loading'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
<div class="avatar">
<img src="@/assets/logo/logo2.png" height="30px" />
</div>
<div content="content" style="padding-left:40px">
<el-card shadow="always" class="dialog-card">
<span style="font-size: 14px">思考中...</span>
</el-card>
</div>
</div>
</template>
@ -61,22 +154,16 @@
<el-input
ref="quickInputRef"
v-model="inputValue"
:placeholder="'@选择机器人,Ctrl+Enter 换行,Enter发送'"
:placeholder="'Ctrl+Enter 换行,Enter发送'"
:autosize="{ minRows: 1, maxRows: 4 }"
type="textarea"
:maxlength="100000"
@input="handleInput"
@keydown.enter="sendChatHandle($event)"
/>
<div class="operate flex align-center">
<el-button
text
class="sent-button"
:disabled="isDisabledChart || loading"
@click="sendChatHandle"
>
<img v-show="isDisabledChart || loading" src="@/assets/aichat/icon_send.svg" alt="" />
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
<el-button text class="sent-button" :disabled="chatList.length>0 && chatList[chatList.length-1].type === 'loading'" @click="sendChatHandle">
<img v-show="chatList.length>0 && chatList[chatList.length-1].type === 'loading'" src="@/assets/aichat/icon_send.svg" alt="" />
<img v-show="chatList.length === 0 || chatList[chatList.length-1].type !== 'loading'" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
</el-button>
</div>
</div>
@ -85,24 +172,16 @@
</template>
<script setup>
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import MdRenderer from '@/views/aichat/MdRenderer.vue'
import {getToken} from "@/utils/auth.js";
import {postChatMessage} from "@/api/aichat/aichat.js"
import cache from "@/plugins/cache.js";
import {postDataQuery} from "@/api/aichat/aichat"
import { datetimeFormat } from '@/utils/time'
import {v4 as uuidv4} from "uuid";
import Cookies from "js-cookie";
import {addChat} from "@/api/aichat/aichat";
import SQLCodeMirror from "@/components/codemirror/SQLCodeMirror.vue";
import DataQueryButton from "@/views/dataint/dataquery/DataQueryButton.vue";
defineOptions({ name: 'AiChat' })
const route = useRoute()
const fullscreenG6Data = ref(null)
const showFullscreenG6Data = ref(false)
const {
params: { accessToken, id },
query: { mode }
} = route
const props = defineProps({
is_large: Boolean,
cookieSessionId: String,
data: {
type: Object,
@ -132,36 +211,27 @@ const loading = ref(false)
const inputValue = ref('')
const chartOpenId = ref('')
const chatList = ref([])
const answerList = ref([])
const controller = ref(null)
const popoverVisible = ref(false)
const currentMachine = ref([])
const currentFiles = ref([])
const upload = reactive({
//
open: false,
//
title: "",
//
isUploading: false,
//
headers: { Authorization: "Bearer " + getToken() },
//
url: import.meta.env.VITE_APP_BASE_API + "/aichat/upload",
data: {"sessionId":Cookies.get("chatSessionId")}
});
const isDisabledChart = computed(
() => !(inputValue.value.trim())
)
const emit = defineEmits(['scroll'])
const emit = defineEmits(['scroll','showEdit','download'])
function changeTab(content){
}
function setScrollBottom() {
//
scrollDiv.value.setScrollTop(getMaxHeight())
}
function edit(data){
emit('showEdit',data)
}
function download(data){
emit('download',data)
}
/**
* 滚动条距离最上面的高度
@ -188,25 +258,6 @@ const handleScroll = () => {
}
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
upload.isUploading = true;
};
const handleFileSuccess = (response, file, fileList) => {
currentFiles.value.push({file:response.data.file,bucket:response.data.bucket})
upload.open = false;
upload.isUploading = false;
proxy.$modal.msgSuccess(response.msg);
// proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "", { dangerouslyUseHTMLString: true });
};
/** 提交上传文件 */
function submitFileForm() {
proxy.$refs["uploadRef"].submit();
}
watch(
() => props.chatId,
(val) => {
@ -231,69 +282,12 @@ watch(
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value})
function regenerationChart(index){
let question = chatList.value[index - 1]
let chat = {
"type":"question",
"content":question.content,
"time": formatDate(new Date()),
"file": question.file}
chatList.value.push(chat)
let data = {
"query": chat.content,
"user_id": cache.local.get("username"),
"robot": currentMachine.value.length>0?currentMachine.value[0]:"",
"session_id": Cookies.get("chatSessionId"),
"doc": chat.file,
"history": []
}
sendChatMessage(data)
}
function changeThumb(index,chat){
chatList.value[index].operate = chat.operate
chatList.value[index].thumbDownReason = chat.thumbDownReason
}
function openUpload(){
upload.open = true
}
function chooseMachine(val){
inputValue.value = inputValue.value.slice(0,-1)
currentMachine.value = [val]
popoverVisible.value = false
}
function closeMachinePop(){
if (inputValue.value.endsWith('@')){
inputValue.value = inputValue.value.slice(0,-1)
}
popoverVisible.value = false
}
function removeMachine(){
currentMachine.value = []
}
function fullscreen(data){
fullscreenG6Data.value = data
showFullscreenG6Data.value = true
}
function clickoutside() {
if (inputValue.value.endsWith('@')){
inputValue.value = inputValue.value.slice(0,-1)
}
popoverVisible.value = false
}
function removeFile(index){
currentFiles.value = currentFiles.value.slice(index,1)
}
function handleInput() {
popoverVisible.value = inputValue.value.endsWith("@");
}
function padZero(num) {
return num < 10 ? '0' + num : num;
@ -310,174 +304,162 @@ function formatDate(date) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function downloadFile(file,bucket,sessionId){
let data = {file,bucket,sessionId}
proxy.download("aichat/file/download", {
...data,
}, file);
}
async function sendChatHandle(event) {
if (!event.ctrlKey) {
// ctrl
event.preventDefault()
if (inputValue.value.trim() && (chatList.value.length === 0||
(chatList.value[chatList.value.length - 1].isStop ||
chatList.value[chatList.value.length - 1].isEnd))) {
chatList.value.push({
if (inputValue.value.trim().length>0) {
let query = {
"type": "question",
"content": inputValue.value.trim(),
"time": formatDate(new Date()),
"sessionId": Cookies.get("chatSessionId"),
"sessionName": chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20),
"file": currentFiles.value
})
let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
question.file = JSON.stringify(question.file)
await addChat(question)
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
let data = {
"query": inputValue.value.trim(),
"user_id": cache.local.get("username"),
"robot": currentMachine.value.length > 0 ? currentMachine.value[0] : "",
"session_id": Cookies.get("chatSessionId"),
"doc": currentFiles.value,
"history": []
}
handleSend(query,inputValue.value.trim())
inputValue.value = ''
sendChatMessage(data)
}
} else {
// ctrl+
inputValue.value += '\n'
}
}
function sendChatMessage(data){
controller.value = new AbortController()
postChatMessage(data,{signal:controller.value.signal}).then(res=>{
if (res.status !== 200){
chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
}else {
currentFiles.value = []
chatList.value.push({"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''})
const reader = res.body.getReader()
const write = getWrite(reader)
reader.read().then(write).then(()=> {
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
answer.content = JSON.stringify(answer.content)
addChat(answer)
})
}
}).catch((e) => {
chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""})
})
}
watch(
chatList,
() => {
function handleSend(queryObj,inputVal){
queryObj.sessionId = Cookies.get("chatSessionId")
queryObj.sessionName = chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20),
chatList.value.push(queryObj)
chatList.value.push({type:'loading'})
// let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
// await addChat(question)
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
},{
deep:true, immediate:true
let data = {
query: inputVal,
}
)
const getWrite = (reader) => {
let tempResult = '';
/**
* 处理流式数据
* @param {done, value} obj - 包含 done value 属性的对象
*/
const write_stream = ({ done,value }) => {
try {
if (done) {
return
}
const decoder = new TextDecoder('utf-8');
let str = decoder.decode(value, { stream: true });
// chunk
tempResult += str;
console.log(tempResult)
const split = tempResult.match(/data:.*}\r\n/g);
if (split) {
str = split.join('');
tempResult = tempResult.replace(str, '');
sendChatMessage(data)
}
function generate_existing_names(existing_names, paramName){
if (existing_names.length > 0){
if (paramName in existing_names){
let lastPara = paramName.charAt(paramName.length -1)
let num = parseInt(lastPara)
if (isNaN(num)){
paramName += "1"
}else{
return reader.read().then(write_stream);
}
// str "data:"
if (str && str.startsWith('data:')) {
split.forEach((chunkStr) => {
const chunk = JSON.parse(chunkStr.replace('data:', '').trim());
if (chunk.docs && chunk.docs[0].length > 0){
//
chatList.value[chatList.value.length - 1].content.push({"content":chunk.docs[0],"type":"docs"})
}else if (chunk.G6_ER && chunk.G6_ER.length > 0){
//ER
chatList.value[chatList.value.length - 1].content.push({"content":chunk.G6_ER,"type":"G6_ER"})
}else if (chunk.html_image && chunk.html_image.length > 0){
//htmlecharts
chatList.value[chatList.value.length - 1].content.push({"content":chunk.html_image,"type":"html_image"})
}else if (chunk.table && chunk.table.length > 0){
//
chatList.value[chatList.value.length - 1].content.push({"content":chunk.table,"type":"table"})
paramName = paramName.replace(lastPara,num+1)
}
generate_existing_names(existing_names, paramName)
}else {
//
let last_answer = chatList.value[chatList.value.length - 1].content
chunk.choices[0].delta.content = chunk.choices[0].delta.content.replace("\n","\n\n")
if (last_answer.length > 0) {
if(last_answer[last_answer.length - 1].type === 'text'){
chatList.value[chatList.value.length - 1].content[last_answer.length - 1].content += chunk.choices[0].delta.content
existing_names.push(paramName)
}
}else {
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
existing_names.push(paramName)
}
}
function sendChatMessage(data){
postDataQuery(data).then(res=>{
let control_params = res.filters
let data = res.data //
let sql = res.sql // sql
let datas = res.datas // tree
let html_content = res.html_content //echarts
let existing_names = []
let query = [] //
let question = '' //
let result = [] //
for (let i = 0; i < control_params.length; i++) {
let param = control_params[i]
generate_existing_names(existing_names,param.name)
if (param.ct_type === 'mselect'
|| param.ct_type === 'datePicker'
|| param.ct_type === 'dateRangePicker'
|| param.ct_type === 'input'
){
if ( ['维度筛选', '时间筛选', '分组条件', '查询其它条件'].includes(param.cd_type)){
if (param.ct_type === 'dateRangePicker'){
param.dateRangeValue = [param.default_value.start_date,param.default_value.end_date]
}
query.push(param)
}
}
if (param.ct_type === 'output' && param.cd_type === '输出结果'){
result.push(param)
}
if (param.name === '原始问题'){
question = param.default_value
}
}
if (chatList.value[chatList.value.length - 1].type === 'loading'){
chatList.value[chatList.value.length - 1] = {
type:'answer',
"time": formatDate(new Date()),
content:{
query:query,
question: question === ''? chatList.value[chatList.value.length - 2].content:question,
result: result,
data: data,
sql: sql,
datas: datas,
html_content: html_content,
tab:'result'
}
}
}else {
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
chatList.value.push(
{
type:'answer',
"time": formatDate(new Date()),
content:{
query:query,
question: question === ''? chatList.value[chatList.value.length - 1].content:question,
result: result,
data: data,
sql: sql,
datas: datas,
html_content: html_content,
tab:'result'
}
}
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
if (chunk.isEnd || chunk.is_end){
chatList.value[chatList.value.length - 1].isEnd = true
chatList.value[chatList.value.length - 1].time = formatDate(new Date())
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
return Promise.resolve();
)
}
});
}).catch(err=>{
if (chatList.value[chatList.value.length - 1].type === 'loading'){
chatList.value[chatList.value.length - 1] = {
type:'answer_err',
content: "服务器繁忙,请稍后再试",
"time": formatDate(new Date()),
}
} catch (e) {
return Promise.reject(e);
}else {
chatList.value.push(
{
type:'answer_err',
"time": formatDate(new Date()),
content: "服务器繁忙,请稍后再试"
}
)
}
return reader.read().then(write_stream);
};
return write_stream
};
const stopChat = (index) => {
chatList.value[index].isStop = true
if (controller.value !== null){
controller.value.abort()
})
}
let answer = JSON.parse(JSON.stringify(chatList.value[index]))
answer.content = JSON.stringify(answer.content)
addChat(answer)
watch(
chatList,
() => {
nextTick(() => {
//
if (chatList.value.length >0){
scrollDiv.value.setScrollTop(getMaxHeight())
}
const startChat = (index) => {
regenerationChart(index)
})
},{
deep:true, immediate:true
}
)
defineExpose({
setScrollBottom
setScrollBottom,handleSend
})
</script>

400
vue-fastapi-frontend/src/views/dataint/dataquery/index.vue

@ -16,14 +16,181 @@
</div>
<div class="chat-embed__main">
<dataquerychat
ref="AiChatRef"
:record="currentRecordList"
:is_large="is_large"
:cookieSessionId = "sessionId"
ref="dataQueryRef"
:record="chatDataList"
class="AiChat-embed"
@scroll="handleScroll"
@showEdit="showEdit"
@download ="download"
></dataquerychat>
</div>
<el-drawer
v-model="showDrawer"
size="65%"
title="数据问答工作台"
>
<el-row :gutter="20">
<el-col :span="8">
<div class="head-container">
<el-input
v-model="chooseData"
placeholder="请输入搜索可选数据"
clearable
prefix-icon="Search"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container" style="height: calc(100vh - 170px);overflow: auto">
<el-tree
:data="treeData"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="dataQueryTreeRef"
node-key="id"
highlight-current
default-expand-all
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<span>{{ node.label }}</span>
<div v-if="data.showStar">
<el-link v-if="!data.checked" :underline="false" type="warning">
<el-icon><Star /></el-icon>
</el-link>
<el-link v-if="data.checked" :underline="false" type="warning">
<el-icon><StarFilled /></el-icon>
</el-link>
</div>
</div>
</template>
</el-tree>
</div>
</el-col>
<el-col :span="16">
<el-divider content-position="left">条件设置</el-divider>
<el-table :data="tableData">
<el-table-column label="字段名称" prop="name" width="120"></el-table-column>
<el-table-column label="条件类型" prop="cd_type" width="130">
<template #default="scope">
<el-select v-model="scope.row.cd_type">
<el-option key="时间筛选" :value="'时间筛选'" label="时间筛选"></el-option>
<el-option key="维度筛选" :value="'维度筛选'" label="维度筛选"></el-option>
<el-option key="分组条件" :value="'分组条件'" label="分组条件"></el-option>
<el-option key="查询其它条件" :value="'查询其它条件'" label="查询其它条件"></el-option>
<el-option key="输出结果" :value="'输出结果'" label="输出结果"></el-option>
<el-option key="输出其它条件" :value="'输出其它条件'" label="输出其它条件"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="控件类型" prop="ct_type" width="130">
<template #default="scope">
<el-select v-model="scope.row.ct_type">
<el-option key="datePicker" :value="'datePicker'" label="单日筛选"></el-option>
<el-option key="dateRangePicker" :value="'dateRangePicker'" label="周期筛选"></el-option>
<el-option key="input" :value="'input'" label="输入框"></el-option>
<el-option key="select" :value="'select'" label="单项筛选"></el-option>
<el-option key="mselect" :value="'mselect'" label="多项筛选"></el-option>
<el-option key="radioGroup" :value="'radioGroup'" label="是否筛选"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="条件取值">
<template #default="scope">
<template v-if="scope.row.ct_type === 'mselect'">
<el-select v-model="scope.row.default_value" multiple style="width: 100%;" clearable>
<el-option v-for="opt in scope.row.options" :key="opt" :value="opt" :label="opt"></el-option>
</el-select>
</template>
<template v-else-if="scope.row.ct_type === 'datePicker'">
<el-date-picker
v-model="scope.row.default_value"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="Pick a day"
/>
</template>
<template v-else-if="scope.row.ct_type === 'dateRangePicker'">
<el-date-picker
v-model="scope.row.dateRangeValue"
type="daterange"
style="width: 100%;"
range-separator="-"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
start-placeholder="Start date"
end-placeholder="End date"
/>
</template>
<template v-else-if="scope.row.ct_type === 'radioGroup'">
<el-radio-group v-model="scope.row.default_value">
<el-radio :key="'radio'+opt" v-for="opt in scope.row.options" :value="opt">{{ opt }}</el-radio>
</el-radio-group>
</template>
<template v-else>
<el-input v-model="scope.row.default_value"></el-input>
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-tooltip
style="margin-right: 10px"
class="box-item"
effect="dark"
content="上移"
placement="top-start"
v-if="scope.$index > 0"
>
<el-link :underline="false" v-if="scope.$index > 0" @click="changeTableIndex('top',scope.$index)" style="margin-right: 10px" type="primary"><el-icon><Top /></el-icon></el-link>
</el-tooltip>
<el-tooltip
style="margin-right: 10px"
class="box-item"
effect="dark"
content="下移"
placement="top-start"
v-if="scope.$index < tableData.length-1"
>
<el-link :underline="false" v-if="scope.$index < tableData.length-1" @click="changeTableIndex('bottom',scope.$index)" style="margin-right: 10px" type="primary"><el-icon><Bottom /></el-icon></el-link>
</el-tooltip>
<el-tooltip
style="margin-right: 10px"
class="box-item"
effect="dark"
content="删除"
placement="top-start"
>
<el-link :underline="false" @click="remove(scope.$index)" type="danger"><el-icon><Delete /></el-icon></el-link>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="operate-textarea flex chat-width" style="margin-top: 20px">
<el-input
ref="quickInputRef"
v-model="inputValue"
:placeholder="'Ctrl+Enter 换行,Enter发送'"
:autosize="{ minRows: 1, maxRows: 4 }"
type="textarea"
:maxlength="100000"
/>
<div class="operate flex align-center">
<el-button
text
class="sent-button"
:disabled="inputValue === ''"
@click="sendChatHandle"
>
<img v-show="inputValue === ''" src="@/assets/aichat/icon_send.svg" alt="" />
<img v-show="inputValue !== ''" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
</el-button>
</div>
</div>
</el-col>
</el-row>
</el-drawer>
</div>
</template>
@ -32,28 +199,20 @@ import { ref, onMounted, reactive, nextTick, computed } from 'vue'
import { v4 as uuidv4 } from 'uuid';
import Cookies from 'js-cookie'
import { useRoute } from 'vue-router'
import { listChatHistory, getChatList, DeleteChatSession } from "@/api/aichat/aichat";
import Dataquerychat from "./dataquerychat.vue";
const route = useRoute()
const { proxy } = getCurrentInstance();
const {
params: { accessToken }
} = route
const props = defineProps({
is_large: Boolean,
chatDataList: Array,
})
const AiChatRef = ref()
const chatDataList = ref([])
const dataQueryRef = ref()
const chatLogeData = ref([])
const show = ref(false)
const showDrawer = ref(false)
const currentRecordList = ref([])
const chooseData = ref("")
const treeData = ref([])
const tableData = ref([])
const inputValue = ref("")
const sessionId = ref(Cookies.get("chatSessionId")) // Id 'new'
const mouseId = ref('')
function handleScroll(event) {
if (event.scrollTop === 0 && currentRecordList.value.length>0) {
@ -61,52 +220,185 @@ function handleScroll(event) {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
}
}
function sendChatHandle(){
let queryObj = {
"type": "question",
"content": inputValue.value.trim(),
"time": formatDate(new Date()),
}
let inputVal = inputValue.value
dataQueryRef.value.handleSend(queryObj,inputVal);
showDrawer.value = false
}
function clickListHandle(item){
getChatList(item.sessionId).then(res=>{
currentRecordList.value = []
let array = res.data
}
function padZero(num) {
return num < 10 ? '0' + num : num;
}
function formatDate(date) {
const year = date.getFullYear();
const month = padZero(date.getMonth() + 1); // 0+1
const day = padZero(date.getDate());
const hours = padZero(date.getHours());
const minutes = padZero(date.getMinutes());
const seconds = padZero(date.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function remove(index){
uncheckTree(tableData.value[index].name)
tableData.value.splice(index,1)
}
function uncheckTree(name){
if (treeData.value.length>0){
for (let i = 0; i < treeData.value.length; i++) {
if (treeData.value[i].children && treeData.value[i].children.length>0){
for (let j = 0; j < treeData.value[i].children.length; j++) {
if (treeData.value[i].children[j].value === name){
treeData.value[i].children[j].checked = false
}
}
}
}
}
}
function changeTableIndex(type,index){
if(type === 'top'){
let temp = tableData.value[index]
tableData.value[index] = tableData.value[index - 1]
tableData.value[index - 1] = temp
}
if (type === 'bottom'){
let temp = tableData.value[index]
tableData.value[index] = tableData.value[index + 1]
tableData.value[index + 1] = temp
}
}
function showEdit(data){
let array = JSON.parse(JSON.stringify(data.content.datas))
let compareArr = JSON.parse(JSON.stringify(data.content.query))
compareArr.push(...JSON.parse(JSON.stringify(data.content.result)))
treeData.value = []
if (array.length >0){
for (let i = 0; i < array.length; i++) {
if (array[i].type === 'answer'){
array[i].content = JSON.parse(array[i].content)
let tab_cn_name = array[i].tab_cn_name;
let fld_cn_name = array[i].fld_cn_name;
let fld_type = array[i].fld_type;
if (treeData.value.length>0){
if (treeData.value.some(item=>item.label === tab_cn_name)){
for (let j = 0; j < treeData.value.length; j++) {
if (treeData.value[j].label === tab_cn_name){
treeData.value[j].children.push({
label:fld_cn_name +" | "+ fld_type,
value: fld_cn_name,
checked: compareArr.length>0?compareArr.some(item=>item.name === fld_cn_name):false,
showStar: true
})
}
if (array[i].type === 'question'){
array[i].file = JSON.parse(array[i].file)
}
currentRecordList.value.push(array[i])
AiChatRef.value.setScrollBottom()
}else {
treeData.value.push({
label: tab_cn_name,
value: tab_cn_name,
checked: false,
showStar: false,
children:[{
label:fld_cn_name +" | "+ fld_type,
value: fld_cn_name,
checked: false,
showStar: true
}]
})
}
show.value = false
sessionId.value = item.sessionId
Cookies.set("chatSessionId",sessionId.value)
}else {
treeData.value.push({
label: tab_cn_name,
value: tab_cn_name,
checked: false,
showStar: false,
children:[{
label:fld_cn_name +" | "+ fld_type,
value: fld_cn_name,
checked: false,
showStar: true,
}]
})
}
watch(() => props.chatDataList, value => currentRecordList.value = JSON.parse(JSON.stringify(value)))
onMounted(
()=>{
if (Cookies.get("chatSessionId")){
//
getChatList(Cookies.get("chatSessionId")).then(res=>{
let array = res.data
if (array && array.length >0){
for (let i = 0; i < array.length; i++) {
if (array[i].type === 'answer'){
array[i].content = JSON.parse(array[i].content)
}
if (array[i].type === 'question'){
array[i].file = JSON.parse(array[i].file)
}
tableData.value = []
tableData.value.push(...data.content.query)
tableData.value.push(...data.content.result)
showDrawer.value = true
}
watch(chooseData, val => {
proxy.$refs["dataQueryTreeRef"].filter(val);
});
function download(data){
}
function handleNodeClick(data){
if (tableData.value.length>0){
if (tableData.value.some(item=>item.name===data.value)){
let index = -1;
for (let i = 0; i < tableData.value.length; i++) {
if (tableData.value[i].name === data.value){
index = i;
}
currentRecordList.value = array
}).then(()=>{
AiChatRef.value.setScrollBottom()
}
if (index >=0){
tableData.value.splice(index, 1);
}
}else {
tableData.value.push({
name:data.value,
cd_type:'',
ct_type:'',
default_value:''
})
}
}
data.checked = !data.checked
}
const filterNode = (value, data) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
function formatDefaultValue(item){
let value = item.default_value
if (item.cd_type === '输出结果'){
return item.name
}
if (item.ct_type === 'dateRangePicker'){
if (item.dateRangeValue){
return "开始日期为"+item.dateRangeValue[0]+"截止日期为"+item.dateRangeValue[1]
}else {
Cookies.set("chatSessionId",uuidv4())
return ""
}
}
)
return value
}
watch(tableData,(newValue,oldValueValue) =>{
if (tableData.value.length>0){
let resultList = []
let queryList = []
for (let i = 0; i < tableData.value.length; i++) {
if (tableData.value[i].cd_type === '输出结果'){
resultList.push(formatDefaultValue(tableData.value[i]))
}else{
queryList.push(tableData.value[i].name+"为"+formatDefaultValue(tableData.value[i]))
}
}
if (resultList.length > 0){
queryList.push("输出结果为"+resultList.join("和"))
}
let result = "查询"+queryList.join(",")
inputValue.value = result
}
},{ deep: true})
</script>
<style lang="scss">
@ -203,4 +495,12 @@ onMounted(
.ml-24 {
margin-left: 24px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>

5
vue-fastapi-frontend/src/views/meta/metaInfo/index.vue

@ -311,7 +311,7 @@
<el-tab-pane label="业务关系" name="businessRelation">业务关系</el-tab-pane>
<el-tab-pane label="血缘关系" name="bloodRelation">血缘关系</el-tab-pane>
<el-tab-pane label="存储过程" name="proc">
<SQLCodeMirror :data="procStr"></SQLCodeMirror>
<SQLCodeMirror v-if="activeColumnTab === 'proc'" :data="procStr"></SQLCodeMirror>
</el-tab-pane>
</el-tabs>
</el-col>
@ -545,7 +545,6 @@
import SQLCodeMirror from "@/components/codemirror/SQLCodeMirror.vue";
import cache from "@/plugins/cache";
const data = reactive({
queryParams:{
ssysCd:'',
@ -608,6 +607,7 @@
const activeColumnTab = ref("column");
const { proxy } = getCurrentInstance();
const changedColumns = ref([])
function changeColumnTab(){
if (activeColumnTab.value === 'proc'){
procStr.value = "--基金量化产品监管报送存储过程:ADS.SP_ADS_SAC_QNTPRD_ALL\n" +
@ -1058,6 +1058,7 @@
getDatabaseList()
handleQuery()
})
</script>
<style scoped lang="scss">

6
vue-fastapi-frontend/vite.config.js

@ -41,6 +41,12 @@ export default defineConfig(({ mode, command }) => {
changeOrigin: true,
rewrite: (p) => p.replace(/^\/aichat-api/, '')
},
'/dataquery-api': {
target: 'http://47.121.207.11:6006',
// target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dataquery-api/, '')
},
"/ds-api": {
target: 'http://47.121.207.11:12345',
changeOrigin: true,

Loading…
Cancel
Save