Browse Source

资产模块

master
xueyinfei 3 months ago
parent
commit
90340354e4
  1. 159
      vue-fastapi-backend/module_admin/controller/data_ast_content_controller.py
  2. 615
      vue-fastapi-backend/module_admin/dao/data_ast_content_dao.py
  3. 15
      vue-fastapi-backend/module_admin/entity/vo/data_ast_content_vo.py
  4. 316
      vue-fastapi-backend/module_admin/service/data_ast_content_service.py
  5. 62
      vue-fastapi-frontend/src/api/dataAsset/directory.js
  6. 105
      vue-fastapi-frontend/src/views/dataAsset/directory/components/AssetBookmarkDialog.vue
  7. 2
      vue-fastapi-frontend/src/views/dataAsset/directory/components/AssetMoveDialog.vue
  8. 82
      vue-fastapi-frontend/src/views/dataAsset/directory/components/BookmarkFormDialog.vue
  9. 117
      vue-fastapi-frontend/src/views/dataAsset/directory/components/BookmarkMoveDialog.vue
  10. 28
      vue-fastapi-frontend/src/views/dataAsset/directory/components/FormDialog.vue
  11. 341
      vue-fastapi-frontend/src/views/dataAsset/directory/index.vue
  12. 633
      vue-fastapi-frontend/src/views/dataint/dataquery/dataquerychat.vue
  13. 203
      vue-fastapi-frontend/src/views/dataint/dataquery/index.vue

159
vue-fastapi-backend/module_admin/controller/data_ast_content_controller.py

@ -10,8 +10,10 @@ from module_admin.annotation.log_annotation import Log
from module_admin.aspect.interface_auth import CheckUserInterfaceAuth
from module_admin.service.login_service import LoginService
from module_admin.service.data_ast_content_service import DataCatalogService
from module_admin.entity.vo.data_ast_content_vo import DataCatalogRequest, DataCatalogResponse, DataCatalogPageQueryModel, DeleteDataCatalogModel,DataCatalogResponseWithChildren,DataAssetCatalogTreeResponse,DataCatalogMovedRequest,DataCatalogMergeRequest,DataCatalogChild,DataCatalogMoverelRequest,DataAstIndxRequest,DataAstBookmarkRelaRequest
from module_admin.entity.vo.data_ast_content_vo import DataAstSecuResponse, DataAstSecuRequest,DataCatalogRequest, DataCatalogResponse, DataCatalogPageQueryModel, DeleteDataCatalogModel,DataCatalogResponseWithChildren,DataAssetCatalogTreeResponse,DataCatalogMovedRequest,DataCatalogMergeRequest,DataCatalogChild,DataCatalogMoverelRequest,DataAstIndxRequest,DataAstBookmarkRelaRequest
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_admin.entity.vo.metasecurity_vo import MetaSecurityApiModel
from module_admin.service.metasecurity_service import MetaSecurityService
from utils.common_util import bytes2file_response
from utils.log_util import logger
from utils.page_util import PageResponseModel
@ -33,8 +35,9 @@ async def get_data_catalog_list(
#设置字段
user_id = current_user.user.user_id
user_name = current_user.user.user_name
# 获取分页数据
catalog_page_query_result = await DataCatalogService.get_catalog_list_services(query_db, catalog_page_query, user_id, is_page=True)
catalog_page_query_result = await DataCatalogService.get_catalog_list_services(query_db, catalog_page_query, user_id, user_name, is_page=True)
logger.info('获取成功')
@ -82,6 +85,46 @@ async def add_data_catalog(
msg=add_result.message
)
@dataCatalogController.get(
'/getMetaSercuityData',
response_model=DataAstSecuResponse,
dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:secu'))]
)
@ValidateFields(validate_model='get_secu_data_request')
async def getMetaSercuityData(
request: Request,
dataAstSecuRequest: DataAstSecuRequest=Depends(DataAstSecuRequest),
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 获取当前用户信息
user_id = current_user.user.user_id
password = current_user.user.password
logger.info(f"获取当前用户信息:user_id={user_id}, password={password}")
# 设置字段
apiModel = MetaSecurityApiModel()
apiModel.dbRId = dataAstSecuRequest.data_ast_src
apiModel.username = user_id
apiModel.password = password
apiModel.sqlStr = "select * from " + dataAstSecuRequest.data_ast_eng_name
logger.info(f"设置 apiModel 参数:dbRId={apiModel.dbRId}, username={apiModel.username}, password={apiModel.password}, sqlStr={apiModel.sqlStr}")
# 打印 apiModel 对象
logger.debug(f"apiModel 对象内容:{apiModel}")
# 调用服务层方法
config_detail_result = await MetaSecurityService.getMetaSercuitybysql(request, query_db, apiModel)
logger.info(f"调用 MetaSecurityService.getMetaSercuitybysql 方法,返回结果:{config_detail_result}")
# 记录成功日志
logger.info(f"获取 config_id 为 {apiModel} 的信息成功")
return ResponseUtil.success(data=config_detail_result)
@dataCatalogController.put('/edit', dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:edit'))])
@ValidateFields(validate_model='edit_data_catalog')
@Log(title='数据目录管理', business_type=BusinessType.UPDATE)
@ -215,7 +258,6 @@ async def delete_ast_book_mark_rela(
):
user_name = current_user.user.user_name
user_id = current_user.user.user_id
print(123456,user_id,type(user_id))
# 创建请求对象
delete_request = DataAstBookmarkRelaRequest(rela_onum=rela_onum)
@ -244,7 +286,6 @@ async def add_ast_book_mark_rela(
user_name = current_user.user.user_name
# 调用服务层方法
print('调用服务层方法',add_bookmark)
add_result = await DataCatalogService.add_ast_book_mark_rela_services(query_db, add_bookmark,user_name)
logger.info(add_result.message)
@ -269,3 +310,113 @@ async def get_data_ast_indx_list(
logger.info('获取成功')
return indx_page_query_result
@dataCatalogController.post('/bookmark/folder', dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:add'))])
@ValidateFields(validate_model='add_bookmark_folder')
@Log(title='收藏目录管理', business_type=BusinessType.INSERT)
async def add_bookmark_folder(
request: Request,
add_folder: DataCatalogRequest,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 设置字段
add_folder.upd_prsn = current_user.user.user_name
add_folder.supr_content_onum = 2 # 固定为"我的收藏"的目录ID
add_folder.content_stat = "1" # 设置为有效状态
add_folder.leaf_node_flag = 1 # 默认为叶子节点
# 调用服务层方法
add_result = await DataCatalogService.add_bookmark_folder_services(query_db, add_folder)
logger.info(add_result.message)
# 返回标准化响应
return ResponseUtil.success(
msg=add_result.message
)
@dataCatalogController.put('/bookmark/folder', dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:edit'))])
@ValidateFields(validate_model='edit_bookmark_folder')
@Log(title='收藏目录管理', business_type=BusinessType.UPDATE)
async def edit_bookmark_folder(
request: Request,
edit_folder: DataCatalogRequest,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 设置审计字段
edit_folder.upd_prsn = current_user.user.user_name
# 调用服务层方法
edit_result = await DataCatalogService.edit_bookmark_folder_services(query_db, edit_folder)
logger.info(edit_result.message)
# 返回标准化响应
return ResponseUtil.success(
msg=edit_result.message
)
@dataCatalogController.delete('/bookmark/folder/{content_onum}', dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:remove'))])
@Log(title='收藏目录管理', business_type=BusinessType.DELETE)
async def delete_bookmark_folder(
request: Request,
content_onum: int,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 调用服务层方法
delete_result = await DataCatalogService.delete_bookmark_folder_services(
query_db,
content_onum,
current_user.user.user_name,
current_user.user.user_id
)
logger.info(delete_result.message)
# 返回标准化响应
return ResponseUtil.success(
msg=delete_result.message
)
@dataCatalogController.get(
'/bookmark/folders',
dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:list'))]
)
async def get_bookmark_folders(
request: Request,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 获取当前用户名
user_name = current_user.user.user_name
# 调用服务层方法
folders = await DataCatalogService.get_bookmark_folders_services(query_db, user_name)
logger.info(f'获取用户 {user_name} 的收藏目录列表成功')
# 返回标准化响应
return ResponseUtil.success(data=folders)
@dataCatalogController.put('/bookmark/asset/move', dependencies=[Depends(CheckUserInterfaceAuth('system:data_catalog:edit'))])
@ValidateFields(validate_model='move_bookmark_asset')
@Log(title='收藏资产管理', business_type=BusinessType.UPDATE)
async def move_bookmark_asset(
request: Request,
moverel_catalog: DataCatalogMoverelRequest,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
# 设置用户信息
moverel_catalog.upd_prsn = current_user.user.user_name
# 调用服务层方法
moverel_result = await DataCatalogService.move_bookmark_asset_services(query_db, moverel_catalog)
logger.info(moverel_result.message)
# 返回标准化响应
return ResponseUtil.success(msg=moverel_result.message)

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

@ -60,6 +60,7 @@ class DataCatalogDAO:
return catalog_info
# @classmethod
# async def get_catalog_list(cls, db: AsyncSession, query_object: DataCatalogPageQueryModel, user_id: int, is_page: bool = False):
# """
@ -70,101 +71,130 @@ class DataCatalogDAO:
# :param is_page: 是否分页
# :return: 数据资产目录分页列表
# """
# # 创建别名对象
# t1 = aliased(DataAstContentRela, name='t1')
# t2 = aliased(DataAstInfo, name='t2')
# t3 = aliased(DataAstBookmarkRela, name='t3')
# query = (
# select(
# DataAstContent.content_onum,
# DataAstContent.content_name,
# DataAstContent.content_stat,
# DataAstContent.content_intr,
# DataAstContent.content_pic,
# DataAstContent.supr_content_onum,
# DataAstContent.leaf_node_flag,
# DataAstContent.upd_prsn,
# DataAstContent.upd_time,
# 创建别名对象
# t1 = aliased(DataAstContentRela, name='t1')
# t2 = aliased(DataAstInfo, name='t2')
# t3 = aliased(DataAstBookmarkRela, name='t3')
# # 修改子查询部分
# subquery_t1 = (
# select(DataAstContentRela)
# .where(DataAstContentRela.upd_prsn == query_object.upd_prsn, DataAstContentRela.content_onum == '2' and DataAstContentRela.rela_status == '1')
# .union_all(
# select(DataAstContentRela)
# .where(DataAstContentRela.content_onum != '2' and DataAstContentRela.rela_status == '1')
# )
# ).alias('subquery_t1') # 为子查询分配唯一别名
# query = (
# select(
# DataAstContent.content_onum,
# DataAstContent.content_name,
# DataAstContent.content_stat,
# DataAstContent.content_intr,
# DataAstContent.content_pic,
# DataAstContent.supr_content_onum,
# DataAstContent.leaf_node_flag,
# DataAstContent.upd_prsn,
# DataAstContent.upd_time,
# t1.rela_onum,
# t1.ast_onum,
# t1.rela_type,
# t1.rela_eff_begn_date,
# t1.rela_eff_end_date,
# t1.upd_prsn,
# subquery_t1.c.rela_onum, # 明确指定子查询的字段
# subquery_t1.c.ast_onum,
# subquery_t1.c.rela_type,
# subquery_t1.c.rela_eff_begn_date,
# subquery_t1.c.rela_eff_end_date,
# subquery_t1.c.upd_prsn,
# t2.data_ast_no,
# t2.data_ast_eng_name,
# t2.data_ast_cn_name,
# t2.data_ast_type,
# t2.data_ast_stat,
# t2.data_ast_desc,
# t2.data_ast_clas,
# t2.data_ast_cont,
# t2.data_ast_faq,
# t2.data_ast_estb_time,
# t2.data_ast_upd_time,
# t2.data_ast_src,
# t2.ast_no,
# t3.bookmark_orde,
# case(
# (t3.rela_onum.isnot(None), 1),
# else_=0
# ).label('bookmark_flag')
# )
# .distinct()
# .select_from(DataAstContent)
# .outerjoin(t1, DataAstContent.content_onum == t1.content_onum)
# .outerjoin(t2, t1.ast_onum == t2.ast_no)
# .outerjoin(t3, and_(
# # t1.rela_onum == t3.rela_onum,
# t2.data_ast_no == t3.data_ast_no,
# t3.user_id == user_id # admin用户的ID,后续以传参的形式过来
# ))
# .where(DataAstContent.content_stat == 1)
# .order_by(DataAstContent.content_onum)
# )
# # 使用分页工具进行查询
# data_ast_list = await PageUtil.paginate(
# db,
# query,
# page_num=query_object.page_num,
# page_size=query_object.page_size,
# is_page=is_page
# )
# return data_ast_list
# t2.data_ast_no,
# t2.data_ast_eng_name,
# t2.data_ast_cn_name,
# t2.data_ast_type,
# t2.data_ast_stat,
# t2.data_ast_desc,
# t2.data_ast_clas,
# t2.data_ast_cont,
# t2.data_ast_faq,
# t2.data_ast_estb_time,
# t2.data_ast_upd_time,
# t2.data_ast_src,
# t2.ast_no,
# t3.bookmark_orde,
# case(
# (t3.rela_onum.isnot(None), 1),
# else_=0
# ).label('bookmark_flag')
# )
# .distinct()
# .select_from(DataAstContent)
# .outerjoin(subquery_t1, DataAstContent.content_onum == subquery_t1.c.content_onum) # 明确使用子查询别名
# .outerjoin(t2, subquery_t1.c.ast_onum == t2.ast_no)
# .outerjoin(t3, and_(
# subquery_t1.c.ast_onum == t3.data_ast_no,
# t3.user_id == user_id
# ))
# .where(DataAstContent.content_stat == 1)
# .order_by(DataAstContent.content_onum)
# )
# # 使用分页工具进行查询
# data_ast_list = await PageUtil.paginate(
# db,
# query,
# page_num=query_object.page_num,
# page_size=query_object.page_size,
# is_page=is_page
# )
# return data_ast_list
@classmethod
async def get_catalog_list(cls, db: AsyncSession, query_object: DataCatalogPageQueryModel, user_id: int, is_page: bool = False):
"""
根据查询参数获取数据资产目录列表
:param db: 异步会话对象
:param query_object: 分页查询参数对象
:param is_page: 是否分页
:return: 数据资产目录分页列表
"""
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')
t3 = aliased(DataAstBookmarkRela, name='t3')
# 修改子查询部分
# 构建子查询1(对应subquery_t1)
subquery_t1 = (
select(DataAstContentRela)
.where(DataAstContentRela.upd_prsn == query_object.upd_prsn, DataAstContentRela.content_onum == '2' and DataAstContentRela.rela_status == '1')
select(t1)
.where(
t1.upd_prsn == user_name,
t1.content_onum == '2',
t1.rela_status == '1'
)
.union_all(
select(DataAstContentRela)
.where(DataAstContentRela.content_onum != '2' and DataAstContentRela.rela_status == '1')
select(t1)
.where(
t1.content_onum != '2',
t1.rela_status == '1'
)
)
).alias('subquery_t1')
# 新增子查询2(对应subquery_t2)
subquery_t2 = (
select(t1.rela_onum)
.where(
t1.rela_status == 1,
t1.upd_prsn == user_name, #query_object.upd_prsn
t1.content_onum.in_(
select(DataAstContent.content_onum)
.where(
or_(
DataAstContent.supr_content_onum == 2,
DataAstContent.content_onum == 2
),
DataAstContent.content_stat == 1,
DataAstContent.upd_prsn == user_name
)
)
)
).alias('subquery_t1') # 为子查询分配唯一别名
).alias('subquery_t2')
# 主查询构建
query = (
select(
# 原有字段保持不变
DataAstContent.content_onum,
DataAstContent.content_name,
DataAstContent.content_stat,
@ -175,13 +205,14 @@ class DataCatalogDAO:
DataAstContent.upd_prsn,
DataAstContent.upd_time,
subquery_t1.c.rela_onum, # 明确指定子查询的字段
subquery_t1.c.rela_onum,
subquery_t1.c.ast_onum,
subquery_t1.c.rela_type,
subquery_t1.c.rela_eff_begn_date,
subquery_t1.c.rela_eff_end_date,
subquery_t1.c.upd_prsn,
# 修正:直接使用t2的属性,而非t2.c
t2.data_ast_no,
t2.data_ast_eng_name,
t2.data_ast_cn_name,
@ -195,25 +226,32 @@ class DataCatalogDAO:
t2.data_ast_upd_time,
t2.data_ast_src,
t2.ast_no,
t3.bookmark_orde,
case(
(t3.rela_onum.isnot(None), 1),
(t3.rela_onum != None, 1),
else_=0
).label('bookmark_flag')
).label('bookmark_flag'),
case(
(subquery_t2.c.rela_onum != None, 1),
else_=0
).label('sczc_flag')
)
.distinct()
.select_from(DataAstContent)
.outerjoin(subquery_t1, DataAstContent.content_onum == subquery_t1.c.content_onum) # 明确使用子查询别名
.outerjoin(subquery_t1, DataAstContent.content_onum == subquery_t1.c.content_onum)
.outerjoin(t2, subquery_t1.c.ast_onum == t2.ast_no)
.outerjoin(t3, and_(
subquery_t1.c.ast_onum == t3.data_ast_no,
t3.user_id == user_id
))
.where(DataAstContent.content_stat == 1)
.outerjoin(subquery_t2, subquery_t1.c.rela_onum == subquery_t2.c.rela_onum)
.where(
DataAstContent.content_stat == 1
)
.order_by(DataAstContent.content_onum)
)
# 使用分页工具进行查询
# 分页处理保持不变
data_ast_list = await PageUtil.paginate(
db,
query,
@ -221,12 +259,9 @@ class DataCatalogDAO:
page_size=query_object.page_size,
is_page=is_page
)
return data_ast_list
@classmethod
async def add_catalog_dao(cls, db: AsyncSession, catalog1: dict, catalog2: dict):
"""
@ -411,26 +446,30 @@ class DataCatalogDAO:
:param db: 异步会话对象
:return: 去重后的数据资产树数据
"""
# 创建别名对象
a = aliased(DataAstInfo, name='a')
b = aliased(DataAstContentRela, name='b')
# 构建查询
query = (
select(
DataAstInfo.data_ast_src,
DataAstInfo.data_ast_eng_name,
DataAstInfo.data_ast_cn_name,
DataAstInfo.ast_no
a.data_ast_src,
a.data_ast_eng_name,
a.data_ast_cn_name,
a.ast_no,
case(
(b.ast_onum.isnot(None), 1),
else_=0
).label('rel_status')
)
.distinct()
.select_from(DataAstInfo)
.where(
DataAstInfo.data_ast_stat == 1,
not_(
DataAstInfo.ast_no.in_(
select(DataAstContentRela.ast_onum)
.where(DataAstContentRela.rela_status == 1)
)
)
)
.select_from(a)
.outerjoin(b, and_(
a.ast_no == b.ast_onum,
b.rela_status == 1
))
.where(a.data_ast_stat == 1)
)
result = await db.execute(query)
rows = result.fetchall()
@ -472,16 +511,6 @@ class DataCatalogDAO:
:return:
"""
# stmt = (
# update(DataAstContent)
# .where(DataAstContent.content_onum == merge_catalog_data['content_onum'] , DataAstContent.supr_content_onum == merge_catalog_data['supr_content_onum'])
# .values(
# content_onum=merge_catalog_data['content_onum_after'],
# supr_content_onum=merge_catalog_data['supr_content_onum_after'],
# upd_time=datetime.now()
# ) )
# await db.execute(stmt)
stmt1 = (
update(DataAstContentRela)
.where( DataAstContentRela.content_onum == merge_catalog_data['content_onum'] and DataAstContentRela.rela_status == 1 )
@ -585,27 +614,61 @@ class DataCatalogDAO:
:return: 操作结果字典包含成功状态和提示信息
"""
try:
# 打印传入的参数
logger.info(f"开始处理取消收藏请求,传入参数:catalog={catalog}, user_name={user_name}, user_id={user_id}")
# 创建子查询:获取需要删除的资产编号
ast_onum_subquery = (
select(DataAstContentRela.ast_onum)
.where(
DataAstContentRela.rela_onum == catalog['rela_onum'],
DataAstBookmarkRela.user_id == user_id,
DataAstContentRela.upd_prsn == user_name,
DataAstContentRela.rela_status == '1'
)
).subquery()
# 创建子查询:获取在收藏目录下的序号content_onum
content_onum_subquery = (
select(DataAstContent.content_onum)
.where(
DataAstContent.content_onum == '2' or DataAstContent.supr_content_onum == '2',
DataAstContent.content_stat == '1',
DataAstContent.upd_prsn == user_name
).subquery()
)
# 创建子查询
content_onum_subquery = (
select(DataAstContent.content_onum)
.where(
and_(
or_(
DataAstContent.content_onum == '2',
DataAstContent.supr_content_onum == '2'
),
DataAstContent.content_stat == '1',
DataAstContent.upd_prsn == user_name
)
)
.subquery()
)
# 打印子查询的SQL语句
logger.info(f"子查询SQL: {str(ast_onum_subquery),str(content_onum_subquery)}")
# 构建删除语句
stmt1 = (
delete(DataAstContentRela)
.where(
DataAstContentRela.upd_prsn == user_name,
DataAstContentRela.content_onum == '2', # 考虑使用变量或常量
DataAstContentRela.ast_onum.in_(ast_onum_subquery),
DataAstContentRela.rela_status == 1
DataAstContentRela.rela_status == 1,
DataAstContentRela.content_onum.in_(content_onum_subquery),
DataAstContentRela.ast_onum.in_(ast_onum_subquery)
)
)
# 打印删除语句1的SQL
logger.info(f"删除语句1 SQL: {str(stmt1)}")
stmt2 = (
delete(DataAstBookmarkRela)
.where(
@ -614,9 +677,14 @@ class DataCatalogDAO:
)
)
# 打印删除语句2的SQL
logger.info(f"删除语句2 SQL: {str(stmt2)}")
# 执行删除操作
await db.execute(stmt1)
logger.info("开始执行删除操作...")
await db.execute(stmt2)
await db.execute(stmt1)
await db.commit()
logger.info(f"成功删除收藏关系")
@ -632,6 +700,8 @@ class DataCatalogDAO:
return {"is_success": False, "message": "未知错误"}
@classmethod
async def delete_ast_book_mark_rela_by_content_onum(cls, db: AsyncSession, content_onum: int, user_id: int):
"""
@ -671,21 +741,6 @@ class DataCatalogDAO:
# @classmethod
# async def add_ast_book_mark_rela_dao(cls, db: AsyncSession, catalog: DataAstBookmarkRelaRequest):
# """
# 添加收藏数据库操作
# :param db: orm对象
# :param catalog: 收藏对象
# :return:
# """
# #如果catalog[user_id]下已经存在了catalog[data_ast_no],那么返回已收藏,否则添加收藏,新添加的收藏顺序号要求是插入之前最大顺序号加1
# db_catalog = DataAstBookmarkRela(**catalog)
# db.add(db_catalog)
# await db.flush()
# logger.info(" 添加收藏,操作成功")
@classmethod
async def add_ast_book_mark_rela_dao(cls, db: AsyncSession, catalog: DataAstBookmarkRelaRequest, user_name: str):
"""
@ -791,4 +846,292 @@ class DataCatalogDAO:
result = await db.execute(query)
rows = result.mappings().all() # 直接获取字典列表
return rows
return rows
@classmethod
async def get_bookmark_folder_by_name(cls, db: AsyncSession, folder_name: str, user_name: str):
"""
根据名称和用户名获取收藏目录
:param db: 数据库会话
:param folder_name: 目录名称
:param user_name: 用户名
:return: 目录对象
"""
query = (
select(DataAstContent)
.where(
DataAstContent.content_name == folder_name,
DataAstContent.upd_prsn == user_name,
DataAstContent.supr_content_onum == 2,
DataAstContent.content_stat == "1"
)
)
result = await db.execute(query)
return result.scalars().first()
@classmethod
async def get_bookmark_folder_by_id(cls, db: AsyncSession, content_onum: int, user_name: str):
"""
根据ID和用户名获取收藏目录
:param db: 数据库会话
:param content_onum: 目录ID
:param user_name: 用户名
:return: 目录对象
"""
query = (
select(DataAstContent)
.where(
DataAstContent.content_onum == content_onum,
DataAstContent.upd_prsn == user_name,
DataAstContent.supr_content_onum == 2,
DataAstContent.content_stat == "1"
)
)
result = await db.execute(query)
return result.scalars().first()
@classmethod
async def check_folder_has_relations(cls, db: AsyncSession, content_onum: int):
"""
检查目录下是否有资产关系
:param db: 数据库会话
:param content_onum: 目录ID
:return: 是否存在关系
"""
query = (
select(func.count(DataAstContentRela.rela_onum))
.where(
DataAstContentRela.content_onum == content_onum,
DataAstContentRela.rela_status == "1"
)
)
result = await db.execute(query)
count = result.scalar()
return count > 0
@classmethod
async def get_bookmark_folders(cls, db: AsyncSession, user_name: str):
"""
获取用户的收藏目录列表
:param db: 数据库会话
:param user_name: 用户名
:return: 目录列表
"""
# 构建联合查询
combined_query = (
select(
DataAstContent.content_onum,
DataAstContent.content_name,
DataAstContent.content_intr,
DataAstContent.content_pic,
DataAstContent.upd_time,
DataAstContent.supr_content_onum,
DataAstContent.leaf_node_flag
)
# 根目录条件
.where(
DataAstContent.content_onum == 2,
DataAstContent.content_stat == "1"
)
# 合并子目录条件
.union_all(
select(
DataAstContent.content_onum,
DataAstContent.content_name,
DataAstContent.content_intr,
DataAstContent.content_pic,
DataAstContent.upd_time,
DataAstContent.supr_content_onum,
DataAstContent.leaf_node_flag
)
.where(
DataAstContent.upd_prsn == user_name,
DataAstContent.supr_content_onum == 2,
DataAstContent.content_stat == "1"
)
.order_by(DataAstContent.upd_time.desc())
)
)
result = await db.execute(combined_query)
return [dict(row) for row in result.mappings().all()]
@classmethod
async def get_bookmark_folder_by_name_exclude_id(cls, db: AsyncSession, folder_name: str, user_name: str, exclude_id: int):
"""
根据名称和用户名获取收藏目录排除指定ID
:param db: 数据库会话
:param folder_name: 目录名称
:param user_name: 用户名
:param exclude_id: 排除的目录ID
:return: 目录对象
"""
query = (
select(DataAstContent)
.where(
DataAstContent.content_name == folder_name,
DataAstContent.upd_prsn == user_name,
DataAstContent.supr_content_onum == 2,
DataAstContent.content_stat == "1",
DataAstContent.content_onum != exclude_id
)
)
result = await db.execute(query)
return result.scalars().first()
@classmethod
async def is_bookmark_folder(cls, db: AsyncSession, content_onum: int, user_name: str = None):
"""
检查目录是否是收藏目录或其子目录
:param db: 数据库会话
:param content_onum: 目录ID
:param user_name: 用户名如果需要验证所有权
:return: 是否是收藏目录
"""
if content_onum == 2: # "我的收藏"根目录
return True
query = select(DataAstContent).where(
DataAstContent.content_onum == content_onum,
DataAstContent.supr_content_onum == 2,
DataAstContent.content_stat == "1"
)
if user_name:
query = query.where(DataAstContent.upd_prsn == user_name)
result = await db.execute(query)
return result.scalars().first() is not None
@classmethod
async def move_bookmark_asset_dao(cls, db: AsyncSession, moverel_catalog_data: dict):
"""
在收藏目录间移动资产的数据库操作
"""
stmt = (
update(DataAstContentRela)
.where(
DataAstContentRela.rela_onum == moverel_catalog_data['rela_onum'],
DataAstContentRela.content_onum == moverel_catalog_data['content_onum'],
DataAstContentRela.rela_status == "1"
)
.values(
content_onum=moverel_catalog_data['content_onum_after'],
upd_prsn=moverel_catalog_data['upd_prsn'],
rela_eff_end_date=datetime.now()
)
)
await db.execute(stmt)
await cls.update_leaf_node_flag(db)
@classmethod
async def delete_bookmark_folder_dao(cls, db: AsyncSession, content_onum: int, user_name: str,user_id:str):
"""
删除收藏目录及其资产关系的专用数据库操作
:param db: orm对象
:param content_onum: 收藏目录ID
:param user_name: 用户名用于权限验证
:return: 操作结果字典
"""
try:
logger.info(f"开始删除用户 {user_name} 的收藏目录 {content_onum}")
# 1. 验证目录是否存在且属于当前用户和收藏体系
folder_query = select(DataAstContent).where(
DataAstContent.content_onum == content_onum,
DataAstContent.upd_prsn == user_name, # 确保只能删除自己的收藏目录
DataAstContent.supr_content_onum == 2, # 确保是收藏子目录
DataAstContent.content_stat == '1'
)
folder_result = await db.execute(folder_query)
folder = folder_result.scalars().first()
if not folder:
logger.warning(f"未找到用户 {user_name} 的收藏目录 {content_onum} 或无权限删除")
return {"success": False, "message": "收藏目录不存在或无权限删除"}
# 创建子查询,获取符合条件的 ast_onum
ast_onum_subs = (
select(DataAstContentRela.ast_onum)
.where(
DataAstContentRela.content_onum == content_onum,
DataAstContentRela.rela_status == '1',
DataAstContentRela.upd_prsn == user_name
)
.subquery()
)
# 删除收藏关系表数据
delete_stmt = (
delete(DataAstBookmarkRela)
.where(
DataAstBookmarkRela.data_ast_no.in_(ast_onum_subs),
DataAstBookmarkRela.user_id == user_id
)
)
# 执行删除操作
await db.execute(delete_stmt)
# 2. 更新关联的资产关系状态
rela_update = await db.execute(
update(DataAstContentRela)
.where(
DataAstContentRela.content_onum == content_onum,
DataAstContentRela.upd_prsn == user_name,
DataAstContentRela.rela_status == '1'
)
.values(
rela_status='0',
rela_eff_end_date=datetime.now()
)
)
rela_count = rela_update.rowcount
logger.info(f"已更新 {rela_count} 个资产关系状态")
# 3. 更新目录状态为无效
folder_update = await db.execute(
update(DataAstContent)
.where(
DataAstContent.content_onum == content_onum,
DataAstContent.upd_prsn == user_name,
DataAstContent.content_stat == '1'
)
.values(
content_stat='0',
upd_time=datetime.now()
)
)
# 4. 更新叶子节点标志
await cls.update_leaf_node_flag(db)
await db.commit()
logger.info(f"成功删除用户 {user_name} 的收藏目录 {content_onum}")
return {"success": True, "message": "收藏目录删除成功"}
except Exception as e:
logger.error(f"删除收藏目录时发生错误: {str(e)}", exc_info=True)
return {"success": False, "message": f"删除操作出错: {str(e)}"}

15
vue-fastapi-backend/module_admin/entity/vo/data_ast_content_vo.py

@ -167,6 +167,7 @@ class DataCatalogMoverelRequest(BaseModel):
rela_onum: Optional[int] = Field(default=None, alias="relaOnum", description='关系序号')
content_onum: Optional[int] = Field(default=None, alias="contentOnum", description='目录序号')
content_onum_after: Optional[int] = Field(default=None, alias="contentOnumAfter", description='移动后的目录序号')
upd_prsn: Optional[str] = Field(default=None, alias="updPrsn", description='更新人员')
class DataAstBookmarkRelaRequest(BaseModel):
@ -203,4 +204,16 @@ class DataAstIndxResponse(BaseModel):
ast_no: Optional[str] = Field(default=None, alias="astNo", description='资产编号')
indx_no: Optional[str] = Field(default=None, alias="indxNo", description='指标编号')
indx_name: Optional[str] = Field(default=None, alias="indxName", description='指标名称')
indx_val: Optional[float] = Field(default=None, alias="indxVal", description='指标值')
indx_val: Optional[float] = Field(default=None, alias="indxVal", description='指标值')
class DataAstSecuRequest(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
data_ast_src: Optional[str] = Field(default=None, alias="dataAstSrc", description='数据源连接')
data_ast_eng_name: Optional[str] = Field(default=None, alias="dataAstEngName", description='表英文名')
class DataAstSecuResponse(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, from_attributes=True)
data_ast_src: Optional[str] = Field(default=None, alias="dataAstSrc", description='数据源连接')
data_ast_eng_name: Optional[str] = Field(default=None, alias="dataAstEngName", description='表英文名')

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

@ -1,9 +1,10 @@
from datetime import datetime
from utils.log_util import logger
from collections import defaultdict
from pyecharts.options import LabelOpts
from pyecharts.options import LabelOpts, InitOpts
from fastapi.responses import JSONResponse
from pyecharts.charts import Pie, Bar, Page
from pyecharts import options as opts
from sqlalchemy.ext.asyncio import AsyncSession
from exceptions.exception import ServiceException
from module_admin.dao.data_ast_content_dao import DataCatalogDAO
@ -18,7 +19,7 @@ class DataCatalogService:
@classmethod
async def get_catalog_list_services(
cls, query_db: AsyncSession, query_object: DataCatalogPageQueryModel, user_id: int, is_page: bool = False
cls, query_db: AsyncSession, query_object: DataCatalogPageQueryModel, user_id: int, user_name: str, is_page: bool = False
):
"""
获取数据目录列表信息service
@ -28,8 +29,8 @@ class DataCatalogService:
:param is_page: 是否开启分页
:return: 数据目录列表信息对象
"""
catalog_list_result = await DataCatalogDAO.get_catalog_list(query_db, query_object, user_id, is_page)
catalog_list_result = await DataCatalogDAO.get_catalog_list(query_db, query_object, user_id, user_name, is_page)
print('获取数据清单内容111:',catalog_list_result)
# 按contentOnum分组
grouped = defaultdict(list)
for item in catalog_list_result.rows:
@ -60,6 +61,7 @@ class DataCatalogService:
# 处理叶子节点的数据资产子节点
if is_leaf and rela_onum :
for item in items:
asset_child = {
'relaOnum': item['relaOnum'],
'contentOnum': first_item['contentOnum'],
@ -83,7 +85,8 @@ class DataCatalogService:
'astNo': item['astNo'],
'relaOnum': item['relaOnum'],
'bookmarkOrde': item['bookmarkOrde'],
'bookmarkFlag': item['bookmarkFlag']
'bookmarkFlag': item['bookmarkFlag'],
'sczcFlag': item['sczcFlag']
}
common_fields['children'].append(asset_child)
@ -99,9 +102,6 @@ class DataCatalogService:
parent = nodes.get(supr)
if parent:
parent['children'].append(node)
print('获取数据清单内容:',root)
catalog_list_result.rows = [root]
return catalog_list_result
@ -274,7 +274,7 @@ class DataCatalogService:
sys_groups = {}
for item in rows:
sys_name, eng_name, cn_name, ast_no = item
sys_name, eng_name, cn_name, ast_no, rel_status = item
# 创建或获取系统分组
if sys_name not in sys_groups:
sys_groups[sys_name] = {
@ -286,7 +286,8 @@ class DataCatalogService:
sys_groups[sys_name]["children"].append({
"dataAssetCatalogNo": eng_name,
"dataAssetCatalogName": cn_name,
"dataAssetCatalogAstno": ast_no
"dataAssetCatalogAstno": ast_no,
"rel_status": rel_status
})
results = list(sys_groups.values())
# 转换为最终列表格式
@ -410,23 +411,6 @@ class DataCatalogService:
logger.error(" 取消收藏,操作失败")
# @classmethod
# async def add_ast_book_mark_rela_services(cls, db: AsyncSession, request: DataAstBookmarkRelaRequest):
# """
# 添加收藏数据库操作
# """
# add_rela_onum = {
# 'user_id': request.user_id,
# 'data_ast_no': request.data_ast_no
# }
# try:
# await DataCatalogDAO.add_ast_book_mark_rela_dao(db, add_rela_onum)
# await db.commit()
# return CrudResponseModel(is_success=True, message='添加收藏,操作成功')
# except Exception as e:
# await db.rollback()
# logger.error(" 添加收藏,操作失败")
@classmethod
async def add_ast_book_mark_rela_services(cls, db: AsyncSession, request: DataAstBookmarkRelaRequest, user_name: str):
@ -442,7 +426,7 @@ class DataCatalogService:
add_rela_onum = {
'user_id': request.user_id,
'data_ast_no': request.data_ast_no, # 我的收藏
'content_onum': 2,
'content_onum': request.content_onum,
'rela_type': "归属关系",
'rela_eff_begn_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 设置默认值,当前时间
'rela_eff_end_date': datetime(year=2999, month=12, day=31, hour=0, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S")
@ -494,43 +478,285 @@ class DataCatalogService:
message=f"收藏操作失败: {str(e)}"
)
# 添加到 DataCatalogService 类中
@classmethod
async def get_data_ast_indx_list_services(cls, query_db: AsyncSession, query_object: DataAstIndxRequest):
async def add_bookmark_folder_services(cls, db: AsyncSession, folder: DataCatalogRequest):
"""
添加收藏子目录服务
:param db: 数据库会话
:param folder: 目录请求对象
:return: 操作结果
"""
try:
# 检查是否已存在同名目录
existing_folder = await DataCatalogDAO.get_bookmark_folder_by_name(
db,
folder.content_name,
folder.upd_prsn
)
if existing_folder:
return CrudResponseModel(is_success=False, message="已存在同名收藏目录")
# 转换为字典并添加
folder_dict = folder.model_dump(exclude_unset=True)
result = await DataCatalogDAO.add_catalog_dao(db, folder_dict, {"children": []})
# 提交事务
await db.commit()
return CrudResponseModel(is_success=True, message="收藏目录添加成功")
except Exception as e:
await db.rollback()
logger.error(f"添加收藏目录失败: {str(e)}", exc_info=True)
return CrudResponseModel(is_success=False, message=f"添加收藏目录失败: {str(e)}")
@classmethod
async def edit_bookmark_folder_services(cls, db: AsyncSession, folder: DataCatalogRequest):
"""
获取数据资产指标列表信息service
修改收藏子目录服务
:param db: 数据库会话
:param folder: 目录请求对象
:return: 操作结果
"""
try:
indx_list_dict = await DataCatalogDAO.get_data_ast_indx_list(query_db, query_object)
# 检查目录是否存在且属于当前用户
existing_folder = await DataCatalogDAO.get_bookmark_folder_by_id(
db,
folder.content_onum,
folder.upd_prsn
)
if not existing_folder:
return CrudResponseModel(is_success=False, message="收藏目录不存在或无权限修改")
# 检查是否已存在同名目录(排除自身)
same_name_folder = await DataCatalogDAO.get_bookmark_folder_by_name_exclude_id(
db,
folder.content_name,
folder.upd_prsn,
folder.content_onum
)
if same_name_folder:
return CrudResponseModel(is_success=False, message="已存在同名收藏目录")
# 转换为字典并更新
folder_dict = folder.model_dump(exclude_unset=True)
folder_dict.update({
"supr_content_onum": 2, # 确保父目录不变
"content_stat": "1", # 添加状态字段
"leaf_node_flag": 1, # 添加叶子节点标志
"content_intr": folder_dict.get("content_intr", None), # 可选字段
"content_pic": folder_dict.get("content_pic", None) # 可选字段
})
await DataCatalogDAO.edit_catalog_dao(db, folder_dict)
# 提交事务
await db.commit()
return CrudResponseModel(is_success=True, message="收藏目录修改成功")
except Exception as e:
await db.rollback()
logger.error(f"修改收藏目录失败: {str(e)}", exc_info=True)
return CrudResponseModel(is_success=False, message=f"修改收藏目录失败: {str(e)}")
@classmethod
async def delete_bookmark_folder_services(cls, db: AsyncSession, content_onum: int, user_name: str,user_id:str):
"""
删除收藏子目录服务
:param db: 数据库会话
:param content_onum: 目录ID
:param user_name: 用户名
:return: 操作结果
"""
try:
# 检查目录是否存在且属于当前用户
existing_folder = await DataCatalogDAO.get_bookmark_folder_by_id(
db,
content_onum,
user_name
)
if not existing_folder:
return CrudResponseModel(is_success=False, message="收藏目录不存在或无权限删除")
# 使用专用方法执行删除操作
delete_result = await DataCatalogDAO.delete_bookmark_folder_dao(db, content_onum, user_name,user_id)
if not delete_result["success"]:
return CrudResponseModel(is_success=False, message=delete_result["message"])
# 提交事务
await db.commit()
return CrudResponseModel(is_success=True, message="收藏目录删除成功")
except Exception as e:
await db.rollback()
logger.error(f"删除收藏目录失败: {str(e)}", exc_info=True)
return CrudResponseModel(is_success=False, message=f"删除收藏目录失败: {str(e)}")
@classmethod
async def get_bookmark_folders_services(cls, db: AsyncSession, user_name: str):
"""
获取用户收藏目录列表服务
:param db: 数据库会话
:param user_name: 用户名
:return: 目录列表
"""
try:
# 获取用户的收藏目录列表
folders = await DataCatalogDAO.get_bookmark_folders(db, user_name)
return folders
except Exception as e:
logger.error(f"获取收藏目录列表失败: {str(e)}", exc_info=True)
return []
# 提取指标数据
@classmethod
async def move_bookmark_asset_services(cls, query_db: AsyncSession, request: DataCatalogMoverelRequest):
"""
在收藏目录间移动资产服务
"""
try:
# 1. 验证源目录和目标目录是否都属于收藏体系
source_is_bookmark = await DataCatalogDAO.is_bookmark_folder(query_db, request.content_onum, request.upd_prsn)
target_is_bookmark = await DataCatalogDAO.is_bookmark_folder(
query_db,
request.content_onum_after,
request.upd_prsn
)
if not source_is_bookmark:
return CrudResponseModel(is_success=False, message="源目录不是收藏目录")
if not target_is_bookmark:
return CrudResponseModel(is_success=False, message="目标目录不是收藏目录或不属于当前用户")
# 2. 构建移动数据
moverel_catalog_data = {
'rela_onum': request.rela_onum,
'content_onum': request.content_onum,
'content_onum_after': request.content_onum_after,
'upd_prsn': request.upd_prsn
}
# 3. 执行移动操作
await DataCatalogDAO.move_bookmark_asset_dao(query_db, moverel_catalog_data)
await query_db.commit()
return CrudResponseModel(is_success=True, message="收藏资产移动成功")
except Exception as e:
await query_db.rollback()
logger.error(f"移动收藏资产失败: {str(e)}", exc_info=True)
return CrudResponseModel(is_success=False, message=f"移动收藏资产失败: {str(e)}")
@classmethod
async def get_data_ast_indx_list_services(cls, query_db: AsyncSession, query_object: DataAstIndxRequest):
"""获取数据资产指标列表信息service"""
try:
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]
# 创建图表
# 创建独立图表
pie = (
Pie()
Pie(init_opts=InitOpts(width="100%", height="180px"))
.add("", list(zip(indx_names, indx_vals)))
.set_global_opts(title_opts={"text": "指标饼图"})
.set_global_opts(
title_opts=opts.TitleOpts(title="指标饼图", padding=[5,0,0,0]),
legend_opts=opts.LegendOpts(
type_="scroll",
orient="vertical",
pos_right="2%",
pos_top="10%"
)
)
.set_series_opts(label_opts=LabelOpts(formatter="{b}: {c}"))
)
bar = (
Bar()
Bar(init_opts=InitOpts(width="100%", height="250px"))
.add_xaxis(indx_names)
.add_yaxis("指标值", indx_vals)
.set_global_opts(title_opts={"text": "指标柱状图"})
.set_global_opts(
title_opts=opts.TitleOpts(title="指标柱状图", padding=[20,0,0,0]),
datazoom_opts=[opts.DataZoomOpts(type_="inside")]
)
)
# 组合图表
page = Page()
page.add(pie, bar)
# 生成独立图表HTML
pie_html = pie.render_embed()
bar_html = bar.render_embed()
# 构建最终HTML结构
responsive_html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="https://assets.pyecharts.org/assets/v5/echarts.min.js"></script>
<style>
html, body {{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}}
.chart-container {{
width: 100%;
height: 400px; /* 总高度根据实际需求调整 */
}}
.echarts {{
width: 100% !important;
height: 180px !important;
}}
</style>
</head>
<body>
<div class="chart-container">
{pie_html}
<br />
{bar_html}
<br />
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
// 统一初始化图表尺寸
const charts = document.querySelectorAll('[id^="chart_"]');
charts.forEach(chartDiv => {{
chartDiv.style.width = '100%';
chartDiv.style.height = '180px';
}});
// 响应式处理
function resizeCharts() {{
charts.forEach(chartDiv => {{
const chart = echarts.getInstanceByDom(chartDiv);
chart && chart.resize();
}});
}}
window.addEventListener('resize', resizeCharts);
resizeCharts();
}});
</script>
</body>
</html>
"""
except Exception as e:
logger.error(f"获取数据资产指标列表信息失败: {str(e)}")
raise ServiceException(message=f"获取数据资产指标列表信息失败: {str(e)}")
else:
return page.render_embed()
logger.info(f"获取数据资产指标列表信息成功")
return responsive_html

62
vue-fastapi-frontend/src/api/dataAsset/directory.js

@ -53,14 +53,6 @@ export function cancelDirectoryCollection(id) {
})
}
export function delDirectoryCollection(data) {
return request({
url: '/default-api/system/delete_data_asset_collection',
method: 'delete',
data,
})
}
export function moveDirectory(data) {
return request({
url: '/default-api/system/data_catalog/moved',
@ -108,3 +100,57 @@ export function getHtmlString(params) {
params,
})
}
export function getMetaSecurityData(params) {
return request({
url: '/default-api/system/data_catalog/getMetaSercuityData',
method: 'get',
params,
})
}
export function moveBookmarkAsset(data) {
return request({
url: '/default-api/system/data_catalog/bookmark/asset/move',
method: 'put',
data,
})
}
export function getBookmarkFolder(params) {
return request({
url: '/default-api/system/data_catalog/bookmark/folders',
method: 'get',
params,
})
}
export function deleteBookmarkFolder(id) {
return request({
url: `/default-api/system/data_catalog/bookmark/folder/${id}`,
method: 'delete',
})
}
export function addBookmarkFolder(data) {
return request({
url: '/default-api/system/data_catalog/bookmark/folder ',
method: 'post',
data,
})
}
export function updateBookmarkFolder(data) {
return request({
url: '/default-api/system/data_catalog/bookmark/folder ',
method: 'put',
data,
})
}
export function getAssetFieldTable(id) {
return request({
url: `/default-api/dasset/meta/getTable/${id}`,
method: 'get',
})
}

105
vue-fastapi-frontend/src/views/dataAsset/directory/components/AssetBookmarkDialog.vue

@ -0,0 +1,105 @@
<template>
<el-dialog width="600px" append-to-body :title="title" v-model="open">
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules">
<el-form-item label="当前资产">
<el-input
placeholder="自动带入"
:disabled="true"
v-model="currentRow.dataAstCnName"
/>
</el-form-item>
<el-form-item label="收藏目录" prop="contentOnum">
<el-select
placeholder="请选择收藏目录"
:disabled="disabled"
v-model="form.contentOnum"
>
<el-option
v-for="item in bookmarkFolder"
:key="item.content_onum"
:label="item.content_name"
:value="item.content_onum"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" :disabled="disabled" @click="submitForm"
>确定</el-button
>
</div>
</template>
</el-dialog>
</template>
<script setup>
import useUserStore from '@/store/modules/user'
import { computed, nextTick } from 'vue'
import {
getBookmarkFolder,
addDirectoryCollection,
} from '@/api/dataAsset/directory'
const userStore = useUserStore()
const title = ref('')
const open = ref(false)
const disabled = ref(false)
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
contentOnum: [
{ required: true, message: '收藏目录不能为空', trigger: 'blur' },
],
})
const bookmarkFolder = ref([])
const setBookmarkFolder = () => {
getBookmarkFolder().then(({ data }) => {
bookmarkFolder.value = data
})
}
const formRef = ref(null)
const currentRow = ref({})
const openDialog = (row) => {
open.value = true
form.value = {
dataAstNo: undefined,
userId: undefined,
contentOnum: undefined,
}
setBookmarkFolder()
if (row.dataAstNo) {
currentRow.value = row
form.value = {
...form.value,
dataAstNo: String(row.dataAstNo),
userId: String(userStore.id),
}
}
nextTick(() => {
formRef.value.clearValidate()
})
}
const emit = defineEmits(['onSuccess'])
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
addDirectoryCollection(form.value).then((response) => {
proxy.$modal.msgSuccess('收藏成功')
open.value = false
emit('onSuccess')
})
}
})
}
const cancel = () => {
open.value = false
}
defineExpose({ title, disabled, openDialog })
</script>

2
vue-fastapi-frontend/src/views/dataAsset/directory/components/AssetMoveDialog.vue

@ -107,7 +107,7 @@ const disabled = ref(false)
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
targetContentOnum: [
contentOnumAfter: [
{ required: true, message: '目标目录不能为空', trigger: 'blur' },
],
})

82
vue-fastapi-frontend/src/views/dataAsset/directory/components/BookmarkFormDialog.vue

@ -0,0 +1,82 @@
<template>
<el-dialog width="600px" append-to-body :title="title" v-model="open">
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules">
<el-form-item label="目录名称" prop="contentName">
<el-input
placeholder="请输入目录名称"
:disabled="disabled"
v-model="form.contentName"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" :disabled="disabled" @click="submitForm"
>确定</el-button
>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { computed, nextTick } from 'vue'
import {
addBookmarkFolder,
updateBookmarkFolder,
} from '@/api/dataAsset/directory'
const title = ref('')
const open = ref(false)
const disabled = ref(false)
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
contentName: [
{ required: true, message: '目录名称不能为空', trigger: 'blur' },
],
})
const formRef = ref(null)
const openDialog = (row) => {
open.value = true
form.value = {
contentName: undefined,
contentStat: '1',
}
if (row.contentOnum) {
form.value = {
...form.value,
...row,
}
}
nextTick(() => {
formRef.value.clearValidate()
})
}
const emit = defineEmits(['onSuccess'])
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
const request = form.value.contentOnum
? updateBookmarkFolder
: addBookmarkFolder
request(form.value).then((response) => {
proxy.$modal.msgSuccess(
form.value.contentOnum ? '修改成功' : '新增成功'
)
open.value = false
emit('onSuccess')
})
}
})
}
const cancel = () => {
open.value = false
}
defineExpose({ title, disabled, openDialog })
</script>

117
vue-fastapi-frontend/src/views/dataAsset/directory/components/BookmarkMoveDialog.vue

@ -0,0 +1,117 @@
<template>
<el-dialog width="800px" append-to-body :title="title" v-model="open">
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules">
<el-row :gutter="16">
<el-col :span="11">
<el-form-item label="当前资产" prop="dataAstCnName">
<el-input :disabled="true" v-model="form.dataAstCnName" />
</el-form-item>
</el-col>
<el-col :span="2">
<div class="arrow">
<span>········</span>
<el-icon><Right /></el-icon>
</div>
</el-col>
<el-col :span="11">
<el-form-item label="目标目录" prop="contentOnumAfter">
<el-select
placeholder="请选择收藏目录"
:disabled="disabled"
v-model="form.contentOnumAfter"
>
<el-option
v-for="item in bookmarkFolder"
:key="item.content_onum"
:label="item.content_name"
:value="item.content_onum"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" :disabled="disabled" @click="submitForm"
>确定</el-button
>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { nextTick } from 'vue'
import { getBookmarkFolder, moveBookmarkAsset } from '@/api/dataAsset/directory'
const title = ref('')
const open = ref(false)
const disabled = ref(false)
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
contentOnumAfter: [
{ required: true, message: '目标目录不能为空', trigger: 'blur' },
],
})
const bookmarkFolder = ref([])
const setBookmarkFolder = () => {
getBookmarkFolder().then(({ data }) => {
bookmarkFolder.value = data
})
}
const formRef = ref(null)
const openDialog = (row) => {
open.value = true
form.value = {
relaOnum: undefined,
contentOnum: undefined,
contentOnumAfter: undefined,
}
setBookmarkFolder()
if (row.relaOnum) {
form.value = {
...form.value,
...row,
}
}
nextTick(() => {
formRef.value.clearValidate()
})
}
const emit = defineEmits(['onSuccess'])
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
moveBookmarkAsset(form.value).then((response) => {
proxy.$modal.msgSuccess('移动成功')
open.value = false
emit('onSuccess')
})
}
})
}
const cancel = () => {
open.value = false
}
defineExpose({ title, disabled, openDialog })
</script>
<style lang="scss" scoped>
.arrow {
display: flex;
font-size: 18px;
text-align: center;
margin: 8px auto;
span {
line-height: 18px;
}
}
</style>

28
vue-fastapi-frontend/src/views/dataAsset/directory/components/FormDialog.vue

@ -38,7 +38,7 @@
filterable
multiple
show-checkbox
value-key="id"
value-key="dataAssetCatalogAstno"
placeholder="请选择关联资产"
:default-expand-all="true"
:render-after-expand="false"
@ -46,7 +46,7 @@
:clearable="true"
:data="assetTree"
:props="{
value: 'id',
value: 'dataAssetCatalogAstno',
label: 'dataAssetCatalogName',
children: 'children',
}"
@ -138,9 +138,11 @@ const rules = ref({
],
})
const currentRow = ref({})
const formRef = ref(null)
const openDialog = (row) => {
open.value = true
currentRow.value = row
setAssetTree()
form.value = {
contentName: undefined,
@ -167,11 +169,29 @@ const openDialog = (row) => {
})
}
const filterAssetTree = (tree) => {
return tree
.map((node, index) => ({
...node,
dataAssetCatalogAstno: node.dataAssetCatalogAstno || index,
})) //
.filter((node) => {
if (node.children) {
node.children = filterAssetTree(node.children) //
}
return node.rel_status !== 1 || hasAsset(node.dataAssetCatalogAstno)
})
}
const hasAsset = (astOnum) => {
return currentRow.value.children.find((i) => i.astOnum === astOnum)
}
const addTreeNodeId = (tree) => {
return tree.map((node, index) => {
return {
...node,
id: node.dataAssetCatalogAstno || index,
dataAssetCatalogAstno: node.dataAssetCatalogAstno || index,
children:
node.children && node.children.length
? addTreeNodeId(node.children)
@ -183,7 +203,7 @@ const addTreeNodeId = (tree) => {
const assetTree = ref([])
const setAssetTree = () => {
getDirectoryAsset().then(({ data }) => {
assetTree.value = addTreeNodeId(data)
assetTree.value = filterAssetTree(data)
})
}

341
vue-fastapi-frontend/src/views/dataAsset/directory/index.vue

@ -1,7 +1,7 @@
<template>
<div class="app-container">
<el-row :gutter="16">
<el-col :span="5">
<el-col :lg="7" :xl="5">
<el-card shadow="never">
<el-input
v-model="filterText"
@ -35,17 +35,14 @@
<el-icon v-else><Document /></el-icon>
<span>{{ data.contentName || data.dataAstCnName }}</span>
</el-space>
<div
v-if="!isCollectionDirectory(data)"
class="tree-node__action"
>
<div class="tree-node__action">
<template v-if="isAsset(data)">
<el-button
v-if="!isCollected(data)"
link
type="warning"
icon="Star"
@click="(e) => handleCollect(data, e)"
@click="(e) => handleAssetBookmarkDialogOpen(data, e)"
></el-button>
<el-button
v-else
@ -61,8 +58,75 @@
</template>
<el-dropdown
v-if="
!isCollection(data) &&
(isDirectory(data) || isRoot(data)) &&
isRoot(data) && hasPermiOr(['dataAsset:directory:add'])
"
placement="right-start"
@command="(command) => handleCommand(command, data)"
>
<el-button
style="margin-left: 4px"
link
type="primary"
icon="Menu"
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleAddDialogOpen">
新增目录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
v-if="isCollectionDirectory(data)"
placement="right-start"
@command="(command) => handleCommand(command, data)"
>
<el-button
style="margin-left: 4px"
link
type="primary"
icon="Menu"
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleBookmarkFormAddDialogOpen"
>
新增目录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
v-if="isCollectionSubDirectory(data)"
placement="right-start"
@command="(command) => handleCommand(command, data)"
>
<el-button
style="margin-left: 4px"
link
type="primary"
icon="Menu"
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleBookmarkFormEditDialogOpen"
>
修改目录
</el-dropdown-item>
<el-dropdown-item
command="handleBookmarkFolderDelete"
>
删除目录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
v-if="
isDirectory(data) &&
hasPermiOr([
'dataAsset:directory:add',
'dataAsset:directory:edit',
@ -83,52 +147,40 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="
isRoot(data) &&
hasPermiOr(['dataAsset:directory:add'])
"
v-if="hasPermiOr(['dataAsset:directory:add'])"
command="handleAddDialogOpen"
>
新增目录
</el-dropdown-item>
<template v-if="isDirectory(data)">
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:add'])"
command="handleAddDialogOpen"
>
新增目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:edit'])"
command="handleEditDialogOpen"
>
修改目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:remove'])"
command="handleDelete"
>
删除目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:move'])"
command="handleMoveDialogOpen"
>
移动目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:merge'])"
command="handleMergerDialogOpen"
>
合并目录
</el-dropdown-item>
</template>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:edit'])"
command="handleEditDialogOpen"
>
修改目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:remove'])"
command="handleDelete"
>
删除目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:move'])"
command="handleMoveDialogOpen"
>
移动目录
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:directory:merge'])"
command="handleMergerDialogOpen"
>
合并目录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
v-if="
!isCollection(data) &&
isAsset(data) &&
hasPermiOr([
'dataAsset:asset:remove',
@ -147,17 +199,31 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:asset:remove'])"
v-if="
!isAssetInCollection(data) &&
hasPermiOr(['dataAsset:asset:remove'])
"
command="handleAssetDelete"
>
删除资产
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiOr(['dataAsset:asst:move'])"
v-if="
!isAssetInCollection(data) &&
hasPermiOr(['dataAsset:asst:move'])
"
command="handleAssetMoveDialogOpen"
>
移动资产
</el-dropdown-item>
<el-dropdown-item
v-if="
isAssetInCollection(data) && isCollected(data)
"
command="handleBookmarkMoveDialogOpen"
>
移动收藏
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@ -168,7 +234,7 @@
</div>
</el-card>
</el-col>
<el-col :span="19">
<el-col :lg="17" :xl="19">
<el-card shadow="never">
<template v-if="currentNode.contentOnum && !currentNode.astOnum">
<el-descriptions
@ -208,6 +274,7 @@
ref="iframe"
:srcdoc="htmlContent"
:style="iframeStyle"
:onLoad="handleIframeLoad"
></iframe>
</el-card>
</el-col>
@ -225,22 +292,25 @@
</el-descriptions>
<el-tabs style="margin-top: 8px" v-model="activeName">
<el-tab-pane label="资产字段" name="1">
<el-table :data="[]" border>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="" label="字段中文名" />
<el-table-column prop="" label="字段英文名" />
<el-table-column prop="" label="字段类型" />
<el-table-column prop="" label="枚举" />
<el-table-column prop="" label="有值率" />
<el-table-column prop="" label="说明" />
<el-table :data="assetFieldData" border>
<el-table-column prop="fldNo" label="序号" width="60" />
<el-table-column prop="fldCnName" label="字段中文名" />
<el-table-column prop="fldEngName" label="字段英文名" />
<el-table-column prop="fldType" label="字段类型" />
<el-table-column prop="batchColClas" label="枚举" />
<el-table-column prop="fldNullRate" label="有值率" />
<el-table-column prop="fldDesc" label="说明" />
</el-table>
</el-tab-pane>
<el-tab-pane label="样例数据" name="2">
<el-table :data="[]" border>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="" label="股票代码" />
<el-table-column prop="" label="股票名称" />
<el-table-column prop="" label="股票价格" />
<el-table :data="metaSecurityData" border>
<el-table-column
v-for="(column, index) in tablesRowCol"
min-width="180"
:key="index"
:prop="column.prop"
:label="column.label || column.prop"
/>
</el-table>
</el-tab-pane>
<el-tab-pane label="常见问题" name="3">
@ -273,6 +343,18 @@
:directoryTree="directoryTree"
@onSuccess="setDirectoryTree"
/>
<BookmarkFormDialog
ref="bookmarkFormDialogRef"
@onSuccess="setDirectoryTree"
/>
<AssetBookmarkDialog
ref="assetBookmarkDialogRef"
@onSuccess="setDirectoryTree"
/>
<BookmarkMoveDialog
ref="bookmarkMoveDialogRef"
@onSuccess="setDirectoryTree"
/>
</div>
</template>
@ -286,17 +368,21 @@ import {
delDirectoryAsset,
addDirectoryCollection,
cancelDirectoryCollection,
getMetaSecurityData,
deleteBookmarkFolder,
getAssetFieldTable,
} from '@/api/dataAsset/directory'
import auth from '@/plugins/auth'
import FormDialog from './components/FormDialog.vue'
import MoveDialog from './components/MoveDialog.vue'
import MergerDialog from './components/MergerDialog.vue'
import AssetMoveDialog from './components/AssetMoveDialog.vue'
import useUserStore from '@/store/modules/user'
import BookmarkFormDialog from './components/BookmarkFormDialog.vue'
import AssetBookmarkDialog from './components/AssetBookmarkDialog.vue'
import BookmarkMoveDialog from './components/BookmarkMoveDialog.vue'
import { nextTick } from 'vue'
const { proxy } = getCurrentInstance()
const { hasPermiOr } = auth
const userStore = useUserStore()
const defaultProps = {
children: 'children',
@ -345,9 +431,6 @@ setDirectoryTree().then(async () => {
currentNode.value = directoryTree.value[0]
directoryTableData.value = directoryTree.value[0].children || []
await setHtmlContent(currentNode.value)
setTimeout(() => {
setIframeSize()
}, 300)
}
})
@ -376,22 +459,31 @@ const isRoot = (data) => {
//
const isCollectionDirectory = (data) => {
return data.contentName === '我的收藏'
return data.contentOnum === 2 && !data.relaOnum
}
//
const isCollection = (data) => {
return false
//
const isCollectionSubDirectory = (data) => {
return data.suprContentOnum === 2
}
//
const isCollected = (data) => {
return data.bookmarkFlag === 1
}
//
const isAssetInCollection = (data) => {
return data.sczcFlag === 1
}
//
const isDirectory = (data) => {
return data.contentOnum && !isRoot(data) && !data.astOnum
return (
data.contentOnum &&
!isRoot(data) &&
!isCollectionDirectory(data) &&
!isCollectionSubDirectory(data) &&
!data.astOnum
)
}
//
@ -400,32 +492,55 @@ const isAsset = (data) => {
}
const activeName = ref('1')
const handleNodeClick = async (data) => {
if (isCollectionDirectory(data)) {
const faq = ref(``)
const metaSecurityData = ref([])
const assetFieldData = ref([])
const tablesRowCol = ref([])
const handleNodeClick = async (node) => {
if (isCollectionDirectory(node)) {
return
}
tablesRowCol.value = []
assetFieldData.value = []
metaSecurityData.value = []
activeName.value = '1'
currentNode.value = {
...data,
...node,
}
directoryTableData.value = data.children
if (!data.astOnum) {
await setHtmlContent(data)
setTimeout(() => {
setIframeSize()
}, 300)
directoryTableData.value = node.children
if (!node.astOnum) {
await setHtmlContent(node)
} else {
faq.value = node.dataAstFaq
getMetaSecurityData({
dataAstSrc: node.dataAstSrc,
dataAstEngName: node.dataAstEngName,
})
.then(({ data }) => {
metaSecurityData.value = data.data
tablesRowCol.value = data.tablesRowCol
.split(',')
.map((i) => ({ prop: i, label: '' }))
return getAssetFieldTable(node.astNo)
})
.then(({ data }) => {
assetFieldData.value = data.columnList || []
tablesRowCol.value = tablesRowCol.value.map((i) => {
const item = assetFieldData.value.find((j) => j.fldEngName === i.prop)
if (item) {
i.label = item.fldCnName
}
return i
})
})
}
}
const handleCollect = (data, e) => {
const assetBookmarkDialogRef = ref(null)
const handleAssetBookmarkDialogOpen = (data, e) => {
e.stopPropagation()
addDirectoryCollection({
dataAstNo: String(data.dataAstNo),
userId: String(userStore.id),
}).then(() => {
proxy.$modal.msgSuccess('收藏成功')
setDirectoryTree()
})
assetBookmarkDialogRef.value.title = '新增收藏'
assetBookmarkDialogRef.value.openDialog(data)
}
const handleCollectionCancel = (data, e) => {
@ -500,6 +615,40 @@ const handleAssetMoveDialogOpen = (data) => {
assetMoveDialogRef.value.openDialog(data)
}
const bookmarkFormDialogRef = ref(null)
const handleBookmarkFormAddDialogOpen = (data) => {
bookmarkFormDialogRef.value.title = '新增收藏目录'
bookmarkFormDialogRef.value.openDialog()
}
const handleBookmarkFormEditDialogOpen = (data) => {
bookmarkFormDialogRef.value.title = '修改收藏目录'
bookmarkFormDialogRef.value.openDialog(data)
}
const handleBookmarkFolderDelete = (data) => {
ElMessageBox.confirm(
`确定删除 ${data.contentName} 目录及其收藏吗?`,
'目录删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
deleteBookmarkFolder(data.contentOnum).then(() => {
proxy.$modal.msgSuccess('删除成功')
setDirectoryTree()
})
})
}
const bookmarkMoveDialogRef = ref(null)
const handleBookmarkMoveDialogOpen = (data) => {
bookmarkMoveDialogRef.value.title = '移动收藏'
bookmarkMoveDialogRef.value.openDialog(data)
}
const handleCommand = (command, data) => {
const strategy = {
handleAddDialogOpen: handleAddDialogOpen,
@ -509,12 +658,14 @@ const handleCommand = (command, data) => {
handleMergerDialogOpen: handleMergerDialogOpen,
handleAssetDelete: handleAssetDelete,
handleAssetMoveDialogOpen: handleAssetMoveDialogOpen,
handleBookmarkFormAddDialogOpen: handleBookmarkFormAddDialogOpen,
handleBookmarkFormEditDialogOpen: handleBookmarkFormEditDialogOpen,
handleBookmarkFolderDelete: handleBookmarkFolderDelete,
handleBookmarkMoveDialogOpen: handleBookmarkMoveDialogOpen,
}
strategy[command](data)
}
const faq = `1、常见问题1\n2、常见问题2\n3、常见问题3`
const iframeStyle = ref({
width: '100%',
height: '100%',
@ -539,13 +690,17 @@ const setIframeSize = () => {
content.body.clientHeight,
content.documentElement.clientHeight
)
console.log('width', width)
console.log('height', height)
iframeStyle.value = {
width: `${width}px`,
height: `${height}px`,
}
}
const handleIframeLoad = () => {
nextTick(() => {
setIframeSize()
})
}
</script>
<style lang="scss" scoped>

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

@ -0,0 +1,633 @@
<template>
<div ref="aiChatRef" class="ai-chat">
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
<div ref="dialogScrollbar" class="ai-chat__content p-24 chat-width" style="padding: 24px;">
<div class="item-content mb-16" style="margin-bottom: 16px">
<div class="avatar">
<img src="@/assets/logo/logo2.png" height="30px" />
</div>
<div class="content">
<el-card shadow="always" class="dialog-card">
<span style="font-size: 14px">您好我是 果知小助手您可以向我提出关于 果知的相关问题</span>
</el-card>
</div>
</div>
<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 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>
</div>
<!-- 回答 -->
<div v-if="item.type === 'answer'" 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 class="content">
<el-card shadow="always" class="dialog-card">
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
</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>
</div>
</div>
</div>
</template>
</div>
</el-scrollbar>
<div class="ai-chat__operate p-24" style="padding: 24px">
<!-- <slot name="operateBefore" />-->
<div class="operate-textarea flex chat-width">
<el-input
ref="quickInputRef"
v-model="inputValue"
: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>
</div>
</div>
</div>
</div>
</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 Cookies from "js-cookie";
import {addChat} from "@/api/aichat/aichat";
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,
default: () => {}
},
record: {
type: Array,
default: () => []
},
chatId: {
type: String,
default: ''
}, // Id
debug: {
type: Boolean,
default: false
}
})
const { proxy } = getCurrentInstance();
const aiChatRef = ref()
const quickInputRef = ref()
const scrollDiv = ref()
const dialogScrollbar = ref()
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'])
function setScrollBottom() {
//
scrollDiv.value.setScrollTop(getMaxHeight())
}
/**
* 滚动条距离最上面的高度
*/
const scrollTop = ref(0)
const getMaxHeight = () => {
return dialogScrollbar.value.scrollHeight
}
const handleScrollTop = ($event) => {
scrollTop.value = $event.scrollTop
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
}
const handleScroll = () => {
if (scrollDiv.value) {
//
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) {
//
scrollDiv.value.setScrollTop(getMaxHeight())
}
}
}
/**文件上传中处理 */
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) => {
if (val && val !== 'new') {
chartOpenId.value = val
} else {
chartOpenId.value = ''
}
},
{ deep: true }
)
watch(
() => props.record,
(value) => {
chatList.value = value
},
{
immediate: true
}
)
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;
}
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 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({
"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": []
}
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,
() => {
nextTick(() => {
//
scrollDiv.value.setScrollTop(getMaxHeight())
})
},{
deep:true, immediate:true
}
)
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, '');
} 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"})
}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
}else{
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
}
}else{
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
}
}
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 (e) {
return Promise.reject(e);
}
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)
}
const startChat = (index) => {
regenerationChart(index)
}
defineExpose({
setScrollBottom
})
</script>
<style lang="scss" scoped>
.ai-chat {
--padding-left: 40px;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
position: relative;
color: #1f2329;
&__content {
height: auto;
padding-top: 0;
box-sizing: border-box;
.avatar {
float: left;
}
.content {
padding-left: var(--padding-left);
:deep(ol) {
margin-left: 16px !important;
}
}
.text {
padding: 6px 0;
}
.problem-button {
width: 100%;
border: none;
border-radius: 8px;
background: #f5f6f7;
height: 46px;
padding: 0 12px;
line-height: 46px;
box-sizing: border-box;
color: #1f2329;
-webkit-line-clamp: 1;
word-break: break-all;
&:hover {
background: var(--el-color-primary-light-9);
}
&.disabled {
&:hover {
background: #f5f6f7;
}
}
:deep(.el-icon) {
color: var(--el-color-primary);
}
}
}
&__operate {
background: #f3f7f9;
position: relative;
width: 100%;
box-sizing: border-box;
z-index: 10;
&:before {
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
content: '';
position: absolute;
width: 100%;
top: -16px;
left: 0;
height: 16px;
}
.operate-textarea {
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #ffffff;
box-sizing: border-box;
&:has(.el-textarea__inner:focus) {
border: 1px solid var(--el-color-primary);
scrollbar-width: none;
}
:deep(.el-textarea__inner) {
border-radius: 8px !important;
box-shadow: none;
resize: none;
padding: 12px 16px;
box-sizing: border-box;
scrollbar-width: none;
}
.operate {
padding: 6px 10px;
.el-icon {
font-size: 20px;
}
.sent-button {
max-height: none;
.el-icon {
font-size: 24px;
}
}
:deep(.el-loading-spinner) {
margin-top: -15px;
.circular {
width: 31px;
height: 31px;
}
}
}
}
}
.dialog-card {
border: none;
border-radius: 8px;
box-sizing: border-box;
}
}
.chat-width {
max-width: 80%;
margin: 0 auto;
}
.machineDiv {
cursor: pointer;
padding-top: 3px;
padding-bottom: 3px;
}
.machineDiv:hover {
background-color: #ebf1ff;
font-weight: bold;
}
@media only screen and (max-width: 1000px) {
.chat-width {
max-width: 100% !important;
margin: 0 auto;
}
}
</style>

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

@ -1,9 +1,206 @@
<template>
<div>数据问答</div>
<div class="chat-embed layout-bg">
<div class="chat-embed__header">
<div class="chat-width flex align-center" style="height: 56px;align-items: center;line-height: 56px">
<div class="mr-12 ml-24 flex">
<el-avatar
shape="square"
:size="32"
style="background: none"
>
<img src="@/assets/logo/deepseek.png" alt="" />
</el-avatar>
</div>
<h4 style="color: #1f2329;font-size: 16px; font-style: normal; font-weight: bold;margin: 0; -webkit-font-smoothing: antialiased">果知小助手</h4>
</div>
</div>
<div class="chat-embed__main">
<dataquerychat
ref="AiChatRef"
:record="currentRecordList"
:is_large="is_large"
:cookieSessionId = "sessionId"
class="AiChat-embed"
@scroll="handleScroll"
></dataquerychat>
</div>
</div>
</template>
<script setup>
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 chatLogeData = ref([])
const show = ref(false)
const currentRecordList = ref([])
const sessionId = ref(Cookies.get("chatSessionId")) // Id 'new'
const mouseId = ref('')
function handleScroll(event) {
if (event.scrollTop === 0 && currentRecordList.value.length>0) {
const history_height = event.dialogScrollbar.offsetHeight
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
}
}
function clickListHandle(item){
getChatList(item.sessionId).then(res=>{
currentRecordList.value = []
let array = res.data
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)
}
currentRecordList.value.push(array[i])
AiChatRef.value.setScrollBottom()
}
show.value = false
sessionId.value = item.sessionId
Cookies.set("chatSessionId",sessionId.value)
})
}
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)
}
}
}
currentRecordList.value = array
}).then(()=>{
AiChatRef.value.setScrollBottom()
})
}else {
Cookies.set("chatSessionId",uuidv4())
}
}
)
</script>
<style scoped lang="scss">
</style>
<style lang="scss">
.chat-embed {
overflow: hidden;
height: 100%;
&__header {
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);;
position: absolute;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: 56px;
line-height: 56px;
box-sizing: border-box;
border-bottom: 1px solid #dee0e3;
}
&__main {
padding-top: 80px;
height: 100%;
overflow: hidden;
}
.new-chat-button {
z-index: 11;
}
//
.chat-popover {
position: absolute;
top: 56px;
background: #ffffff;
padding-bottom: 24px;
z-index: 2009;
}
.chat-popover-button {
z-index: 100;
position: absolute;
top: 18px;
right: 85px;
font-size: 20px;
}
.chat-popover-mask {
background-color: var(--el-overlay-color-lighter);
bottom: 0;
height: 100%;
left: 0;
overflow: auto;
position: absolute;
right: 0;
top: 56px;
z-index: 2008;
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.AiChat-embed {
.ai-chat__operate {
padding-top: 12px;
}
}
.chat-width {
max-width: 860px;
margin: 0 auto;
}
}
.flex {
display: flex;
}
.mr-12 {
margin-right: 12px;
}
.ml-24 {
margin-left: 24px;
}
</style>

Loading…
Cancel
Save