mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-02 10:45:27 +08:00
v3.8.1发布,上传前端代码
This commit is contained in:
@ -272,6 +272,12 @@ export const schemas: FormSchema[] = [
|
||||
span: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'user4',
|
||||
component: 'JEllipsis',
|
||||
label: '选中用户',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'role2',
|
||||
component: 'JSelectRole',
|
||||
|
||||
@ -5,6 +5,7 @@ export enum Api {
|
||||
//知识库管理
|
||||
list = '/airag/app/list',
|
||||
save = '/airag/app/edit',
|
||||
release = '/airag/app/release',
|
||||
delete = '/airag/app/delete',
|
||||
queryById = '/airag/app/queryById',
|
||||
queryBathById = '/airag/knowledge/query/batch/byId',
|
||||
@ -44,6 +45,17 @@ export const saveApp = (params) => {
|
||||
return defHttp.put({ url: Api.save, params });
|
||||
};
|
||||
|
||||
// 发布应用
|
||||
export function releaseApp(appId: string, release = false) {
|
||||
return defHttp.post({
|
||||
url: Api.release,
|
||||
params: {
|
||||
id: appId,
|
||||
release: release,
|
||||
}
|
||||
}, {joinParamsToUrl: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除应用
|
||||
* @param params
|
||||
@ -77,5 +89,15 @@ export const queryFlowById = (params) => {
|
||||
* @param params
|
||||
*/
|
||||
export const promptGenerate = (params) => {
|
||||
return defHttp.get({ url: Api.promptGenerate, params,timeout: 5 * 60 * 1000 }, { isTransformResponse: false });
|
||||
return defHttp.post(
|
||||
{
|
||||
url: Api.promptGenerate+'?prompt='+ params.prompt,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -42,7 +42,7 @@ export const formSchema: FormSchema[] = [
|
||||
label: '选择应用类型',
|
||||
field: 'type',
|
||||
component: 'Input',
|
||||
ifShow:({ values })=>{
|
||||
show:({ values })=>{
|
||||
return !values.id;
|
||||
},
|
||||
slot: 'typeSlot',
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
<!--知识库文档列表-->
|
||||
<template>
|
||||
<div class="p-2 knowledge">
|
||||
<div class="knowledge">
|
||||
<!--查询区域-->
|
||||
<div class="jeecg-basic-table-form-container">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
@keyup.enter.native="reload"
|
||||
@keyup.enter.native="searchQuery"
|
||||
:model="queryParam"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
style="background-color: #f7f8fc"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :lg="6">
|
||||
<a-col :xl="7" :lg="7" :md="8" :sm="24">
|
||||
<a-form-item name="name" label="应用名称">
|
||||
<JInput v-model:value="queryParam.name" placeholder="请输入应用名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6">
|
||||
<a-col :xl="7" :lg="7" :md="8" :sm="24">
|
||||
<a-form-item name="type" label="应用类型">
|
||||
<j-dict-select-tag v-model:value="queryParam.type" dict-code="ai_app_type" placeholder="请选择应用类型" />
|
||||
</a-form-item>
|
||||
@ -25,7 +25,7 @@
|
||||
<a-col :xl="6" :lg="7" :md="8" :sm="24">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="reload">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
|
||||
</a-col>
|
||||
</span>
|
||||
@ -35,11 +35,10 @@
|
||||
</div>
|
||||
<a-row :span="24" class="knowledge-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
|
||||
<span style="line-height: 18px; font-weight: 500; color: #676f83; font-size: 12px">创建应用</span>
|
||||
<div class="add-knowledge-doc" @click="handleCreateApp">
|
||||
<Icon icon="ant-design:form-outlined" size="13"></Icon>
|
||||
<span>创建空白应用</span>
|
||||
<a-card class="add-knowledge-card" @click="handleCreateApp">
|
||||
<div class="flex">
|
||||
<Icon icon="ant-design:plus-outlined" class="add-knowledge-card-icon" size="20"></Icon>
|
||||
<span class="add-knowledge-card-title">创建应用</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@ -49,7 +48,11 @@
|
||||
<img class="header-img" :src="getImage(item.icon)" />
|
||||
<div class="header-text">
|
||||
<span class="header-text-top header-name ellipsis"> {{ item.name }} </span>
|
||||
<span class="header-text-top header-create ellipsis"> 创建者:{{ item.createBy }} </span>
|
||||
<span class="header-text-top header-create ellipsis">
|
||||
<a-tag v-if="item.status === 'release'" color="green">已发布</a-tag>
|
||||
<a-tag v-if="item.status === 'disable'">已禁用</a-tag>
|
||||
<span>创建者:{{ item.createBy_dictText || item.createBy }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-tag">
|
||||
@ -69,25 +72,41 @@
|
||||
<Icon class="operation" icon="ant-design:youtube-outlined" size="20" color="#1F2329"></Icon>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" style="float: left" />
|
||||
<a-tooltip title="删除">
|
||||
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
|
||||
<Icon icon="ant-design:delete-outlined" class="operation" size="20" color="#1F2329"></Icon>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<template v-if="item.status !== 'release'">
|
||||
<a-divider type="vertical" style="float: left" />
|
||||
<a-tooltip title="删除">
|
||||
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
|
||||
<Icon icon="ant-design:delete-outlined" class="operation" size="18" color="#1F2329"></Icon>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-divider type="vertical" style="float: left" />
|
||||
<a-tooltip title="发布">
|
||||
<a-dropdown class="card-footer-icon" placement="bottomRight" :trigger="['click']">
|
||||
<div @click.prevent.stop>
|
||||
<Icon icon="ant-design:send-outlined"></Icon>
|
||||
<Icon style="position: relative;top: 1px" icon="ant-design:send-outlined" size="16" color="#1F2329"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<template v-if="item.status === 'enable'">
|
||||
<a-menu-item key="release" @click.prevent.stop="handleSendClick(item,'release')">
|
||||
<Icon icon="lineicons:rocket-5" size="16"></Icon>
|
||||
发布
|
||||
</a-menu-item>
|
||||
<a-menu-divider/>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'release'">
|
||||
<a-menu-item key="un-release" @click.prevent.stop="handleSendClick(item,'un-release')">
|
||||
<Icon icon="tabler:rocket-off" size="16"></Icon>
|
||||
取消发布
|
||||
</a-menu-item>
|
||||
<a-menu-divider/>
|
||||
</template>
|
||||
<a-menu-item key="web" @click.prevent.stop="handleSendClick(item,'web')">
|
||||
<Icon icon="ant-design:dribbble-outlined" size="16"></Icon>
|
||||
嵌入网站
|
||||
</a-menu-item>
|
||||
<a-menu-item key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
|
||||
<a-menu-item v-if="isShowMenu" key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
|
||||
<Icon icon="ant-design:menu-outlined" size="16"></Icon> 配置菜单
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
@ -109,6 +128,7 @@
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
:show-total="() => `共${total}条` "
|
||||
/>
|
||||
<!-- Ai新增弹窗 -->
|
||||
<AiAppModal @register="registerModal" @success="handleSuccess"></AiAppModal>
|
||||
@ -120,7 +140,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
@ -131,10 +151,12 @@
|
||||
import AiAppSettingModal from './components/AiAppSettingModal.vue';
|
||||
import AiAppSendModal from './components/AiAppSendModal.vue';
|
||||
import Icon from '@/components/Icon';
|
||||
import { appList, deleteApp } from './AiApp.api';
|
||||
import { $electron } from "@/electron";
|
||||
import { appList, deleteApp, releaseApp } from './AiApp.api';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: 'AiAppList',
|
||||
@ -168,7 +190,7 @@
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerSettingModal, { openModal: openAppModal }] = useModal();
|
||||
const [registerAiAppSendModal, { openModal: openAiAppSendModal }] = useModal();
|
||||
const { createMessage } = useMessage();
|
||||
const { createMessage, createConfirmSync } = useMessage();
|
||||
//查询参数
|
||||
const queryParam = reactive<any>({});
|
||||
//查询区域label宽度
|
||||
@ -185,7 +207,7 @@
|
||||
});
|
||||
//表单的ref
|
||||
const formRef = ref();
|
||||
|
||||
|
||||
reload();
|
||||
|
||||
/**
|
||||
@ -263,14 +285,27 @@
|
||||
/**
|
||||
* 演示
|
||||
*/
|
||||
function handleViewClick(id) {
|
||||
window.open('/ai/app/chat/' + id , '_blank');
|
||||
function handleViewClick(id: string) {
|
||||
let url = '/ai/app/chat/' + id;
|
||||
|
||||
// update-begin--author:sunjianlei---date:20250411---for:【QQYUN-9685】构建 electron 桌面应用
|
||||
if ($electron.isElectron()) {
|
||||
url = $electron.resolveRoutePath(url);
|
||||
window.open(url, '_blank', 'width=1200,height=800');
|
||||
return
|
||||
}
|
||||
// update-end----author:sunjianlei---date:20250411---for:【QQYUN-9685】构建 electron 桌面应用
|
||||
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
function handleDeleteClick(item) {
|
||||
if(knowledgeAppDataList.value.length == 1 && pageNo.value > 1) {
|
||||
pageNo.value = pageNo.value - 1;
|
||||
}
|
||||
deleteApp({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
@ -280,22 +315,78 @@
|
||||
* @param type 类别
|
||||
*/
|
||||
function handleSendClick(item,type) {
|
||||
if (type === 'release' || type === 'un-release') {
|
||||
return onRelease(item);
|
||||
}
|
||||
|
||||
openAiAppSendModal(true,{
|
||||
type: type,
|
||||
data: item
|
||||
})
|
||||
}
|
||||
|
||||
async function onRelease(item) {
|
||||
const toRelease = item.status === 'enable';
|
||||
let flag = await createConfirmSync({
|
||||
title: toRelease ? '发布应用' : '取消发布应用',
|
||||
content: toRelease ? '确定要发布应用吗?发布后将不允许修改应用。' : '确定要取消发布应用吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
});
|
||||
if (!flag) {
|
||||
return
|
||||
}
|
||||
doRelease(item, item.status === 'enable');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布
|
||||
*/
|
||||
async function doRelease(item, release: boolean) {
|
||||
let success: boolean = await releaseApp(item.id, release);
|
||||
if (success) {
|
||||
// 发布成功
|
||||
if (release) {
|
||||
item.status = 'release'
|
||||
} else {
|
||||
item.status = 'enable'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
pageNo.value = 1;
|
||||
formRef.value.resetFields();
|
||||
queryParam.name = '';
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery(){
|
||||
pageNo.value = 1;
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
//是否显示菜单配置选项
|
||||
const isShowMenu = ref<boolean>(false);
|
||||
onMounted((()=>{
|
||||
let fullPath = router.currentRoute.value.fullPath;
|
||||
console.log(fullPath)
|
||||
if(fullPath === '/myapps/ai/app'){
|
||||
isShowMenu.value = false;
|
||||
} else {
|
||||
isShowMenu.value = true;
|
||||
}
|
||||
}))
|
||||
|
||||
return {
|
||||
handleCreateApp,
|
||||
knowledgeAppDataList,
|
||||
@ -320,6 +411,8 @@
|
||||
registerAiAppSendModal,
|
||||
searchReset,
|
||||
formRef,
|
||||
isShowMenu,
|
||||
searchQuery,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -327,30 +420,38 @@
|
||||
|
||||
<style scoped lang="less">
|
||||
.knowledge {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
height: calc(100vh - 115px);
|
||||
background: #f7f8fc;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.add-knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: inline-flex;
|
||||
font-size: 16px;
|
||||
height: 152px;
|
||||
width: calc(100% - 20px);
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
height: 152px;
|
||||
width: calc(100% - 20px);
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
color: #1f2329;
|
||||
background-color: #f5f6f7;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.add-knowledge-card-title {
|
||||
font-size: 16px;
|
||||
color:#1f2329;
|
||||
font-weight: 400;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
@ -360,7 +461,7 @@
|
||||
height: 152px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
.header-img {
|
||||
width: 40px;
|
||||
@ -396,10 +497,12 @@
|
||||
|
||||
.add-knowledge-card:hover,
|
||||
.knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
}
|
||||
|
||||
.knowledge-row {
|
||||
max-height: calc(100% - 100px);
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@ -455,7 +558,7 @@
|
||||
|
||||
.card-footer-icon:hover {
|
||||
color: #000000;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
background-color: #e9ecf2;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
|
||||
<template v-if="dataSource">
|
||||
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div v-if="isMultiSession" class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div class="content">
|
||||
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
|
||||
<slide :source="source" v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
|
||||
</div>
|
||||
<div class="toggle-btn" @click="handleToggle">
|
||||
<span class="icon">
|
||||
@ -30,6 +30,7 @@
|
||||
@reload-message-title="reloadMessageTitle"
|
||||
:chatTitle="chatTitle"
|
||||
:quickCommandData="quickCommandData"
|
||||
:showAdvertising = "showAdvertising"
|
||||
></chat>
|
||||
</div>
|
||||
</template>
|
||||
@ -46,6 +47,7 @@
|
||||
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
|
||||
const router = useRouter();
|
||||
const userId = useUserStore().getUserInfo?.id;
|
||||
@ -77,6 +79,8 @@
|
||||
const prologue = ref<string>('');
|
||||
//快捷指令
|
||||
const quickCommandData = ref<any>([]);
|
||||
//是否显示广告位
|
||||
const showAdvertising = ref<boolean>(false);
|
||||
|
||||
const priming = () => {
|
||||
dataSource.value = {
|
||||
@ -142,6 +146,13 @@
|
||||
);
|
||||
};
|
||||
|
||||
//是否为多会话模式
|
||||
const isMultiSession = ref<boolean>(true);
|
||||
//是否为手机
|
||||
const { getIsMobile } = useAppInject();
|
||||
//来源
|
||||
const source = ref<string>('');
|
||||
|
||||
/**
|
||||
* 初始化聊天信息
|
||||
* @param appId
|
||||
@ -184,6 +195,13 @@
|
||||
{ name: 'JEECG有哪些优势?', descr: "JEECG有哪些优势?" },
|
||||
{ name: 'JEECG可以做哪些事情?', descr: "JEECG可以做哪些事情?" },];
|
||||
}
|
||||
let query: any = router.currentRoute.value.query;
|
||||
source.value = query.source;
|
||||
if(query.source){
|
||||
showAdvertising.value = query.source === 'chatJs';
|
||||
}else{
|
||||
showAdvertising.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -203,7 +221,7 @@
|
||||
await defHttp
|
||||
.get(
|
||||
{
|
||||
url: '/airag/app/queryById',
|
||||
url: '/airag/chat/init',
|
||||
params: { id: appId },
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
@ -220,6 +238,23 @@
|
||||
if (res.result && res.result.presetQuestion) {
|
||||
presetQuestion.value = res.result.presetQuestion;
|
||||
}
|
||||
if (res.result && res.result.metadata) {
|
||||
let metadata = JSON.parse(res.result.metadata);
|
||||
//判斷是否为手机模式
|
||||
if(!getIsMobile.value){
|
||||
//是否为多会话模式
|
||||
if((metadata.multiSession && metadata.multiSession === '1') || !metadata.multiSession) {
|
||||
isMultiSession.value = true;
|
||||
} else {
|
||||
isMultiSession.value = false;
|
||||
expand.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(getIsMobile.value){
|
||||
isMultiSession.value = false;
|
||||
expand.value = false;
|
||||
}
|
||||
} else {
|
||||
appData.value = {};
|
||||
}
|
||||
@ -281,7 +316,7 @@
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
background: white;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@ -339,7 +374,7 @@
|
||||
color: rgb(51, 54, 57);
|
||||
border: 1px solid rgb(239, 239, 245);
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 2px 4px 0px #e7e9ef;
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@ -17,19 +17,19 @@
|
||||
import AiChat from './AiChat.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
//aiChat<EFBFBD><EFBFBD>ref
|
||||
//aiChat的ref
|
||||
const aiChatRef = ref();
|
||||
//Ӧ<EFBFBD><EFBFBD>id
|
||||
//应用id
|
||||
const appId = ref<string>('');
|
||||
|
||||
//<EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
//是否显示聊天
|
||||
const showChat = ref<any>(false);
|
||||
const router = useRouter();
|
||||
//<EFBFBD>ж<EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>
|
||||
//判断是否为初始化
|
||||
const isInit = ref<boolean>(false);
|
||||
|
||||
/**
|
||||
* chatͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
|
||||
* chat图标点击事件
|
||||
*/
|
||||
function chatClick() {
|
||||
showChat.value = !showChat.value;
|
||||
@ -67,7 +67,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
box-shadow: #cccccc 0 4px 8px 0;
|
||||
}
|
||||
.footer-close-icon {
|
||||
color: #0a3069;
|
||||
|
||||
@ -3,6 +3,13 @@
|
||||
<div class="content">
|
||||
<div class="header-title" v-if="type === 'view' && headerTitle">
|
||||
{{headerTitle}}
|
||||
<div v-if="showAdvertising" class="header-advertisint">
|
||||
AI客服由
|
||||
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
|
||||
敲敲云
|
||||
</a>
|
||||
提供
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div id="scrollRef" ref="scrollRef" class="scrollArea">
|
||||
@ -19,6 +26,8 @@
|
||||
:appData="appData"
|
||||
:presetQuestion="item.presetQuestion"
|
||||
:images = "item.images"
|
||||
:retrievalText="item.retrievalText"
|
||||
:referenceKnowledge="item.referenceKnowledge"
|
||||
@send="handleOutQuestion"
|
||||
></chatMessage>
|
||||
</div>
|
||||
@ -86,6 +95,7 @@
|
||||
autofocus
|
||||
:readonly="loading"
|
||||
style="border-color: #ffffff !important;box-shadow:none"
|
||||
@paste="paste"
|
||||
>
|
||||
</a-textarea>
|
||||
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
|
||||
@ -122,7 +132,7 @@
|
||||
>
|
||||
<a-tooltip title="图片上传,支持jpg/jpeg/png">
|
||||
<a-button class="sendBtn" type="text">
|
||||
<Icon icon="ant-design:picture-outlined" style="color: rgba(15,21,40,0.8)"></Icon>
|
||||
<Icon icon="ant-design:picture-outlined" style="color: #3d4353"></Icon>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-upload>
|
||||
@ -178,21 +188,22 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {getFileAccessHttpUrl, getHeaders} from "@/utils/common/compUtils";
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
import { useGlobSetting } from "@/hooks/setting";
|
||||
|
||||
message.config({
|
||||
prefixCls: 'ai-chat-message',
|
||||
});
|
||||
|
||||
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
|
||||
|
||||
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData','showAdvertising']);
|
||||
const emit = defineEmits(['save','reload-message-title']);
|
||||
const { scrollRef, scrollToBottom } = useScroll();
|
||||
const prompt = ref<string>('');
|
||||
const loading = ref<boolean>(false);
|
||||
const inputRef = ref<Ref | null>(null);
|
||||
const headerTitle = ref<string>(props.chatTitle);
|
||||
|
||||
|
||||
//聊天数据
|
||||
const chatData = ref<any>([]);
|
||||
//应用数据
|
||||
@ -202,15 +213,24 @@
|
||||
const topicId = ref<string>('');
|
||||
//请求id
|
||||
const requestId = ref<string>('');
|
||||
const { getIsMobile } = useAppInject();
|
||||
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
|
||||
const placeholder = computed(() => {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
if(getIsMobile.value){
|
||||
return '来说点什么吧...'
|
||||
} else {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
}
|
||||
});
|
||||
//token
|
||||
const headers = getHeaders();
|
||||
//文本域点击事件
|
||||
const textareaActive = ref<boolean>(false);
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const baseUploadUrl = globSetting.uploadUrl;
|
||||
const uploadUrl = ref<string>(`${baseUploadUrl}/airag/chat/upload`);
|
||||
|
||||
function handleEnter(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
@ -269,6 +289,7 @@
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: [],
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
@ -318,7 +339,7 @@
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: data,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
error: true,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: null,
|
||||
@ -370,19 +391,27 @@
|
||||
|
||||
handleStop();
|
||||
|
||||
const knowList = ref<Recordable[]>([])
|
||||
|
||||
/**
|
||||
* 停止消息
|
||||
*/
|
||||
function handleStopChat() {
|
||||
if(requestId.value){
|
||||
//调用后端接口停止响应
|
||||
defHttp.get({
|
||||
url: '/airag/chat/stop/' + requestId.value,
|
||||
},{ isTransformResponse: false });
|
||||
//update-begin---author:wangshuai---date:2025-06-03---for:【issues/8338】AI应用聊天回复stop无效,仍会继续输出回复---
|
||||
const currentRequestId = requestId.value
|
||||
if(currentRequestId){
|
||||
try{
|
||||
//调用后端接口停止响应
|
||||
defHttp.get({
|
||||
url: '/airag/chat/stop/' + currentRequestId,
|
||||
},{ isTransformResponse: false });
|
||||
} finally {
|
||||
handleStop();
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-06-03---for:【issues/8338】AI应用聊天回复stop无效,仍会继续输出回复---
|
||||
}
|
||||
handleStop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 读取文本
|
||||
* @param message
|
||||
@ -409,15 +438,16 @@
|
||||
};
|
||||
|
||||
if(headerTitle.value == '新建聊天'){
|
||||
headerTitle.value = message.length>5?message.substring(0,5):message
|
||||
headerTitle.value = message.length>10?truncateString(message,10):message
|
||||
}
|
||||
|
||||
emit("reload-message-title",message.length>5?message.substring(0,5):message)
|
||||
emit("reload-message-title",message.length>10?truncateString(message,10):message)
|
||||
}
|
||||
|
||||
uploadUrlList.value = [];
|
||||
fileInfoList.value = [];
|
||||
|
||||
knowList.value = [];
|
||||
|
||||
const readableStream = await defHttp.post(
|
||||
{
|
||||
url: props.url,
|
||||
@ -430,9 +460,18 @@
|
||||
isTransformResponse: false,
|
||||
}
|
||||
).catch((e)=>{
|
||||
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
|
||||
handleStop();
|
||||
return;
|
||||
//update-begin---author:wangshuai---date:2025-04-28---for:【QQYUN-12297】【AI】聊天,超时以后提示---
|
||||
if(e.code === 'ETIMEDOUT'){
|
||||
updateChatFail(uuid, chatData.value.length - 1, "当前用户较多,排队中,请稍候再次重试!");
|
||||
handleStop();
|
||||
return;
|
||||
}else{
|
||||
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
|
||||
handleStop();
|
||||
return;
|
||||
}
|
||||
console.error(e)
|
||||
//update-end---author:wangshuai---date:2025-04-28---for:【QQYUN-12297】【AI】聊天,超时以后提示---
|
||||
});
|
||||
const reader = readableStream.getReader();
|
||||
const decoder = new TextDecoder('UTF-8');
|
||||
@ -448,9 +487,9 @@
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
result = buffer + result;
|
||||
const lines = result.split('\n\n');
|
||||
for (const line of lines) {
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const content = line.replace('data:', '').trim();
|
||||
let content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
@ -461,6 +500,9 @@
|
||||
buffer = "";
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse,conversationId,text,options).then((res)=>{
|
||||
text = res.returnText;
|
||||
@ -482,6 +524,9 @@
|
||||
buffer = "";
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(line.indexOf(":::card:::") !== -1){
|
||||
line = line.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(line);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
@ -523,24 +568,42 @@
|
||||
async function renderText(item,conversationId,text,options) {
|
||||
let returnText = "";
|
||||
if (item.event == 'MESSAGE') {
|
||||
text = text + item.data.message;
|
||||
returnText = text;
|
||||
let message = item.data.message;
|
||||
let messageText = "";
|
||||
//update-begin---author:wangshuai---date:2025-04-24---for:应该先判断是否包含card---
|
||||
if(message && message.indexOf("::card::") !== -1){
|
||||
messageText = message;
|
||||
} else {
|
||||
text = text + item.data.message;
|
||||
messageText = text;
|
||||
returnText = text;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-04-24---for:应该先判断是否包含card---
|
||||
// 从消息中获取 requestId
|
||||
if (item.requestId) {
|
||||
requestId.value = item.requestId;
|
||||
}
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: text,
|
||||
content: messageText,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: knowList.value,
|
||||
});
|
||||
}
|
||||
if(item.event == 'INIT_REQUEST_ID'){
|
||||
if (item.requestId) {
|
||||
requestId.value = item.requestId;
|
||||
}
|
||||
}
|
||||
if (item.event == 'MESSAGE_END') {
|
||||
topicId.value = item.topicId;
|
||||
conversationId = item.conversationId;
|
||||
uuid.value = item.conversationId;
|
||||
requestId.value = item.requestId;
|
||||
handleStop();
|
||||
}
|
||||
if (item.event == 'FLOW_FINISHED') {
|
||||
@ -565,41 +628,60 @@
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
if(item.event === "NODE_STARTED"){
|
||||
let aiText = "";
|
||||
if(item.data.type === 'llm'){
|
||||
aiText = "正在构建响应内容";
|
||||
if(!item.data || item.data.type !== 'end'){
|
||||
let aiText = "";
|
||||
if(item.data.type === 'llm'){
|
||||
aiText = "正在构建响应内容";
|
||||
}
|
||||
if(item.data.type === 'knowledge'){
|
||||
aiText = "正在对知识库进行深度检索";
|
||||
}
|
||||
if(item.data.type === 'classifier'){
|
||||
aiText = "正在分类";
|
||||
}
|
||||
if(item.data.type === 'code'){
|
||||
aiText = "正在实施代码运行操作";
|
||||
}
|
||||
if(item.data.type === 'subflow'){
|
||||
aiText = "正在运行子流程";
|
||||
}
|
||||
if(item.data.type === 'enhanceJava'){
|
||||
aiText = "正在执行java增强";
|
||||
}
|
||||
if(item.data.type === 'http'){
|
||||
aiText = "正在发送http请求";
|
||||
}
|
||||
if(!text){
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
retrievalText: aiText,
|
||||
text:"",
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
referenceKnowledge: knowList.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
if(item.data.type === 'knowledge'){
|
||||
aiText = "正在对知识库进行深度检索";
|
||||
}
|
||||
if(item.data.type === 'classifier'){
|
||||
aiText = "正在分类";
|
||||
}
|
||||
if(item.data.type === 'code'){
|
||||
aiText = "正在实施代码运行操作";
|
||||
}
|
||||
if(item.data.type === 'subflow'){
|
||||
aiText = "正在运行子流程";
|
||||
}
|
||||
if(item.data.type === 'enhanceJava'){
|
||||
aiText = "正在执行java增强";
|
||||
}
|
||||
if(item.data.type === 'http'){
|
||||
aiText = "正在发送http请求";
|
||||
}
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: aiText,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
|
||||
else if (item.event === 'NODE_FINISHED') {
|
||||
if(!item.data || item.data.type !== 'end'){
|
||||
if(item.data.type === 'knowledge'){
|
||||
const id = item.data.id;
|
||||
const data = item.data.outputs[id + ".data"]
|
||||
knowList.value.push(data)
|
||||
//更新聊天信息
|
||||
updateChatSome(uuid.value, chatData.value.length - 1, {referenceKnowledge: knowList.value})
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!returnText){
|
||||
returnText = text;
|
||||
}
|
||||
return { returnText, conversationId };
|
||||
}
|
||||
|
||||
@ -607,7 +689,7 @@
|
||||
const uploadUrlList = ref<any>([]);
|
||||
//文件集合
|
||||
const fileInfoList = ref<any>([]);
|
||||
|
||||
|
||||
/**
|
||||
* 文件上传回调事件
|
||||
* @param info
|
||||
@ -615,8 +697,9 @@
|
||||
function handleChange(info) {
|
||||
let { fileList, file } = info;
|
||||
fileInfoList.value = fileList;
|
||||
if (file.status === 'error') {
|
||||
if (file.status === 'error' || (file.response && file.response.code == 500)) {
|
||||
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
|
||||
return;
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
uploadUrlList.value.push(file.response.message);
|
||||
@ -625,7 +708,7 @@
|
||||
|
||||
/**
|
||||
* 获取图片地址
|
||||
*
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function getImage(url) {
|
||||
@ -643,6 +726,10 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(uploadUrlList.value && uploadUrlList.value.length > 2){
|
||||
message.warning("最多只能上传三张!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -665,7 +752,85 @@
|
||||
let imageList = [getImage(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
* @param str
|
||||
* @param maxLength
|
||||
*/
|
||||
function truncateString(str, maxLength) {
|
||||
if (str.length <= maxLength){
|
||||
return str;
|
||||
}
|
||||
let chineseCount = 0;
|
||||
let englishCount = 0;
|
||||
let digitCount = 0;
|
||||
let result = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) { // 判断是否为汉字
|
||||
chineseCount++;
|
||||
} else if (/[a-zA-Z]/.test(char)) { // 判断是否为英文字母
|
||||
englishCount++;
|
||||
} else if (/\d/.test(char)) { // 判断是否为数字
|
||||
digitCount++;
|
||||
}
|
||||
if (chineseCount + englishCount / 2 + digitCount / 2 > maxLength) {
|
||||
break;
|
||||
}
|
||||
result += char;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘贴事件
|
||||
* @param event
|
||||
*/
|
||||
function paste(event) {
|
||||
if(uploadUrlList.value && uploadUrlList.value.length > 2){
|
||||
message.warning("最多只能上传三张!");
|
||||
return;
|
||||
}
|
||||
const items = (event.clipboardData || window.clipboardData).items;
|
||||
if (!items || items.length === 0){
|
||||
//说明浏览器不支持复制图片
|
||||
message.error('当前浏览器不支持本地打开图片!');
|
||||
return;
|
||||
}
|
||||
let image = null;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
image = items[i].getAsFile();
|
||||
handleUploadImage(image);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘贴图片
|
||||
* @param image
|
||||
*/
|
||||
async function handleUploadImage(image) {
|
||||
const isReturn = (fileInfo) => {
|
||||
try {
|
||||
if (fileInfo.code === 0) {
|
||||
let { message } = fileInfo;
|
||||
uploadUrlList.value.push(message);
|
||||
fileInfoList.value.push(image);
|
||||
} else if (fileInfo.code === 500 || fileInfo.code === 510) {
|
||||
message.error(fileInfo.message || `${image.name} 导入失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('导入的数据异常', error);
|
||||
message.error(`${image.name} 导入失败`);
|
||||
}
|
||||
};
|
||||
await defHttp.uploadFile({ url: "/airag/chat/upload" }, { file: image }, { success: isReturn });
|
||||
}
|
||||
|
||||
//监听开场白
|
||||
watch(
|
||||
() => props.prologue,
|
||||
@ -676,8 +841,8 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
);
|
||||
|
||||
);
|
||||
|
||||
//监听开场白预制问题
|
||||
watch(
|
||||
() => props.presetQuestion,
|
||||
@ -698,7 +863,7 @@
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
|
||||
//监听历史信息
|
||||
watch(
|
||||
() => props.historyData,
|
||||
@ -840,6 +1005,14 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
.header-advertisint{
|
||||
display:flex;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.chat-textarea{
|
||||
display: flex;
|
||||
@ -850,7 +1023,7 @@
|
||||
border-width: 1px;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
border-color: rgba(68,83,130,0.2);
|
||||
border-color: #d2d7e5;
|
||||
.textarea-top{
|
||||
border-bottom: 1px solid #f0f0f5;
|
||||
padding: 12px 28px;
|
||||
@ -881,10 +1054,10 @@
|
||||
}
|
||||
}
|
||||
.chat-textarea:hover{
|
||||
border-color: rgba(59,130,246,0.5)
|
||||
border-color: #9dc1fb;
|
||||
}
|
||||
.textarea-active{
|
||||
border-color: rgba(59,130,246,0.5) !important;
|
||||
border-color: #98bdfa !important;
|
||||
}
|
||||
:deep(.ant-divider-vertical){
|
||||
margin: 0 2px;
|
||||
@ -899,7 +1072,7 @@
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
margin-left: 44px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
@ -908,6 +1081,24 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
//手机下的样式 平板不需要调整
|
||||
.footer{
|
||||
padding: 0;
|
||||
.bottomArea{
|
||||
.delBtn{
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chatWrap{
|
||||
padding: 10px 10px 10px 0;
|
||||
}
|
||||
.main .chatContentArea{
|
||||
padding: 10px 0 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.ai-chat-modal{
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="avatar">
|
||||
<img v-if="inversion === 'user'" :src="avatar()" />
|
||||
<img v-else :src="getAiImg()">
|
||||
<img v-else :src="getAiImg()" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="date">
|
||||
@ -14,8 +14,24 @@
|
||||
<img :src="getImageUrl(item)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msgArea">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
|
||||
<div v-if="inversion === 'ai' && retrievalText && loading" class="retrieval">
|
||||
{{retrievalText}}
|
||||
</div>
|
||||
<div v-if="inversion === 'ai' && isCard" class="card">
|
||||
<a-row>
|
||||
<a-col :xl="6" :lg="8" :md="10" :sm="24" style="flex:1" v-for="item in getCardList()">
|
||||
<a-card class="ai-card" @click="aiCardHandleClick(item.linkUrl)">
|
||||
<div class="ai-card-title">{{item.productName}}</div>
|
||||
<div class="ai-card-img">
|
||||
<img :src="item.productImage">
|
||||
</div>
|
||||
<span class="ai-card-desc">{{item.descr}}</span>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div class="msgArea" v-if="!isCard">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading" :referenceKnowledge="referenceKnowledge"></chatText>
|
||||
</div>
|
||||
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
|
||||
<span>{{item.descr}}</span>
|
||||
@ -26,13 +42,31 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import chatText from './chatText.vue';
|
||||
import defaultAvatar from '/@/assets/images/ai/avatar.jpg';
|
||||
import defaultAvatar from "@/assets/images/ai/avatar.jpg";
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images']);
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images','retrievalText', 'referenceKnowledge']);
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
import { computed } from "vue";
|
||||
|
||||
const getText = computed(()=>{
|
||||
let text = props.text || props.retrievalText;
|
||||
if(text){
|
||||
text = text.trim();
|
||||
}
|
||||
return text;
|
||||
})
|
||||
|
||||
const isCard = computed(() => {
|
||||
let text = props.text;
|
||||
if (text && text.indexOf('::card::') != -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
const avatar = () => {
|
||||
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
|
||||
@ -44,7 +78,7 @@
|
||||
|
||||
/**
|
||||
* 预设问题点击事件
|
||||
*
|
||||
*
|
||||
*/
|
||||
function presetQuestionClick(descr) {
|
||||
emit("send",descr)
|
||||
@ -52,7 +86,7 @@
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
*
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function getImageUrl(item) {
|
||||
@ -77,7 +111,28 @@
|
||||
let imageList = [getImageUrl(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取卡片列表
|
||||
*/
|
||||
function getCardList() {
|
||||
let text = props.text;
|
||||
let card = text.replace('::card::', '').replace(/\s+/g, '');
|
||||
try {
|
||||
return JSON.parse(card);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ai卡片点击事件
|
||||
* @param url
|
||||
*/
|
||||
function aiCardHandleClick(url){
|
||||
window.open(url,'_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@ -112,6 +167,7 @@
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: 90%;
|
||||
.date {
|
||||
color: #b4bbc4;
|
||||
font-size: 0.75rem;
|
||||
@ -134,14 +190,15 @@
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
}
|
||||
|
||||
|
||||
.images{
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: end;
|
||||
.image{
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
@ -154,4 +211,76 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.retrieval,
|
||||
.card {
|
||||
background-color: #f4f6f8;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.retrieval:after{
|
||||
animation: blink 1s steps(5, start) infinite;
|
||||
color: #000;
|
||||
content: '_';
|
||||
font-weight: 700;
|
||||
margin-left: 3px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.card{
|
||||
width: 100%;
|
||||
background-color: unset;
|
||||
}
|
||||
.ai-card{
|
||||
width: 98%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
.ai-card-title{
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: #191919;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
.ai-card-img{
|
||||
margin-top: 10px;
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
.ai-card-desc{
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
white-space: pre-line;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
color: #666f;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.content{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
<template>
|
||||
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
|
||||
<div v-if="inversion != 'user'">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
|
||||
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" :style="{color:error?'#FF4444 !important':''}" v-html="text" />
|
||||
<template v-if="showRefKnow">
|
||||
<a-divider orientation="left">引用</a-divider>
|
||||
<template v-for="(item, idx) of referenceKnowledge" :key="idx">
|
||||
<a-tooltip :title="item.substring(0, 800)">
|
||||
<a-tag >
|
||||
<a-space>
|
||||
<img :src="knowledgePng" width="16" height="16"/>
|
||||
<div style="max-width: 240px; overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">
|
||||
{{ item }}
|
||||
</div>
|
||||
</a-space>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="msg" v-text="text" />
|
||||
<div v-else class="msg" v-html="text" />
|
||||
</div>
|
||||
<ImageViewer v-if="amplifyImage" :imageUrl="imageUrl" @hide="pictureHide"></ImageViewer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -16,11 +32,21 @@
|
||||
import './style/github-markdown.less';
|
||||
import './style/highlight.less';
|
||||
import './style/style.less';
|
||||
import ImageViewer from '@/views/super/airag/aiapp/chat/components/ImageViewer.vue';
|
||||
import { useAppInject } from "@/hooks/web/useAppInject";
|
||||
import { useGlobSetting } from "@/hooks/setting";
|
||||
import knowledgePng from '../../aiknowledge/icon/knowledge.png'
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
|
||||
/**
|
||||
* 屏幕宽度
|
||||
*/
|
||||
const screenWidth = ref<string>();
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading', 'referenceKnowledge']);
|
||||
const textRef = ref();
|
||||
const mdi = new MarkdownIt({
|
||||
html: false,
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language));
|
||||
@ -36,11 +62,49 @@
|
||||
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
|
||||
|
||||
const text = computed(() => {
|
||||
const value = props.text ?? '';
|
||||
if (props.inversion != 'user') return mdi.render(value);
|
||||
return value;
|
||||
let value = props.text ?? '';
|
||||
if (props.inversion != 'user'){
|
||||
value = replaceImageWith(value);
|
||||
value = replaceDomainUrl(value);
|
||||
return mdi.render(value);
|
||||
}
|
||||
return value.replace("\n","<br>");
|
||||
});
|
||||
|
||||
// 是否显示引用知识库
|
||||
const showRefKnow = computed(() => {
|
||||
const {loading, referenceKnowledge} = props
|
||||
if (loading) {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(referenceKnowledge) && referenceKnowledge.length > 0;
|
||||
})
|
||||
|
||||
//替换图片宽度
|
||||
const replaceImageWith = markdownContent => {
|
||||
// 支持图片设置width的写法 
|
||||
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
|
||||
return markdownContent.replace(regex, (match, alt, src, width) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
src = src.replace(reg,domainUrl);
|
||||
return `<div><img src='${src}' alt='${alt}' width='${width}' /></div>`;
|
||||
});
|
||||
};
|
||||
const { domainUrl } = useGlobSetting();
|
||||
//替换domainURL
|
||||
const replaceDomainUrl = markdownContent => {
|
||||
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
|
||||
return markdownContent.replace(regex, (match) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
return match.replace(reg,domainUrl);
|
||||
})
|
||||
}
|
||||
|
||||
//是否放大图片
|
||||
const amplifyImage = ref<boolean>(false);
|
||||
//图片地址
|
||||
const imageUrl = ref<string>('');
|
||||
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
|
||||
}
|
||||
@ -72,16 +136,74 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加图片点击事件
|
||||
*/
|
||||
function addImageClickEvent() {
|
||||
if (textRef.value) {
|
||||
const image = textRef.value.querySelectorAll('img');
|
||||
image.forEach((img) => {
|
||||
img.addEventListener('click', () => {
|
||||
imageUrl.value = img.src;
|
||||
amplifyImage.value = true;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移出图片点击事件
|
||||
*/
|
||||
function removeImageClickEvent(){
|
||||
if (textRef.value) {
|
||||
const image = textRef.value.querySelectorAll('img');
|
||||
image.forEach((img) => {
|
||||
img.removeEventListener('click', () => { })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片隐藏
|
||||
*/
|
||||
function pictureHide(){
|
||||
amplifyImage.value = false;
|
||||
imageUrl.value = ""
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置markdown body整体宽度
|
||||
*/
|
||||
function setMarkdownBodyWidth() {
|
||||
//平板
|
||||
console.log("window.innerWidth::",window.innerWidth)
|
||||
if(window.innerWidth>600 && window.innerWidth<1024){
|
||||
screenWidth.value = window.innerWidth - 120 + 'px';
|
||||
}else if(window.innerWidth < 600){
|
||||
//手机
|
||||
screenWidth.value = window.innerWidth - 60 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addCopyEvents();
|
||||
addImageClickEvent();
|
||||
setMarkdownBodyWidth();
|
||||
window.addEventListener('resize', setMarkdownBodyWidth);
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
addCopyEvents();
|
||||
addImageClickEvent();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeCopyEvents();
|
||||
removeImageClickEvent();
|
||||
window.removeEventListener('resize', setMarkdownBodyWidth);
|
||||
});
|
||||
|
||||
function copyToClip(text: string) {
|
||||
@ -111,6 +233,18 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: linear-gradient(135deg, #FF4444, #FF914D) !important;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.self {
|
||||
// background-color: #d2f9d1;
|
||||
background-color: @primary-color;
|
||||
@ -125,4 +259,11 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
//手机和平板下的样式
|
||||
.textWrap{
|
||||
margin-left: -40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
<!--image放大封装-->
|
||||
<template>
|
||||
<div class="amplify-image">
|
||||
<div class="img-preview-content" @click="hideImageClick" @mousewheel="handlePicMousewheel">
|
||||
<img :src="imageUrl" ref="imageRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//图片地址
|
||||
import {onMounted, ref, unref} from 'vue';
|
||||
const props = defineProps(['imageUrl']);
|
||||
const emit = defineEmits(['register', 'hide']);
|
||||
//图片的ref
|
||||
const imageRef = ref();
|
||||
//缩放级别
|
||||
const scale = ref<number>(1);
|
||||
|
||||
/**
|
||||
* 隐藏图片
|
||||
*/
|
||||
function hideImageClick() {
|
||||
scale.value = 1;
|
||||
emit('hide')
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标滑轮滚动
|
||||
* @param event
|
||||
*/
|
||||
function handlePicMousewheel(event) {
|
||||
event.preventDefault();
|
||||
// 判断是放大还是缩小
|
||||
const delta = event.deltaY > 0 ? -1 : 1;
|
||||
const scaleStep = 0.1;
|
||||
// 更新缩放级别
|
||||
scale.value = scale.value + delta * scaleStep
|
||||
imageRef.value.style.transform = `scale(${unref(scale)})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.amplify-image{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
.img-preview-content{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
touch-action: none;
|
||||
-webkit-user-drag: none;
|
||||
img{
|
||||
transition: transform 0.3s;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,38 +2,40 @@
|
||||
(function () {
|
||||
let widgetInstance = null;
|
||||
const defaultConfig = {
|
||||
// ֧<EFBFBD><EFBFBD>'top-left'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'top-right'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'bottom-left'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 'bottom-right'<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 支持'top-left'左上, 'top-right'右上, 'bottom-left'左下, 'bottom-right'右下
|
||||
iconPosition: 'bottom-right',
|
||||
//ͼ<EFBFBD><EFBFBD><EFBFBD>Ĵ<EFBFBD>С
|
||||
iconSize: '30px',
|
||||
//ͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
|
||||
//图标的大小
|
||||
iconSize: '45px',
|
||||
//图标的颜色
|
||||
iconColor: '#155eef',
|
||||
//<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
//必填不允许修改
|
||||
appId: '',
|
||||
//<EFBFBD><EFBFBD><EFBFBD>쵯<EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><EFBFBD><EFBFBD>
|
||||
//聊天弹窗的宽度
|
||||
chatWidth: '800px',
|
||||
//<EFBFBD><EFBFBD><EFBFBD>쵯<EFBFBD><EFBFBD><EFBFBD>ĸ߶<EFBFBD>
|
||||
//聊天弹窗的高度
|
||||
chatHeight: '700px',
|
||||
};
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD><EFBFBD><EFBFBD>aiͼ<EFBFBD><EFBFBD>
|
||||
* 创建ai图标
|
||||
* @param config
|
||||
*/
|
||||
function createAiChat(config) {
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD>ȷ<EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>
|
||||
// 单例模式,确保只存在一个实例
|
||||
if (widgetInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// <EFBFBD>ϲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 合并配置
|
||||
const finalConfig = { ...defaultConfig, ...config };
|
||||
|
||||
if (!finalConfig.appId) {
|
||||
console.error('appIdΪ<EFBFBD>գ<EFBFBD>');
|
||||
console.error('appId为空!');
|
||||
return;
|
||||
}
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
let body = document.body;
|
||||
body.style.margin = "0";
|
||||
// 创建容器
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
@ -41,39 +43,50 @@
|
||||
${getPositionStyles(finalConfig.iconPosition)}
|
||||
cursor: pointer;
|
||||
`;
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ<EFBFBD><EFBFBD>
|
||||
// 创建图标
|
||||
const icon = document.createElement('div');
|
||||
icon.style.cssText = `
|
||||
width: ${finalConfig.iconSize};
|
||||
height: ${finalConfig.iconSize};
|
||||
background-color: ${finalConfig.iconColor};
|
||||
border-radius: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
padding: 12px;
|
||||
box-shadow: #cccccc 0 4px 8px 0;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
icon.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" d="M573 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40m-280 0c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path><path fill="currentColor" d="M894 345c-48.1-66-115.3-110.1-189-130v.1c-17.1-19-36.4-36.5-58-52.1c-163.7-119-393.5-82.7-513 81c-96.3 133-92.2 311.9 6 439l.8 132.6c0 3.2.5 6.4 1.5 9.4c5.3 16.9 23.3 26.2 40.1 20.9L309 806c33.5 11.9 68.1 18.7 102.5 20.6l-.5.4c89.1 64.9 205.9 84.4 313 49l127.1 41.4c3.2 1 6.5 1.6 9.9 1.6c17.7 0 32-14.3 32-32V753c88.1-119.6 90.4-284.9 1-408M323 735l-12-5l-99 31l-1-104l-8-9c-84.6-103.2-90.2-251.9-11-361c96.4-132.2 281.2-161.4 413-66c132.2 96.1 161.5 280.6 66 412c-80.1 109.9-223.5 150.5-348 102m505-17l-8 10l1 104l-98-33l-12 5c-56 20.8-115.7 22.5-171 7l-.2-.1C613.7 788.2 680.7 742.2 729 676c76.4-105.3 88.8-237.6 44.4-350.4l.6.4c23 16.5 44.1 37.1 62 62c72.6 99.6 68.5 235.2-8 330"></path><path fill="currentColor" d="M433 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path></svg>';
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>iframe<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 创建iframe容器
|
||||
const iframeContainer = document.createElement('div');
|
||||
let right = finalConfig.chatWidth === '100%' ? '0' : '10px';
|
||||
let bottom = finalConfig.chatHeight === '100%' ? '0' : '10px';
|
||||
let chatWidth = finalConfig.chatWidth;
|
||||
let chatHeight = finalConfig.chatHeight;
|
||||
if(isMobileDevice()){
|
||||
chatWidth = "100%";
|
||||
chatHeight = "100%";
|
||||
right = '0';
|
||||
bottom = '0';
|
||||
}
|
||||
iframeContainer.style.cssText = `
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: ${finalConfig.chatWidth} !important;
|
||||
height: ${finalConfig.chatHeight} !important;
|
||||
position: fixed;
|
||||
right: ${right};
|
||||
bottom: ${bottom};
|
||||
width: ${chatWidth} !important;
|
||||
height: ${chatHeight} !important;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0 20px #cccccc;
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>iframe
|
||||
// 创建iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
@ -83,15 +96,23 @@
|
||||
`;
|
||||
|
||||
iframe.id = 'ai-app-chat-document';
|
||||
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId;
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>رհ<D8B1>ť
|
||||
//update-begin---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
|
||||
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId + "?source=chatJs";
|
||||
//update-end---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
|
||||
let iconRight = finalConfig.chatWidth === '100%'?'0':'-6px';
|
||||
let iconTop = finalConfig.chatWidth === '100%'?'0':'-9px';
|
||||
if(isMobileDevice()){
|
||||
iconRight = '2px';
|
||||
iconTop = '2px';
|
||||
}
|
||||
// 创建关闭按钮
|
||||
const closeBtn = document.createElement('div');
|
||||
closeBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" fill-rule="evenodd" d="M799.855 166.312c.023.007.043.018.084.059l57.69 57.69c.041.041.052.06.059.084a.1.1 0 0 1 0 .069c-.007.023-.018.042-.059.083L569.926 512l287.703 287.703c.041.04.052.06.059.083a.12.12 0 0 1 0 .07c-.007.022-.018.042-.059.083l-57.69 57.69c-.041.041-.06.052-.084.059a.1.1 0 0 1-.069 0c-.023-.007-.042-.018-.083-.059L512 569.926L224.297 857.629c-.04.041-.06.052-.083.059a.12.12 0 0 1-.07 0c-.022-.007-.042-.018-.083-.059l-57.69-57.69c-.041-.041-.052-.06-.059-.084a.1.1 0 0 1 0-.069c.007-.023.018-.042.059-.083L454.073 512L166.371 224.297c-.041-.04-.052-.06-.059-.083a.12.12 0 0 1 0-.07c.007-.022.018-.042.059-.083l57.69-57.69c.041-.041.06-.052.084-.059a.1.1 0 0 1 .069 0c.023.007.042.018.083.059L512 454.073l287.703-287.702c.04-.041.06-.052.083-.059a.12.12 0 0 1 .07 0Z"></path></svg>';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -10px;
|
||||
margin-top: ${iconTop};
|
||||
right: ${iconRight};
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
width: 25px;
|
||||
@ -100,17 +121,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 5px #cccccc;
|
||||
`;
|
||||
|
||||
// <EFBFBD><EFBFBD>װԪ<EFBFBD><EFBFBD>
|
||||
// 组装元素
|
||||
iframeContainer.appendChild(closeBtn);
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.body.appendChild(iframeContainer);
|
||||
container.appendChild(icon);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// <EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 事件监听
|
||||
icon.addEventListener('click', () => {
|
||||
iframeContainer.style.display = 'block';
|
||||
});
|
||||
@ -119,7 +140,7 @@
|
||||
iframeContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 保存实例引用
|
||||
widgetInstance = {
|
||||
remove: () => {
|
||||
container.remove();
|
||||
@ -129,7 +150,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD>ȡλ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
* 获取位置信息
|
||||
*
|
||||
* @param position
|
||||
* @returns {*|string}
|
||||
@ -145,7 +166,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD>ȡsrc<EFBFBD><EFBFBD>ַ
|
||||
* 获取src地址
|
||||
*/
|
||||
function getIframeSrc(finalConfig) {
|
||||
const specificScript = document.getElementById("e7e007dd52f67fe36365eff636bbffbd");
|
||||
@ -154,6 +175,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// <20><>¶ȫ<C2B6>ַ<EFBFBD><D6B7><EFBFBD>
|
||||
/**
|
||||
* 判断是否为手机
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isMobileDevice() {
|
||||
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// 暴露全局方法
|
||||
window.createAiChat = createAiChat;
|
||||
})();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="presetQuestion-wrap">
|
||||
<svg
|
||||
<!-- <svg
|
||||
v-if="btnShow"
|
||||
class="leftBtn"
|
||||
:class="leftBtnStatus"
|
||||
@ -16,7 +16,7 @@
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
</svg>-->
|
||||
<div class="content">
|
||||
<ul ref="ulElemRef">
|
||||
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item.descr)">
|
||||
@ -28,7 +28,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg
|
||||
<!-- <svg
|
||||
v-if="btnShow"
|
||||
class="rightBtn"
|
||||
:class="rightBtnStatus"
|
||||
@ -44,7 +44,7 @@
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
</svg>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -135,7 +135,8 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
/* 隐藏所有滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@ -143,8 +144,15 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
ul:hover {
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
@ -71,6 +71,13 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="left-footer" v-if="source!='chatJs'">
|
||||
AI客服由
|
||||
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
|
||||
敲敲云
|
||||
</a>
|
||||
提供
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -80,7 +87,7 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
const props = defineProps(['dataSource', 'appData']);
|
||||
const props = defineProps(['dataSource', 'appData','source']);
|
||||
const emit = defineEmits(['save', 'click', 'reloadRight', 'prologue']);
|
||||
const inputRef = ref(null);
|
||||
const router = useRouter();
|
||||
@ -149,8 +156,7 @@
|
||||
emit('click', props.dataSource.history[0].title, findIndex);
|
||||
} else {
|
||||
// 删没了(删除了最后一个)
|
||||
props.dataSource.active = null;
|
||||
emit('click', "", -1);
|
||||
handleCreate();
|
||||
}
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空,无法删除---
|
||||
@ -216,6 +222,7 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-bottom: 20px;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@ -285,6 +292,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
&.ant-input {
|
||||
margin-right: 20px;
|
||||
}
|
||||
@ -316,4 +326,13 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.left-footer{
|
||||
display:flex;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -38,10 +38,10 @@ html.dark {
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-neutral-muted: #bfc4cc;
|
||||
--color-accent-fg: #58a6ff;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-attention-subtle: #ece6d9;
|
||||
--color-danger-fg: #f85149;
|
||||
}
|
||||
}
|
||||
@ -86,7 +86,7 @@ html {
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: hsla(210, 18%, 87%, 1);
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||
--color-neutral-muted: #e7ebf2;
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-attention-subtle: #fff8c5;
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<div class="flex text-status" v-if="item.metadata && item.metadata.length>0">
|
||||
<span class="tag-input">输入</span>
|
||||
<div v-for="(metaItem, index) in item.metadata">
|
||||
<a-tag color="rgba(87,104,161,0.08)" class="tags-meadata">
|
||||
<a-tag color="#f2f3f8" class="tags-meadata">
|
||||
<span v-if="index<3" class="tag-text">{{ metaItem.field }}</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
@ -126,7 +126,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载知识库
|
||||
* 加载AI流程
|
||||
*/
|
||||
function loadFlowData() {
|
||||
let params = {
|
||||
@ -135,7 +135,7 @@
|
||||
column: 'createTime',
|
||||
order: 'desc',
|
||||
name: searchText.value,
|
||||
status:'enable'
|
||||
status: 'enable,release'
|
||||
};
|
||||
getAiFlowList(params).then((res) =>{
|
||||
if(res){
|
||||
@ -313,11 +313,11 @@
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
color: rgba(15, 21, 40,0.82);
|
||||
color: #3a3f4f;
|
||||
}
|
||||
.tag-input{
|
||||
align-self: center;
|
||||
color: rgba(55,67,106,0.7);
|
||||
color: #707a97;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<div class="flex header">
|
||||
<span>所选知识库必须使用相同的 Embedding 模型</span>
|
||||
<a-input
|
||||
@pressEnter="loadKnowledgeData"
|
||||
class="header-search"
|
||||
@ -19,7 +18,7 @@
|
||||
<img class="checkbox-img" :src="knowledge" />
|
||||
<span class="checkbox-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker"> </a-checkbox>
|
||||
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker" @change="(e)=>handleChange(e,item)"> </a-checkbox>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@ -91,6 +90,7 @@
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
console.log("知识库确定选中的值",knowledgeData.value);
|
||||
emit('success', knowledgeIds.value, knowledgeData.value);
|
||||
handleCancel();
|
||||
}
|
||||
@ -103,16 +103,17 @@
|
||||
}
|
||||
|
||||
//复选框选中事件
|
||||
const handleSelect = (item) => {
|
||||
function handleSelect(item){
|
||||
let id = item.id;
|
||||
const target = appKnowledgeOption.value.find((item) => item.id === id);
|
||||
if (target) {
|
||||
target.checked = !target.checked;
|
||||
}
|
||||
//存放选中的知识库的id
|
||||
if (knowledgeIds.value.length == 0) {
|
||||
if (!knowledgeIds.value || knowledgeIds.value.length == 0) {
|
||||
knowledgeIds.value.push(id);
|
||||
knowledgeData.value.push(item);
|
||||
console.log("知识库勾选或取消勾选复选框的值",knowledgeData.value);
|
||||
return;
|
||||
}
|
||||
let findIndex = knowledgeIds.value.findIndex((item) => item === id);
|
||||
@ -123,7 +124,8 @@
|
||||
knowledgeIds.value.splice(findIndex, 1);
|
||||
knowledgeData.value.splice(findIndex, 1);
|
||||
}
|
||||
};
|
||||
console.log("知识库勾选或取消勾选复选框的值",knowledgeData.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载知识库
|
||||
@ -176,6 +178,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 复选框选中事件
|
||||
*
|
||||
* @param e
|
||||
* @param item
|
||||
*/
|
||||
function handleChange(e, item) {
|
||||
if (e.target.checked) {
|
||||
knowledgeIds.value.push(item.id);
|
||||
knowledgeData.value.push(item);
|
||||
} else {
|
||||
let findIndex = knowledgeIds.value.findIndex((val) => val === item.id);
|
||||
if (findIndex != -1) {
|
||||
knowledgeIds.value.splice(findIndex, 1);
|
||||
knowledgeData.value.splice(findIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
@ -193,6 +214,7 @@
|
||||
searchText,
|
||||
loadKnowledgeData,
|
||||
handleClearClick,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -222,7 +244,7 @@
|
||||
.list-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 260px;
|
||||
right: 10px;
|
||||
}
|
||||
.checkbox-card {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@ -109,24 +109,80 @@
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-04-01---for:【QQYUN-11796】【AI】提示词生成器,改成异步---
|
||||
/**
|
||||
* 生成
|
||||
*/
|
||||
function generatedPrompt() {
|
||||
async function generatedPrompt() {
|
||||
content.value = '';
|
||||
loading.value = true;
|
||||
promptGenerate({ prompt: prompt.value })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
content.value = res.result;
|
||||
let readableStream = await promptGenerate({ prompt: prompt.value }).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
const reader = readableStream.getReader();
|
||||
const decoder = new TextDecoder('UTF-8');
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
const lines = result.split('\n\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
if(!content.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
renderText(content)
|
||||
} else {
|
||||
if(!line) {
|
||||
continue;
|
||||
}
|
||||
if(!line.endsWith('}')) {
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
renderText(line)
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文本
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function renderText(item) {
|
||||
try {
|
||||
let parse = JSON.parse(item);
|
||||
if (parse.event == 'MESSAGE') {
|
||||
content.value += parse.data.message;
|
||||
if(loading.value){
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
if (parse.event == 'MESSAGE_END') {
|
||||
loading.value = false;
|
||||
}
|
||||
if (parse.event == 'ERROR') {
|
||||
content.value = parse.data.message?parse.data.message:'生成失败,请稍后重试!'
|
||||
loading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-04-01---for:【QQYUN-11796】【AI】提示词生成器,改成异步---
|
||||
|
||||
/**
|
||||
* 指令点击事件
|
||||
*/
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="800px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<template #title>
|
||||
<span style="display: flex">
|
||||
{{title}}
|
||||
<a-tooltip title="AI应用文档">
|
||||
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">
|
||||
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<BasicForm @register="registerForm">
|
||||
<template #typeSlot="{ model, field }">
|
||||
<a-radio-group v-model:value="type" style="display: flex">
|
||||
<a-radio-group v-model:value="model[field]" style="display: flex">
|
||||
<a-card
|
||||
v-for="item in appTypeOption"
|
||||
style="margin-right: 10px; cursor: pointer; width: 100%"
|
||||
@click="handleTypeClick(item.value)"
|
||||
:style="type === item.value ? { borderColor: '#3370ff' } : {}"
|
||||
@click="model[field] = item.value"
|
||||
:style="model[field] === item.value ? { borderColor: '#3370ff' } : {}"
|
||||
>
|
||||
<a-radio :value="item.value">
|
||||
<div class="type-title">{{ item.title }}</div>
|
||||
@ -23,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import { ref, unref, computed } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
|
||||
@ -42,15 +52,13 @@
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('创建应用');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
|
||||
const title = computed<string>(() => isUpdate.value ? '修改应用' : '创建应用');
|
||||
|
||||
//app类型
|
||||
const appTypeOption = ref<any>([]);
|
||||
//应用类型
|
||||
const type = ref<string>('chatSimple');
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
|
||||
@ -63,7 +71,6 @@
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
type.value = 'chatSimple';
|
||||
//update-begin---author:wangshuai---date:2025-03-11---for: 【QQYUN-11324】8.修改弹窗head---
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate)) {
|
||||
@ -71,6 +78,10 @@
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
} else {
|
||||
await setFieldsValue({
|
||||
type: 'chatSimple',
|
||||
})
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-11---for:【QQYUN-11324】8.修改弹窗head---
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
@ -83,7 +94,6 @@
|
||||
try {
|
||||
let values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
values.type = type.value;
|
||||
let result = await saveApp(values);
|
||||
if (result) {
|
||||
//关闭弹窗
|
||||
@ -128,13 +138,6 @@
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用类型点击事件
|
||||
*/
|
||||
function handleTypeClick(val) {
|
||||
type.value = val;
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
@ -142,8 +145,6 @@
|
||||
handleOk,
|
||||
handleCancel,
|
||||
appTypeOption,
|
||||
type,
|
||||
handleTypeClick,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,13 +6,20 @@
|
||||
<div style="display: flex">
|
||||
<img :src="getImage()" class="header-img"/>
|
||||
<div class="header-name">{{formState.name}}</div>
|
||||
<a-tooltip title="编辑">
|
||||
<a-tooltip v-if="!isRelease" title="编辑">
|
||||
<Icon icon="ant-design:edit-outlined" style="margin-left: 4px;cursor: pointer" color="#354052" size="20" @click="handleEdit"></Icon>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div>应用编排</div>
|
||||
<div>
|
||||
应用编排
|
||||
<a-tooltip title="AI应用文档">
|
||||
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">
|
||||
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
<a-button @click="handleOk" style="margin-right: 30px" type="primary">保存</a-button>
|
||||
<a-button v-if="!isRelease" @click="handleOk" style="margin-right: 30px" type="primary">保存</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -35,7 +42,7 @@
|
||||
<template #label>
|
||||
<div style="display: flex;justify-content: space-between;width: 100%;">
|
||||
<span>关联流程</span>
|
||||
<span @click="handleAddFlowClick" class="knowledge-txt">
|
||||
<span v-if="!isRelease" @click="handleAddFlowClick" class="knowledge-txt">
|
||||
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 2px"></Icon>添加
|
||||
</span>
|
||||
</div>
|
||||
@ -49,14 +56,14 @@
|
||||
<div class="flex text-status" v-if="flowData.metadata && flowData.metadata.length>0">
|
||||
<span class="tag-input">输入</span>
|
||||
<div v-for="(metaItem, index) in flowData.metadata">
|
||||
<a-tag color="rgba(87,104,161,0.08)" class="tags-meadata">
|
||||
<a-tag color="#f2f3f8" class="tags-meadata">
|
||||
<span v-if="index<5" class="tag-text">{{ metaItem.field }}</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Icon @click="handleDeleteFlow" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
|
||||
<Icon v-if="!isRelease" @click="handleDeleteFlow" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
|
||||
</div>
|
||||
</a-card>
|
||||
<div v-else class="data-empty-text">
|
||||
@ -71,7 +78,7 @@
|
||||
<template #label>
|
||||
<div class="prompt-title-padding item-title space-between">
|
||||
<span>提示词</span>
|
||||
<a-button size="middle" @click="generatedPrompt" ghost>
|
||||
<a-button v-if="!isRelease" size="middle" @click="generatedPrompt" ghost>
|
||||
<span style="align-items: center;display:flex">
|
||||
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
|
||||
<span style="margin-left: 4px">生成</span>
|
||||
@ -79,7 +86,7 @@
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-textarea :rows="8" v-model:value="formState.prompt" placeholder="请输入提示词"/>
|
||||
<a-textarea :disabled="isRelease" :rows="8" v-model:value="formState.prompt" placeholder="请输入提示词"/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-col>
|
||||
@ -90,7 +97,7 @@
|
||||
<div class="prompt-title-padding item-title">开场白</div>
|
||||
</template>
|
||||
<div class="prologue-chunk-edit">
|
||||
<j-markdown-editor :height="166" v-model:value="formState.prologue" @change="prologueTextAreaBlur" :preview="{ mode: 'view', action: [] }"></j-markdown-editor>
|
||||
<j-markdown-editor :height="166" v-model:value="formState.prologue" :disabled="isRelease" @change="prologueTextAreaBlur" :preview="{ mode: 'view', action: [] }"></j-markdown-editor>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
@ -101,7 +108,7 @@
|
||||
<template #label>
|
||||
<div class="prompt-title-padding item-title space-between">
|
||||
<div class="item-title">预设问题</div>
|
||||
<a-tooltip title="添加预设问题">
|
||||
<a-tooltip v-if="!isRelease" title="添加预设问题">
|
||||
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 16px;cursor: pointer" @click="presetQuestionAddClick"></Icon>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@ -110,9 +117,9 @@
|
||||
<draggable :disabled="disabledDrag" item-key="key" v-model="presetQuestionList" @end="presetQuestionEnd">
|
||||
<template #item="{ element:item }">
|
||||
<div style="display: flex;width: 100%;margin-top: 10px">
|
||||
<Icon icon="ant-design:holder-outlined" size="20"></Icon>
|
||||
<a-input placeholder="输入预设问题" v-model:value="item.descr" style="margin-left: 10px;" @blur="onBlur(item)" @focus="onFocus(item)" @change="questionChange"></a-input>
|
||||
<Icon style="cursor: pointer;margin-left: 10px" icon="ant-design:delete-outlined" @click="deleteQuestionClick(item.key)"></Icon>
|
||||
<Icon v-if="!isRelease" icon="ant-design:holder-outlined" size="20"></Icon>
|
||||
<a-input :disabled="isRelease" placeholder="输入预设问题" v-model:value="item.descr" style="margin-left: 10px;" @blur="onBlur(item)" @focus="onFocus(item)" @change="questionChange"></a-input>
|
||||
<Icon v-if="!isRelease" style="cursor: pointer;margin-left: 10px" icon="ant-design:delete-outlined" @click="deleteQuestionClick(item.key)"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
@ -129,7 +136,7 @@
|
||||
<template #label>
|
||||
<div class="prompt-title-padding item-title space-between">
|
||||
<div class="item-title">快捷指令</div>
|
||||
<a-tooltip title="添加快捷指令">
|
||||
<a-tooltip v-if="!isRelease" title="添加快捷指令">
|
||||
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 16px;cursor: pointer" @click="quickCommandAddClick"></Icon>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@ -143,7 +150,7 @@
|
||||
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
|
||||
<div style="max-width: 400px;margin-left: 4px" class="ellipsis">{{item.name}}</div>
|
||||
</div>
|
||||
<div style="align-items: center" class="quick-command-icon">
|
||||
<div v-if="!isRelease" style="align-items: center" class="quick-command-icon">
|
||||
<a-tooltip title="编辑">
|
||||
<Icon style="cursor: pointer;margin-left: 10px" icon="ant-design:edit-outlined" @click="editCommandClick(item)"></Icon>
|
||||
</a-tooltip>
|
||||
@ -167,13 +174,14 @@
|
||||
<template #label>
|
||||
<div style="display: flex;justify-content: space-between;width: 100%;margin-right: 2px">
|
||||
<div class="item-title">AI模型</div>
|
||||
<div @click="handleParamSettingClick('model')" class="knowledge-txt">
|
||||
<div v-if="!isRelease" @click="handleParamSettingClick('model')" class="knowledge-txt">
|
||||
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<JDictSelectTag
|
||||
v-model:value="formState.modelId"
|
||||
:disabled="isRelease"
|
||||
placeholder="请选择AI模型"
|
||||
dict-code="airag_model where model_type = 'LLM',name,id"
|
||||
style="width: 100%;"
|
||||
@ -193,7 +201,7 @@
|
||||
<template #label>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%;margin-left: 2px;">
|
||||
<div class="item-title">知识库</div>
|
||||
<div>
|
||||
<div v-if="!isRelease">
|
||||
<span @click="handleParamSettingClick('knowledge')" class="knowledge-txt">
|
||||
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
|
||||
</span>
|
||||
@ -209,9 +217,10 @@
|
||||
<div style="display: flex; width: 100%; justify-content: space-between">
|
||||
<div>
|
||||
<img class="knowledge-img" :src="knowledge" />
|
||||
<span class="knowledge-name">{{ item.name }}</span>
|
||||
<span class="knowledge-name" style="color: #e03e2d;text-decoration: line-through" v-if="item.type">{{ item.name }}</span>
|
||||
<span class="knowledge-name" v-else>{{ item.name }}</span>
|
||||
</div>
|
||||
<Icon @click="handleDeleteKnowledge(item.id)" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
|
||||
<Icon v-if="!isRelease" @click="handleDeleteKnowledge(item.id)" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@ -228,10 +237,23 @@
|
||||
<template #label>
|
||||
<div style="margin-left: 2px">历史聊天记录</div>
|
||||
</template>
|
||||
<a-input-number v-model:value="formState.msgNum"></a-input-number>
|
||||
<a-input-number :disabled="isRelease" v-model:value="formState.msgNum"></a-input-number>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="24" class="mt-10">
|
||||
<div class="prologue-chunk">
|
||||
<div style="margin-left: 2px">个性化设置</div>
|
||||
<a-row>
|
||||
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.multiSession">
|
||||
<div style="display: flex;margin-top: 10px">
|
||||
<div style="margin-left: 2px">多会话模式:</div>
|
||||
<a-switch :disabled="isRelease" v-model:checked="multiSessionChecked" checked-children="开" un-checked-children="关" @change="handleMultiSessionChange"></a-switch>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-col>
|
||||
@ -284,7 +306,6 @@
|
||||
import draggable from 'vuedraggable';
|
||||
import { useMessage } from "@/hooks/web/useMessage";
|
||||
import defaultFlowImg from "@/assets/images/ai/aiflow.png";
|
||||
|
||||
export default {
|
||||
name: 'AiAppSettingModal',
|
||||
components: {
|
||||
@ -358,10 +379,15 @@
|
||||
//快捷指令
|
||||
const quickCommand = ref<any>('');
|
||||
const { createMessage } = useMessage();
|
||||
//多会话模式选中状态
|
||||
const multiSessionChecked = ref<boolean>(true);
|
||||
// 是否已发布
|
||||
const isRelease = ref<boolean>(false);
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
appId.value = data.id;
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
isRelease.value = data?.status === 'release';
|
||||
clearParam();
|
||||
if (isUpdate.value) {
|
||||
setTimeout(() => {
|
||||
@ -371,14 +397,16 @@
|
||||
//新增成功之后需要有id
|
||||
queryById({ id: data.id }).then((res) => {
|
||||
if (res.success) {
|
||||
resetFields();
|
||||
//赋值
|
||||
Object.assign(formState, res.result);
|
||||
formState.prompt = AiAppJson.prompt;
|
||||
formState.prologue = AiAppJson.prologue;
|
||||
formState.presetQuestion = JSON.stringify(AiAppJson.presetQuestion);
|
||||
prologue.value = AiAppJson.prologue;
|
||||
formState.prompt = cloneDeep(AiAppJson.prompt);
|
||||
formState.prologue = cloneDeep(AiAppJson.prologue);
|
||||
formState.presetQuestion = JSON.stringify(cloneDeep(AiAppJson.presetQuestion));
|
||||
formState.msgNum = 1;
|
||||
prologue.value = cloneDeep(AiAppJson.prologue);
|
||||
presetQuestion.value = formState.presetQuestion;
|
||||
presetQuestionList.value = AiAppJson.presetQuestion;
|
||||
presetQuestionList.value = cloneDeep(AiAppJson.presetQuestion);
|
||||
addRules(res.result.type)
|
||||
}
|
||||
});
|
||||
@ -403,6 +431,7 @@
|
||||
setModalProps({ confirmLoading: true });
|
||||
formState.knowledgeIds = knowledgeIds.value;
|
||||
await saveApp(formState);
|
||||
emit('success')
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
@ -457,7 +486,9 @@
|
||||
*/
|
||||
function handleSuccess(knowledgeId, knowledgeData) {
|
||||
knowledgeIds.value = cloneDeep(knowledgeId.join(','));
|
||||
console.log("知识库id",knowledgeIds.value);
|
||||
knowledgeDataList.value = cloneDeep(knowledgeData);
|
||||
console.log("知识库的数据",knowledgeDataList.value);
|
||||
formState.knowledgeIds = knowledgeIds.value;
|
||||
}
|
||||
|
||||
@ -481,10 +512,30 @@
|
||||
*/
|
||||
function getKnowledgeDataList(ids) {
|
||||
queryKnowledgeBathById({ ids: ids }).then((res) => {
|
||||
if (res.success) {
|
||||
knowledgeDataList.value = res.result;
|
||||
//update-begin---author:wangshuai---date:2025-04-24---for:【QQYUN-12133】【AI】应用关联的知识库呗删除后,再次进入应用看不到已删除的知识库,并且无法清理掉知识库。---
|
||||
if (res.success && res.result) {
|
||||
let result = res.result;
|
||||
let idArray = ids.split(",");
|
||||
let arr = [];
|
||||
for (const id of idArray) {
|
||||
let filter = result.filter((item) => item.id === id);
|
||||
if(filter && filter.length > 0) {
|
||||
arr.push({ id: id, name: filter[0].name});
|
||||
} else {
|
||||
arr.push({ name: '该知识库已被删除', id: id,type: 'delete' })
|
||||
}
|
||||
}
|
||||
knowledgeDataList.value = arr;
|
||||
knowledgeIds.value = ids;
|
||||
} else {
|
||||
let arr = [];
|
||||
for (const id of ids) {
|
||||
arr.push({ name: '该知识库已被删除', id: id})
|
||||
}
|
||||
knowledgeDataList.value = arr;
|
||||
knowledgeIds.value = ids;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-04-24---for:【QQYUN-12133】【AI】应用关联的知识库呗删除后,再次进入应用看不到已删除的知识库,并且无法清理掉知识库。---
|
||||
});
|
||||
}
|
||||
|
||||
@ -497,7 +548,7 @@
|
||||
|
||||
/**
|
||||
* 关闭弹窗触发列表刷新
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function visibleChange(value) {
|
||||
@ -508,7 +559,7 @@
|
||||
|
||||
/**
|
||||
* 添加检验
|
||||
*
|
||||
*
|
||||
* @param type
|
||||
*/
|
||||
function addRules(type){
|
||||
@ -535,13 +586,13 @@
|
||||
|
||||
/**
|
||||
* 参数配置确定回调事件
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleParamsSettingOk(value){
|
||||
metadata.value = value;
|
||||
Object.assign(metadata.value,value)
|
||||
if(value) {
|
||||
formState.metadata = JSON.stringify(value);
|
||||
formState.metadata = JSON.stringify(metadata.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -620,16 +671,16 @@
|
||||
|
||||
/**
|
||||
* 应用编辑回调事件
|
||||
*
|
||||
*
|
||||
* @param values
|
||||
*/
|
||||
function handelEditSuccess(values) {
|
||||
formState.icon = values.icon ? values.icon:'';
|
||||
formState.name = values.name ? values.name:'';
|
||||
}
|
||||
|
||||
|
||||
//=========== begin预设问题 ===========================
|
||||
|
||||
|
||||
// 编辑状态不允许拖动
|
||||
const disabledDrag = computed(()=>{
|
||||
let list = presetQuestionList.value;
|
||||
@ -641,7 +692,7 @@
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 预设问题拖拽
|
||||
*/
|
||||
@ -652,7 +703,7 @@
|
||||
|
||||
/**
|
||||
* 预设问题添加
|
||||
*
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
function presetQuestionAddClick(e){
|
||||
@ -662,11 +713,11 @@
|
||||
}
|
||||
const length = presetQuestionList.value.length;
|
||||
presetQuestionList.value.push({key: length + 1, sort: length + 1, descr: ''})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 预设问题删除
|
||||
*
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
function deleteQuestionClick(key){
|
||||
@ -693,7 +744,7 @@
|
||||
|
||||
/**
|
||||
* 预设问题值改变事件
|
||||
*
|
||||
*
|
||||
*/
|
||||
function questionChange() {
|
||||
if(presetQuestionList.value && presetQuestionList.value.length>0){
|
||||
@ -704,9 +755,9 @@
|
||||
formState.presetQuestion = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//=========== end预设问题 ===========================
|
||||
|
||||
|
||||
/**
|
||||
* 清除参数
|
||||
*/
|
||||
@ -717,9 +768,10 @@
|
||||
flowId.value = '';
|
||||
flowData.value = null;
|
||||
presetQuestion.value = '';
|
||||
presetQuestionList.value = [];
|
||||
presetQuestionList.value = [{ key:1, sort: 1, descr: '' }];
|
||||
quickCommandList.value = [];
|
||||
quickCommand.value = '';
|
||||
multiSessionChecked.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -735,13 +787,24 @@
|
||||
data.msgNum = data.msgNum ? data.msgNum : 1;
|
||||
if(data.metadata){
|
||||
metadata.value = JSON.parse(data.metadata);
|
||||
if(metadata.value?.multiSession){
|
||||
multiSessionChecked.value = metadata.value.multiSession === '1';
|
||||
}else{
|
||||
multiSessionChecked.value = "1";
|
||||
}
|
||||
}
|
||||
if(data.presetQuestion){
|
||||
presetQuestion.value = data.presetQuestion;
|
||||
presetQuestionList.value = JSON.parse(data.presetQuestion);
|
||||
}
|
||||
if(data.quickCommand){
|
||||
quickCommandList.value = JSON.parse(data.quickCommand);
|
||||
//update-begin---author:wangshuai---date:2025-04-08---for:【QQYUN-11939】ai应用 快捷指令 修改保存以后,再次打开还是原来的---
|
||||
let parse = JSON.parse(data.quickCommand);
|
||||
for (let i = 0; i < parse.length; i++) {
|
||||
parse[i].key = (i+1).toString();
|
||||
}
|
||||
quickCommandList.value = parse;
|
||||
//update-end---author:wangshuai---date:2025-04-08---for:【QQYUN-11939】ai应用 快捷指令 修改保存以后,再次打开还是原来的---
|
||||
}
|
||||
//赋值
|
||||
Object.assign(formState, data);
|
||||
@ -765,14 +828,14 @@
|
||||
|
||||
/**
|
||||
* 提示词回调
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleAiAppPromptOk(value) {
|
||||
formState.prompt = value;
|
||||
}
|
||||
//============= end 提示词 ================================
|
||||
|
||||
|
||||
//=============== begin 快捷指令 ============================
|
||||
function quickCommandEnd() {
|
||||
quickCommand.value = JSON.stringify(quickCommandList.value);
|
||||
@ -780,7 +843,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷指令新增呢个==点击事件
|
||||
* 快捷指令新增点击事件
|
||||
*/
|
||||
function quickCommandAddClick(){
|
||||
if(quickCommandList.value && quickCommandList.value.length > 4){
|
||||
@ -788,8 +851,8 @@
|
||||
return;
|
||||
}
|
||||
aiAppCommandModalOpen(true,{})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷指令编辑点击事件
|
||||
* @param item
|
||||
@ -799,18 +862,21 @@
|
||||
isUpdate: true,
|
||||
record: item
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷指令添加回调事件
|
||||
* @param value
|
||||
*/
|
||||
function handleAiAppCommandOk(value){
|
||||
quickCommandList.value.push({ key: quickCommandList.value.length + 1, ...value });
|
||||
//update-begin---author:wangshuai---date:2025-04-08---for:【QQYUN-11939】ai应用 快捷指令 修改保存以后,再次打开还是原来的---
|
||||
value.key = (quickCommandList.value.length + 1).toString();
|
||||
//update-end---author:wangshuai---date:2025-04-08---for:【QQYUN-11939】ai应用 快捷指令 修改保存以后,再次打开还是原来的---
|
||||
quickCommandList.value.unshift({...value });
|
||||
quickCommand.value = JSON.stringify(quickCommandList.value);
|
||||
formState.quickCommand = quickCommand.value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 快捷指令更新回调事件
|
||||
* @param value
|
||||
@ -819,6 +885,8 @@
|
||||
let findIndex = quickCommandList.value.findIndex(item => item.key === value.key);
|
||||
if(findIndex>-1){
|
||||
quickCommandList.value[findIndex] = value;
|
||||
quickCommand.value = JSON.stringify(quickCommandList.value);
|
||||
formState.quickCommand = quickCommand.value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -835,10 +903,23 @@
|
||||
}
|
||||
}
|
||||
//=============== end 快捷指令 ============================
|
||||
|
||||
|
||||
/**
|
||||
* 复选框相中时的回调
|
||||
*/
|
||||
function handleMultiSessionChange(checked){
|
||||
if(checked){
|
||||
metadata.value.multiSession = "1";
|
||||
}else{
|
||||
metadata.value.multiSession = "0";
|
||||
}
|
||||
formState.metadata = JSON.stringify(metadata.value);
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
isRelease,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
appTypeOption,
|
||||
@ -895,6 +976,8 @@
|
||||
quickCommand,
|
||||
getFlowImage,
|
||||
metadata,
|
||||
multiSessionChecked,
|
||||
handleMultiSessionChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -997,7 +1080,7 @@
|
||||
align-content: center;
|
||||
}
|
||||
.prompt-back{
|
||||
background-color: rgba(238,244,255,1);
|
||||
background-color: #eef4ff;
|
||||
border-radius: 12px;
|
||||
padding: 2px;
|
||||
border: 1px solid #77B2F8;
|
||||
@ -1019,8 +1102,8 @@
|
||||
border-radius: 12px;
|
||||
padding: 2px 10px 2px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.prologue-chunk-edit{
|
||||
background-color: #f2f4f7;
|
||||
border-radius: 12px;
|
||||
@ -1033,7 +1116,7 @@
|
||||
:deep(.ant-form-item-label){
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
:deep(.ant-form-item-required){
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
@ -1052,7 +1135,7 @@
|
||||
}
|
||||
:deep(.vditor){
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
:deep(.vditor-sv){
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -1100,7 +1183,7 @@
|
||||
}
|
||||
}
|
||||
.data-empty-text{
|
||||
color: rgba(32,41,69,0.6);
|
||||
color: #757c8f;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.flow-icon{
|
||||
@ -1123,11 +1206,11 @@
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
color: rgba(15, 21, 40,0.82);
|
||||
color: #3a3f4f;
|
||||
}
|
||||
.tag-input{
|
||||
align-self: center;
|
||||
color: rgba(55,67,106,0.7);
|
||||
color: #737c97;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
|
||||
@ -13,6 +13,7 @@ enum Api {
|
||||
knowledgeDocList = '/airag/knowledge/doc/list',
|
||||
knowledgeEditDoc = '/airag/knowledge/doc/edit',
|
||||
knowledgeDeleteBatchDoc = '/airag/knowledge/doc/deleteBatch',
|
||||
knowledgeDeleteAllDoc = '/airag/knowledge/doc/deleteAll',
|
||||
knowledgeRebuildDoc = '/airag/knowledge/doc/rebuild',
|
||||
knowledgeEmbeddingHitTest = '/airag/knowledge/embedding/hitTest',
|
||||
}
|
||||
@ -114,6 +115,18 @@ export const knowledgeDeleteBatchDoc = (params, handleSuccess) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除文档
|
||||
*
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const knowledgeDeleteAllDoc = (knowId: string, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.knowledgeDeleteAllDoc, params: {knowId} }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 命中测试
|
||||
* @param params
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import {knowledgeDeleteAllDoc} from "./AiKnowledgeBase.api";
|
||||
import {useMessage} from "@/hooks/web/useMessage";
|
||||
|
||||
const {createConfirmSync} = useMessage();
|
||||
|
||||
// 清空文档
|
||||
export async function doDeleteAllDoc(knowledgeId: string, reload: () => void) {
|
||||
const flag = await createConfirmSync({
|
||||
title: '清空文档',
|
||||
content: () => (
|
||||
<p>
|
||||
<span>确定要清空所有文档吗?</span>
|
||||
<br/>
|
||||
<span style="color: #ee0000;">
|
||||
此操作会删除所有已录入的文档,并且不能恢复,请谨慎操作
|
||||
</span>
|
||||
</p>
|
||||
),
|
||||
});
|
||||
if (!flag) {
|
||||
return;
|
||||
}
|
||||
knowledgeDeleteAllDoc(knowledgeId, reload)
|
||||
}
|
||||
@ -45,7 +45,7 @@
|
||||
<img class="header-img" src="./icon/knowledge.png" />
|
||||
<div class="header-text">
|
||||
<span class="header-text-top header-name ellipsis" :title="item.name"> {{ item.name }} </span>
|
||||
<span class="header-text-top"> 创建者:{{ item.createBy }} </span>
|
||||
<span class="header-text-top"> 创建者:{{ item.createBy_dictText || item.createBy }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,6 +75,10 @@
|
||||
<Icon class="pointer" icon="ant-design:delete-outlined" size="16"></Icon>
|
||||
删除
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clear" @click.prevent.stop="onDeleteAllDoc(item)">
|
||||
<Icon icon="ant-design:delete-outlined" size="16"></Icon>
|
||||
清空文档
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
@ -93,6 +97,7 @@
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
:show-total="() => `共${total}条` "
|
||||
/>
|
||||
<!--添加知识库弹窗-->
|
||||
<KnowledgeBaseModal @register="registerModal" @success="reload"></KnowledgeBaseModal>
|
||||
@ -105,6 +110,7 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { deleteModel, list, rebuild } from './AiKnowledgeBase.api';
|
||||
import { doDeleteAllDoc } from "./AiKnowledgeBase.api.util";
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
import KnowledgeBaseModal from './components/AiKnowledgeBaseModal.vue';
|
||||
@ -221,13 +227,26 @@
|
||||
* @param item
|
||||
*/
|
||||
async function handleDelete(item) {
|
||||
if(knowledgeList.value.length == 1 && pageNo.value > 1) {
|
||||
pageNo.value = pageNo.value - 1;
|
||||
}
|
||||
await deleteModel({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空文档
|
||||
* @param item
|
||||
*/
|
||||
async function onDeleteAllDoc(item) {
|
||||
pageNo.value = 1;
|
||||
return doDeleteAllDoc(item.id, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
pageNo.value = 1;
|
||||
reload();
|
||||
}
|
||||
|
||||
@ -237,6 +256,7 @@
|
||||
function searchReset() {
|
||||
formRef.value.resetFields();
|
||||
queryParam.createBy = '';
|
||||
pageNo.value = 1;
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
@ -279,6 +299,7 @@
|
||||
total,
|
||||
handlePageChange,
|
||||
handleDelete,
|
||||
onDeleteAllDoc,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
queryParam,
|
||||
@ -389,7 +410,7 @@
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
@ -414,7 +435,7 @@
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
@ -424,11 +445,11 @@
|
||||
border-radius: 10px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
.knowledge-btn {
|
||||
display: block;
|
||||
}
|
||||
@ -459,7 +480,7 @@
|
||||
}
|
||||
.model-icon:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
background-color: #e9ecf2;
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<template #title>
|
||||
<span style="display: flex">
|
||||
{{title}}
|
||||
<a-tooltip title="AI知识库文档">
|
||||
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/knowledge" target="_blank">
|
||||
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
</BasicModal>
|
||||
</div>
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { MarkdownViewer } from '@/components/Markdown';
|
||||
import { useGlobSetting } from "@/hooks/setting";
|
||||
|
||||
export default {
|
||||
name: 'AiTextDescModal',
|
||||
@ -37,10 +38,32 @@
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
hitTextDescData.value.source = 'score' + ' ' + data.score.toFixed(2);
|
||||
//替换图片宽度
|
||||
data.content = replaceImageWith(data.content);
|
||||
//替换图片domainUrl
|
||||
data.content = replaceDomainUrl(data.content);
|
||||
hitTextDescData.value.content = data.content;
|
||||
setModalProps({ header: '300px' })
|
||||
});
|
||||
|
||||
const { domainUrl } = useGlobSetting();
|
||||
const replaceImageWith = markdownContent => {
|
||||
// 支持图片设置width的写法 
|
||||
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
|
||||
return markdownContent.replace(regex, (match, alt, src, width) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
src = src.replace(reg,domainUrl);
|
||||
return `<img src='${src}' alt='${alt}' width='${width}' />`;
|
||||
});
|
||||
};
|
||||
|
||||
//替换domainURL
|
||||
const replaceDomainUrl = markdownContent => {
|
||||
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
|
||||
return markdownContent.replace(regex, (match) => {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
return match.replace(reg,domainUrl);
|
||||
})
|
||||
}
|
||||
return {
|
||||
registerModal,
|
||||
hitTextDescData
|
||||
|
||||
@ -16,18 +16,46 @@
|
||||
</a-layout-sider>
|
||||
<a-layout-content :style="contentStyle">
|
||||
<div v-if="selectedKey === 'document'">
|
||||
<a-input v-model:value="searchText" placeholder="请输入文档名称,回车搜索" class="search-title" @pressEnter="reload"/>
|
||||
<div class="search-header" style="text-align: left;">
|
||||
<a-space align="center" wrap>
|
||||
<a-input
|
||||
class="search-input"
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入文档名称,回车搜索"
|
||||
@pressEnter="searchTextEnter"
|
||||
/>
|
||||
<template v-if="selectedRows.length > 0">
|
||||
<div>
|
||||
<span>已进入多选模式,当前选中</span>
|
||||
<a style="margin: 0 4px;"> {{ selectedRows.length }} </a>
|
||||
<span>条文档</span>
|
||||
</div>
|
||||
<div>
|
||||
<a @click="onClearSelected">清空选择</a>
|
||||
<a-divider type="vertical"/>
|
||||
<a @click="onDeleteBatch">批量删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-row :span="24" class="knowledge-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
|
||||
<span style="line-height: 18px;font-weight: 500;color:#676f83;font-size: 12px">创建文档</span>
|
||||
<span style="line-height: 18px;font-weight: 500;color:#676f83;font-size: 12px">
|
||||
创建文档
|
||||
<a-tooltip title="知识库文档">
|
||||
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/knowledge#4-%E7%9F%A5%E8%AF%86%E5%BA%93%E6%96%87%E6%A1%A3" target="_blank">
|
||||
<Icon style="position:relative;top:1px" icon="ant-design:question-circle-outlined" size="14"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<div class="add-knowledge-doc" @click="handleCreateText">
|
||||
<Icon icon="ant-design:form-outlined" size="13"></Icon><span>手动录入</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc" @click="handleCreateUpload">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" size="13"></Icon><span>文件上传</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc" @click="handleCreateUploadLibrary">
|
||||
<div class="add-knowledge-doc">
|
||||
<a-upload
|
||||
accept=".zip"
|
||||
name="file"
|
||||
@ -37,15 +65,36 @@
|
||||
:beforeUpload="beforeUpload"
|
||||
:action="uploadUrl"
|
||||
@change="handleUploadChange"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<Icon style="margin-left: 0" icon="ant-design:project-outlined" size="13"></Icon>
|
||||
<span>文档库上传</span>
|
||||
<div style="display: flex;width: 100%">
|
||||
<Icon style="margin-left: 0;color:#676f83" icon="ant-design:project-outlined" size="13"></Icon>
|
||||
<span style="color:#676f83;font-size: 12px">文档库上传</span>
|
||||
</div>
|
||||
</a-upload>
|
||||
</div>
|
||||
<a-dropdown placement="bottomRight" :trigger="['click']">
|
||||
<div class="ant-dropdown-link pointer operation" @click.prevent.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined" size="16"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="delete" @click="onDeleteAll">
|
||||
<Icon icon="ant-design:delete-outlined" size="16"></Icon>
|
||||
清空文档
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeDocDataList">
|
||||
<a-card class="knowledge-card pointer" @click="handleEdit(item)">
|
||||
<a-card :class="['knowledge-card','pointer', {
|
||||
checked: item.__checked,
|
||||
}]" @click="handleEdit(item)">
|
||||
<div class="knowledge-checkbox">
|
||||
<a-checkbox v-model:checked="item.__checked" @click.stop=""/>
|
||||
</div>
|
||||
<div class="knowledge-header">
|
||||
<div class="header-text flex">
|
||||
<Icon v-if="item.type==='text'" icon="ant-design:file-text-outlined" size="32" color="#00a7d0"></Icon>
|
||||
@ -77,6 +126,12 @@
|
||||
<img src="../icon/draft.png" style="width: 16px;height: 16px" />
|
||||
<span class="ml-2">草稿</span>
|
||||
</div>
|
||||
<a-tooltip v-else-if="item.status==='failed'" :title="getDocFailedReason(item)">
|
||||
<div class="card-text-status">
|
||||
<Icon icon="ant-design:close-circle-outlined" size="16" color="#FF4D4F"></Icon>
|
||||
<span class="ml-2">失败</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-dropdown placement="bottomRight" :trigger="['click']">
|
||||
<div class="ant-dropdown-link pointer operation" @click.prevent.stop>
|
||||
@ -114,6 +169,7 @@
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
:show-total="() => `共${total}条` "
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -186,6 +242,7 @@
|
||||
</div>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
<Loading tip="上传中,请稍后" :loading="uploadLoading"></Loading>
|
||||
</BasicModal>
|
||||
|
||||
<!-- 手工录入文本 -->
|
||||
@ -195,11 +252,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onBeforeMount, reactive, ref, unref, h } from 'vue';
|
||||
<script lang="tsx">
|
||||
import { onBeforeMount, computed, ref, unref, h } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { knowledgeDocList, knowledgeDeleteBatchDoc, knowledgeRebuildDoc, knowledgeEmbeddingHitTest } from '../AiKnowledgeBase.api';
|
||||
import { knowledgeDocList, knowledgeDeleteBatchDoc, knowledgeDeleteAllDoc, knowledgeRebuildDoc, knowledgeEmbeddingHitTest } from '../AiKnowledgeBase.api';
|
||||
import { doDeleteAllDoc } from '../AiKnowledgeBase.api.util';
|
||||
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
|
||||
import { useListPage } from '@/hooks/system/useListPage';
|
||||
import AiragKnowledgeDocTextModal from './AiragKnowledgeDocTextModal.vue';
|
||||
@ -212,6 +270,7 @@
|
||||
import defaultImg from '/@/assets/images/header.jpg';
|
||||
import Icon from "@/components/Icon";
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import Loading from '@/components/Loading/src/Loading.vue';
|
||||
|
||||
export default {
|
||||
name: 'AiragKnowledgeDocListModal',
|
||||
@ -225,6 +284,7 @@
|
||||
BasicModal,
|
||||
AiragKnowledgeDocTextModal,
|
||||
AiTextDescModal,
|
||||
Loading,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
@ -274,7 +334,9 @@
|
||||
const globSetting = useGlobSetting();
|
||||
//上传路径
|
||||
const uploadUrl = ref<string>(globSetting.domainUrl+"/airag/knowledge/doc/import/zip");
|
||||
|
||||
//上传加载
|
||||
const uploadLoading = ref<boolean>(false);
|
||||
|
||||
//菜单项
|
||||
const menuItems = ref<any>([
|
||||
{
|
||||
@ -290,6 +352,10 @@
|
||||
title: '命中测试',
|
||||
},
|
||||
]);
|
||||
|
||||
// 当前选中的行
|
||||
const selectedRows = computed(() => knowledgeDocDataList.value.filter(item => item.__checked))
|
||||
|
||||
//注册modal
|
||||
const [docTextRegister, { openModal: docTextOpenModal }] = useModal();
|
||||
const [docTextDescRegister, { openModal: docTextDescOpenModal }] = useModal();
|
||||
@ -329,8 +395,8 @@
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const { createMessage, createConfirmSync } = useMessage();
|
||||
|
||||
/**
|
||||
* 手工录入文本
|
||||
@ -343,6 +409,7 @@
|
||||
* 文件上传
|
||||
*/
|
||||
function handleCreateUpload() {
|
||||
console.log("11111111111")
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "file" });
|
||||
}
|
||||
|
||||
@ -357,6 +424,13 @@
|
||||
* 编辑
|
||||
*/
|
||||
function handleEdit(record) {
|
||||
// 判断如果有选中的行,则说明是批量操作模式
|
||||
if (selectedRows.value.length > 0) {
|
||||
record.__checked = !record.__checked;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (record.type === 'text' || record.type === 'file') {
|
||||
docTextOpenModal(true, {
|
||||
record,
|
||||
@ -376,11 +450,28 @@
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
if(knowledgeDocDataList.value.length == 1 && pageNo.value > 1) {
|
||||
pageNo.value = pageNo.value - 1;
|
||||
}
|
||||
knowledgeDeleteBatchDoc({ ids: id }, reload);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getDocFailedReason(doc) {
|
||||
let metadata = doc?.metadata;
|
||||
if (!metadata) {
|
||||
return '构建失败,原因未知';
|
||||
}
|
||||
try {
|
||||
metadata = JSON.parse(metadata);
|
||||
return metadata?.failedReason || '构建失败,原因未知';
|
||||
} catch (e) {
|
||||
console.log('getDocFailedReason', e);
|
||||
return '构建失败,原因未知';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量化
|
||||
*
|
||||
@ -394,11 +485,9 @@
|
||||
* 文档新增和编辑成功回调
|
||||
*/
|
||||
function handleSuccess() {
|
||||
if(!timer.value){
|
||||
reload();
|
||||
}
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
reload();
|
||||
triggeringTimer();
|
||||
}
|
||||
|
||||
@ -421,7 +510,7 @@
|
||||
pageNo.value = 1;
|
||||
pageSize.value = 10;
|
||||
searchText.value = "";
|
||||
|
||||
|
||||
reload();
|
||||
});
|
||||
} else {
|
||||
@ -511,7 +600,10 @@
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11636】向量化功能改成异步---
|
||||
knowledgeDocDataList.value = res.result.records;
|
||||
knowledgeDocDataList.value = res.result.records.map((item)=>{
|
||||
item.__checked = false;
|
||||
return item;
|
||||
});
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
knowledgeDocDataList.value = [];
|
||||
@ -519,7 +611,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
@ -548,10 +640,11 @@
|
||||
*/
|
||||
function beforeUpload(file) {
|
||||
let fileType = file.type;
|
||||
if (fileType !== 'application/x-zip-compressed') {
|
||||
if (fileType !== 'application/zip' && fileType !== 'application/x-zip-compressed') {
|
||||
createMessage.warning('请上传zip文件');
|
||||
return false;
|
||||
}
|
||||
uploadLoading.value = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -561,24 +654,63 @@
|
||||
*/
|
||||
function handleUploadChange(info) {
|
||||
let { file } = info;
|
||||
if (file.status === 'error') {
|
||||
createMessage.error(file.response.message ||`${file.name} 上传失败.`);
|
||||
if (file.status === 'error' || (file.response && file.response.code == 500)) {
|
||||
createMessage.error(file.response?.message ||`${file.name} 上传失败,请查看服务端日志`);
|
||||
uploadLoading.value = false;
|
||||
return;
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
|
||||
if(!file.response.success){
|
||||
createMessage.warning(file.response.message);
|
||||
uploadLoading.value = false;
|
||||
return;
|
||||
}
|
||||
uploadLoading.value = false;
|
||||
createMessage.success(file.response.message);
|
||||
handleSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
function onClearSelected() {
|
||||
knowledgeDocDataList.value.forEach(item => {
|
||||
item.__checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 清空文档
|
||||
async function onDeleteAll() {
|
||||
pageNo.value = 1;
|
||||
doDeleteAllDoc(knowledgeId.value, reload);
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
async function onDeleteBatch() {
|
||||
const flag = await createConfirmSync({ title: '批量删除', content: `确定要删除这 ${selectedRows.value.length} 条数据吗?` });
|
||||
if (!flag) {
|
||||
return;
|
||||
}
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
let number = knowledgeDocDataList.value.length - ids.length;
|
||||
if(number == 0 && pageNo.value > 1) {
|
||||
pageNo.value = pageNo.value - 1;
|
||||
}
|
||||
knowledgeDeleteBatchDoc({ ids }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回车搜索
|
||||
*/
|
||||
function searchTextEnter(){
|
||||
pageNo.value = 1;
|
||||
reload();
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
@ -607,6 +739,7 @@
|
||||
knowledgeDocDataList,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
getDocFailedReason,
|
||||
handleVectorization,
|
||||
pageNo,
|
||||
pageSize,
|
||||
@ -623,6 +756,12 @@
|
||||
uploadUrl,
|
||||
handleUploadChange,
|
||||
knowledgeId,
|
||||
uploadLoading,
|
||||
selectedRows,
|
||||
onClearSelected,
|
||||
onDeleteAll,
|
||||
onDeleteBatch,
|
||||
searchTextEnter,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -703,7 +842,7 @@
|
||||
border-radius: 10px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
.card-title {
|
||||
justify-content: space-between;
|
||||
@ -715,12 +854,12 @@
|
||||
}
|
||||
}
|
||||
.hit-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15) !important;
|
||||
box-shadow: 0 6px 12px #d0d3d8 !important;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.card-description {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@ -734,16 +873,16 @@
|
||||
font-size: 12px;
|
||||
color: #676F83;
|
||||
}
|
||||
|
||||
|
||||
.card-title-tag {
|
||||
color: #477dee;
|
||||
}
|
||||
|
||||
|
||||
.knowledge-row {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.add-knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
@ -753,14 +892,14 @@
|
||||
width: calc(100% - 20px);
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-right: 20px;
|
||||
@ -768,8 +907,29 @@
|
||||
height: 166px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.knowledge-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover, &.checked {
|
||||
.knowledge-checkbox {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border: 1px solid @primary-color;
|
||||
}
|
||||
|
||||
.knowledge-header {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
@ -786,7 +946,7 @@
|
||||
margin-left: 4px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
.header-text {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@ -801,34 +961,37 @@
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover,.knowledge-card:hover{
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
}
|
||||
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.card-text{
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-title{
|
||||
width: 200px;
|
||||
|
||||
.search-header {
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
margin-left: 20px;
|
||||
margin-left: 26px;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.operation{
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
@ -838,13 +1001,13 @@
|
||||
right: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.knowledge-card:hover{
|
||||
|
||||
.add-knowledge-card:hover, .knowledge-card:hover{
|
||||
.operation{
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.add-knowledge-doc{
|
||||
margin-top: 6px;
|
||||
color:#6F6F83;
|
||||
@ -867,7 +1030,7 @@
|
||||
}
|
||||
.operation:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
background-color: #e9ecf2;
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
@ -892,6 +1055,11 @@
|
||||
.ml-2{
|
||||
margin-left: 2px;
|
||||
}
|
||||
.add-knowledge-doc {
|
||||
:deep(.ant-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.airag-knowledge-doc .scroll-container {
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
</li>
|
||||
<li class="flex mr-14 mt-6">
|
||||
<span class="label">创建者</span>
|
||||
<span class="described">{{ item.createBy }}</span>
|
||||
<span class="described">{{ item.createBy_dictText || item.createBy }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -93,6 +93,7 @@
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
:show-total="() => `共${total}条` "
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -218,6 +219,9 @@
|
||||
* @param item
|
||||
*/
|
||||
async function handleDeleteClick(item) {
|
||||
if(modalList.value.length == 1 && pageNo.value > 1) {
|
||||
pageNo.value = pageNo.value - 1;
|
||||
}
|
||||
await deleteModel({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
@ -225,6 +229,7 @@
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
pageNo.value = 1;
|
||||
reload();
|
||||
}
|
||||
|
||||
@ -350,14 +355,14 @@
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
height: 152px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.model-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
.model-btn {
|
||||
display: flex;
|
||||
}
|
||||
@ -385,7 +390,7 @@
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
@ -410,7 +415,7 @@
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 12px #d0d3d8;
|
||||
}
|
||||
.model-icon{
|
||||
background-color: unset;
|
||||
@ -419,7 +424,7 @@
|
||||
}
|
||||
.model-icon:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
background-color: #e9ecf2;
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
|
||||
@ -3,7 +3,14 @@
|
||||
<div class="modal">
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">选择供应商</span>
|
||||
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">
|
||||
选择供应商
|
||||
<a-tooltip title="供应商文档" v-if="dataIndex ==='list'">
|
||||
<a style="color: #333333" href="https://help.jeecg.com/aigc/guide/model/#2-%E4%BE%9B%E5%BA%94%E5%95%86%E9%80%89%E6%8B%A9" target="_blank">
|
||||
<Icon style="position:relative;left: -2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<span v-if="dataIndex === 'add'" class="add-header-title"> > </span>
|
||||
<span v-if="dataIndex === 'add'" style="color: #1f2329">添加 {{ providerName }}</span>
|
||||
</span>
|
||||
@ -27,7 +34,17 @@
|
||||
</a-row>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="activeKey" v-if="dataIndex === 'add' || dataIndex === 'edit'">
|
||||
<a-tab-pane :key="1" tab="基础信息">
|
||||
<a-tab-pane :key="1">
|
||||
<template #tab>
|
||||
<span style="display: flex">
|
||||
基础信息
|
||||
<a-tooltip title="基础信息文档">
|
||||
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#31-%E5%A1%AB%E5%86%99%E5%9F%BA%E7%A1%80%E4%BF%A1%E6%81%AF" target="_blank">
|
||||
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<div class="model-content">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #modelType="{ model, field }">
|
||||
@ -62,13 +79,24 @@
|
||||
</BasicForm>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="2" tab="高级配置" v-if="modelParamsShow">
|
||||
<a-tab-pane :key="2" v-if="modelParamsShow">
|
||||
<template #tab>
|
||||
<span style="display: flex">
|
||||
高级配置
|
||||
<a-tooltip title="高级配置文档">
|
||||
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#32-%E9%85%8D%E7%BD%AE%E9%AB%98%E7%BA%A7%E5%8F%82%E6%95%B0" target="_blank">
|
||||
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<AiModelSeniorForm ref="modelParamsRef" :modelParams="modelParams"></AiModelSeniorForm>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
</div>
|
||||
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
|
||||
<a-button @click="test" :loading="testLoading">测试</a-button>
|
||||
<a-button @click="cancel">关闭</a-button>
|
||||
<a-button @click="save" type="primary">保存</a-button>
|
||||
</template>
|
||||
@ -87,9 +115,10 @@
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { formSchema, imageList } from '../model.data';
|
||||
import { editModel, queryById, saveModel } from '../model.api';
|
||||
import { editModel, queryById, saveModel, testConn } from '../model.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import AiModelSeniorForm from './AiModelSeniorForm.vue';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
export default {
|
||||
name: 'AddModelModal',
|
||||
components: {
|
||||
@ -128,6 +157,8 @@
|
||||
const modelParamsShow = ref<boolean>(false);
|
||||
//模型参数ref
|
||||
const modelParamsRef = ref();
|
||||
//测试按钮loading状态
|
||||
const testLoading = ref<boolean>(false);
|
||||
|
||||
const getImage = (name) => {
|
||||
return imageList.value[name];
|
||||
@ -206,7 +237,7 @@
|
||||
*/
|
||||
function initModelTypeOption() {
|
||||
initDictOptions('model_type').then((data) => {
|
||||
modelTypeOption.value = data;
|
||||
modelTypeOption.value = cloneDeep(data);
|
||||
//update-begin---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
|
||||
if(data[0].value != 'all'){
|
||||
modelTypeOption.value.unshift({
|
||||
@ -301,6 +332,38 @@
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试连接
|
||||
*/
|
||||
async function test() {
|
||||
try {
|
||||
testLoading.value = true;
|
||||
let values = await validate();
|
||||
let credential = {
|
||||
apiKey: values.apiKey,
|
||||
secretKey: values.secretKey,
|
||||
};
|
||||
if (modelParamsRef.value) {
|
||||
let modelParams = modelParamsRef.value.emitChange();
|
||||
if (modelParams) {
|
||||
values.modelParams = JSON.stringify(modelParams);
|
||||
}
|
||||
}
|
||||
values.credential = JSON.stringify(credential);
|
||||
if (!values.provider) {
|
||||
values.provider = modelData.value.value;
|
||||
}
|
||||
//测试
|
||||
await testConn(values);
|
||||
} catch (e) {
|
||||
if (e.hasOwnProperty('errorFields')) {
|
||||
activeKey.value = 1;
|
||||
}
|
||||
} finally {
|
||||
testLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型类型选择事件
|
||||
* @param value
|
||||
@ -364,6 +427,8 @@
|
||||
modelParamsRef,
|
||||
filterOption,
|
||||
getTitle,
|
||||
test,
|
||||
testLoading,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "https://api.openai.com/v1/",
|
||||
"LLMDefaultValue": "gpt-3.5-turbo",
|
||||
"LLMDefaultValue": "gpt-4o-mini",
|
||||
"EMBEDDefaultValue": "text-embedding-ada-002"
|
||||
},
|
||||
{
|
||||
|
||||
@ -4,6 +4,7 @@ import { Modal } from 'ant-design-vue';
|
||||
enum Api {
|
||||
list = '/airag/airagModel/list',
|
||||
save = '/airag/airagModel/add',
|
||||
testConn = '/airag/airagModel/test',
|
||||
delete = '/airag/airagModel/delete',
|
||||
queryById = '/airag/airagModel/queryById',
|
||||
edit = '/airag/airagModel/edit',
|
||||
@ -43,13 +44,22 @@ export const editModel = (params) => {
|
||||
return defHttp.put({ url: Api.edit, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 测试链接
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const testConn = (params) => {
|
||||
return defHttp.post({ url: Api.testConn, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除数据权限
|
||||
*/
|
||||
export const deleteModel = (params, handleSuccess) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '是否删除名称为'+params.name+'的模型吗?',
|
||||
content: '是否删除名称为' + params.name + '的模型吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
|
||||
46
jeecgboot-vue3/src/views/super/airag/ocr/AiOcr.api.ts
Normal file
46
jeecgboot-vue3/src/views/super/airag/ocr/AiOcr.api.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
export enum Api {
|
||||
list = '/airag/ocr/list',
|
||||
add = '/airag/ocr/add',
|
||||
edit = '/airag/ocr/edit',
|
||||
deleteById = '/airag/ocr/deleteById',
|
||||
flowRun = '/airag/flow/run',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询ocr列表
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const list = (params) => {
|
||||
return defHttp.get({ url: Api.list, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加Orc
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const addOcr = (params) => {
|
||||
return defHttp.post({ url: Api.add, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑Orc
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const editOcr = (params) => {
|
||||
return defHttp.put({ url: Api.edit, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据id删除 Orc
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const deleteOcrById = (params) => {
|
||||
return defHttp.delete({ url: Api.deleteById, params });
|
||||
};
|
||||
81
jeecgboot-vue3/src/views/super/airag/ocr/AiOcr.data.ts
Normal file
81
jeecgboot-vue3/src/views/super/airag/ocr/AiOcr.data.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { BasicColumn, FormSchema } from '@/components/Table';
|
||||
|
||||
//ocr表格
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '编号',
|
||||
dataIndex: 'id',
|
||||
ifShow: false,
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
ellipsis: true,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: '提示词',
|
||||
dataIndex: 'prompt',
|
||||
ellipsis: true,
|
||||
width: 300,
|
||||
},
|
||||
];
|
||||
|
||||
//ocr表单
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '标题',
|
||||
field: 'title',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '提示词',
|
||||
field: 'prompt',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
row: 4,
|
||||
autosize: { minRows: 4, maxRows: 6 },
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
//ocr解析表单
|
||||
export const analysisSchemas: FormSchema[] = [
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
field: 'url',
|
||||
component: 'JImageUpload',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '提示词',
|
||||
field: 'prompt',
|
||||
component: 'InputTextArea',
|
||||
show:false,
|
||||
},
|
||||
{
|
||||
label: '解析结果',
|
||||
field: 'analysisResult',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
row: 10,
|
||||
autosize: { minRows: 10, maxRows: 10 },
|
||||
readonly: true,
|
||||
placeholder:"解析结果将在这里显示"
|
||||
},
|
||||
},
|
||||
];
|
||||
104
jeecgboot-vue3/src/views/super/airag/ocr/AiOcrList.vue
Normal file
104
jeecgboot-vue3/src/views/super/airag/ocr/AiOcrList.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<!--插槽:table标题-->
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<AiOcrModal @register="registerModal" @success="reload()"></AiOcrModal>
|
||||
<AiOcrAnalysisModal @register="registerAnalysisModal" @success="reload()"></AiOcrAnalysisModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="site-list" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { list, deleteOcrById } from './AiOcr.api';
|
||||
import { columns } from './AiOcr.data';
|
||||
import AiOcrModal from './components/AiOcrModal.vue';
|
||||
import AiOcrAnalysisModal from './components/AiOcrAnalysisModal.vue';
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerAnalysisModal, { openModal: openAnalysisModal }] = useModal();
|
||||
|
||||
// 列表页面公共参数、方法
|
||||
const { prefixCls, tableContext } = useListPage({
|
||||
tableProps: {
|
||||
api: list,
|
||||
columns,
|
||||
useSearchForm: false,
|
||||
pagination: false,
|
||||
actionColumn: {
|
||||
width: 120,
|
||||
},
|
||||
canResize: false,
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
function handleCreate() {
|
||||
openModal(true, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
showFooter: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
async function handleDelete(id) {
|
||||
await deleteOcrById({ id: id });
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析
|
||||
* @param record
|
||||
*/
|
||||
function handleAnalysis(record){
|
||||
openAnalysisModal(true,{
|
||||
isUpdate: true,
|
||||
record
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作栏
|
||||
*/
|
||||
function getTableAction(record) {
|
||||
return [
|
||||
{
|
||||
label: '解析',
|
||||
onClick: handleAnalysis.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: {
|
||||
title: '是否确认删除',
|
||||
placement: 'left',
|
||||
confirm: handleDelete.bind(null, record.id),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" :footer="null">
|
||||
<a-spin tip="解析中,请稍后" :spinning="loading">
|
||||
<BasicForm @register="registerForm" />
|
||||
<div style="width: 100%;text-align: center; margin-bottom: 10px">
|
||||
<a-button type="primary" @click="analysisHandleClick">解析 </a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
|
||||
</template>
|
||||
<script lang="ts" name="AiOcrModal" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { analysisSchemas } from '../AiOcr.data';
|
||||
import { addOcr, Api, editOcr } from '../AiOcr.api';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
//标题
|
||||
const title = ref<string>('解析');
|
||||
//是否更新
|
||||
const isUpdate = ref<boolean>();
|
||||
//加载
|
||||
const loading = ref<boolean>(false);
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: analysisSchemas,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: true, bodyStyle:{ padding:'24px'} });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
//重置表单
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
if (unref(isUpdate)) {
|
||||
//表单赋值
|
||||
await setFieldsValue({ ...data.record });
|
||||
}
|
||||
});
|
||||
|
||||
//表单提交事件
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
if (unref(isUpdate)) {
|
||||
await editOcr(values);
|
||||
} else {
|
||||
await addOcr(values);
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//刷新列表
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析
|
||||
*/
|
||||
async function analysisHandleClick() {
|
||||
const values = await validate();
|
||||
loading.value = true;
|
||||
await defHttp
|
||||
.post(
|
||||
{
|
||||
url: Api.flowRun,
|
||||
params: {
|
||||
flowId: '1904779811574784002',
|
||||
inputParams: {
|
||||
content: values.prompt,
|
||||
images: values.url,
|
||||
},
|
||||
responseMode: 'blocking',
|
||||
},
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
let replace = res.result.data.replace(/\s+/g, '');
|
||||
let parse = JSON.parse(replace);
|
||||
let text = parse.text;
|
||||
let lastText = "";
|
||||
for (const textKey in text) {
|
||||
lastText = lastText + textKey +":"+ text[textKey] + "\n";
|
||||
}
|
||||
setFieldsValue({ analysisResult: lastText });
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch((res)=>{
|
||||
createMessage.warning(res.message);
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" name="AiOcrModal" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { schemas } from '../AiOcr.data';
|
||||
import {addOcr, editOcr} from "../AiOcr.api";
|
||||
const title = ref<string>('新增');
|
||||
const isUpdate = ref<boolean>();
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: schemas,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: true, bodyStyle:{ padding:'24px'} });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
title.value = !unref(isUpdate) ? '新增' : '编辑'
|
||||
//重置表单
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
if(unref(isUpdate)){
|
||||
//表单赋值
|
||||
await setFieldsValue({ ...data.record });
|
||||
}
|
||||
});
|
||||
|
||||
//表单提交事件
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
if(unref(isUpdate)){
|
||||
await editOcr(values);
|
||||
} else{
|
||||
await addOcr(values);
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//刷新列表
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-modal-body){
|
||||
padding: 24px !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<a-card :bordered="false" style="height: 100%">
|
||||
<a-card :bordered="false" style="height: 100%;" :body-style="{ background: backgroundColor }" >
|
||||
<a-spin :spinning="loading">
|
||||
<a-input-search placeholder="按部门名称搜索…" style="margin-bottom: 10px" @search="onSearch" allowClear />
|
||||
<a-input-search v-if="showSearch" placeholder="按部门名称搜索…" style="margin-bottom: 10px" @search="onSearch" allowClear />
|
||||
<!--组织机构树-->
|
||||
<template v-if="treeData.length > 0">
|
||||
<a-tree
|
||||
v-if="!treeReloading"
|
||||
:style="{ background: backgroundColor }"
|
||||
showLine
|
||||
:clickRowToExpand="false"
|
||||
:treeData="treeData"
|
||||
@ -27,6 +28,19 @@
|
||||
import { Popconfirm } from 'ant-design-vue';
|
||||
|
||||
const prefixCls = inject('prefixCls');
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
// 是否显示搜索框
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 背景色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: 'inherit',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['select', 'rootTreeData']);
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
@ -74,6 +74,12 @@ export function useBasicFormSchema() {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
label: 'ID',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
];
|
||||
return { basicFormSchema };
|
||||
}
|
||||
|
||||
@ -1,63 +1,84 @@
|
||||
<template>
|
||||
<div class="bg-white m-4 mr-0 overflow-hidden">
|
||||
<div v-if="userIdentity === '2'" class="j-table-operator" style="width: 100%">
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddChildDepart">添加下级</a-button>
|
||||
<!-- <a-button type="primary" preIcon="ant-design:edit-outlined" @click="editDepart">编辑</a-button>-->
|
||||
<a-button :disabled="!(checkedKeys && checkedKeys.length > 0)" preIcon="ant-design:delete-outlined" @click="onDeleteBatch">删除</a-button>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="userIdentity === '2'">
|
||||
<a-input-search placeholder="按部门名称搜索…" style="margin-bottom: 10px" @search="onSearch" />
|
||||
<!--组织机构树-->
|
||||
<BasicTree
|
||||
v-if="!treeReloading"
|
||||
title="部门列表"
|
||||
toolbar
|
||||
search
|
||||
showLine
|
||||
:checkStrictly="true"
|
||||
:toolbar="false"
|
||||
:search="false"
|
||||
:showLine="false"
|
||||
:clickRowToExpand="false"
|
||||
:multiple="false"
|
||||
:checkStrictly="true"
|
||||
:treeData="treeData"
|
||||
:checkedKeys="checkedKeys"
|
||||
:selectedKeys="selectedKeys"
|
||||
:expandedKeys="expandedKeys"
|
||||
:autoExpandParent="autoExpandParent"
|
||||
:beforeRightClick="getRightMenuList"
|
||||
@select="onSelect"
|
||||
@expand="onExpand"
|
||||
@search="onSearch"
|
||||
@check="onCheck"
|
||||
/>
|
||||
</template>
|
||||
<a-empty v-else description="普通员工无此权限" />
|
||||
</a-spin>
|
||||
<DepartFormModal :rootTreeData="treeData" @register="registerModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { BasicTree } from '/@/components/Tree';
|
||||
import { BasicTree, ContextMenuItem } from '/@/components/Tree';
|
||||
import { queryMyDepartTreeList, searchByKeywords } from '../depart.user.api';
|
||||
import DepartFormModal from '@/views/system/depart/components/DepartFormModal.vue';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { deleteBatchDepart } from '@/views/system/depart/depart.api';
|
||||
|
||||
const prefixCls = inject('prefixCls');
|
||||
const emit = defineEmits(['select']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
let loading = ref<boolean>(false);
|
||||
// 负责部门ID
|
||||
let myDepIds = ref<any[]>([]);
|
||||
// 部门树列表数据
|
||||
let treeData = ref<any[]>([]);
|
||||
// 当前展开的项
|
||||
let expandedKeys = ref<any[]>([]);
|
||||
// 当前选中的项
|
||||
let selectedKeys = ref<any[]>([]);
|
||||
// 当前选中的项
|
||||
let selectedNode = ref<any>({});
|
||||
// 当前选中的项
|
||||
let checkedKeys = ref<any[]>([]);
|
||||
// 是否自动展开父级
|
||||
let autoExpandParent = ref<boolean>(true);
|
||||
// 用户身份
|
||||
// 用户身份(1:普通员工 2:上级)
|
||||
let userIdentity = ref<string>('2');
|
||||
// 树组件重新加载
|
||||
let treeReloading = ref<boolean>(false);
|
||||
|
||||
// 注册 modal
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
// 加载部门信息
|
||||
function loadDepartTreeData() {
|
||||
loading.value = true;
|
||||
treeReloading.value = true;
|
||||
treeData.value = [];
|
||||
queryMyDepartTreeList()
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
if (Array.isArray(res.result)) {
|
||||
treeData.value = res.result;
|
||||
myDepIds.value = res.result.map((item) => item.id);
|
||||
userIdentity.value = res.message;
|
||||
autoExpandParentNode();
|
||||
}
|
||||
@ -65,7 +86,11 @@
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
})
|
||||
.finally(() => (loading.value = false));
|
||||
.finally(async () => {
|
||||
await nextTick();
|
||||
loading.value = false;
|
||||
treeReloading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadDepartTreeData();
|
||||
@ -88,6 +113,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 添加子级部门
|
||||
function onAddChildDepart() {
|
||||
if (selectedKeys.value && selectedKeys.value.length === 0) {
|
||||
createMessage.warning('请先选择一个部门');
|
||||
return;
|
||||
}
|
||||
const record = { parentId: selectedKeys.value[0] };
|
||||
openModal(true, { isUpdate: false, isChild: true, record });
|
||||
}
|
||||
|
||||
// 编辑部门
|
||||
function editDepart() {
|
||||
if (selectedKeys.value && selectedKeys.value.length === 0) {
|
||||
createMessage.warning('请先选择一个部门');
|
||||
return;
|
||||
}
|
||||
if (myDepIds.value.includes(selectedKeys.value[0])) {
|
||||
createMessage.warning('不能编辑负责部门');
|
||||
return;
|
||||
}
|
||||
console.log('selectedNode', selectedNode.value);
|
||||
openModal(true, { isUpdate: false, isChild: true, record: { ...selectedNode.value } });
|
||||
}
|
||||
|
||||
// 删除部门
|
||||
async function onDeleteBatch() {
|
||||
const idList = checkedKeys.value;
|
||||
if (myDepIds.value.includes(idList[0])) {
|
||||
createMessage.warning('不能删除负责部门');
|
||||
return;
|
||||
}
|
||||
if (idList.length > 0) {
|
||||
try {
|
||||
loading.value = true;
|
||||
await deleteBatchDepart({ ids: idList.join(',') }, true);
|
||||
await loadDepartTreeData();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 重新加载树组件,防止无法默认展开数据
|
||||
async function reloadTree() {
|
||||
await nextTick();
|
||||
@ -101,7 +167,9 @@
|
||||
*/
|
||||
function setSelectedKey(key: string, data?: object) {
|
||||
selectedKeys.value = [key];
|
||||
checkedKeys.value = [key];
|
||||
if (data) {
|
||||
selectedNode.value = { ...data };
|
||||
emit('select', data);
|
||||
}
|
||||
}
|
||||
@ -133,6 +201,16 @@
|
||||
// 这样可以防止用户取消选择
|
||||
setSelectedKey(selectedKeys.value[0]);
|
||||
}
|
||||
checkedKeys.value = [selectedKeys.value[0]];
|
||||
}
|
||||
|
||||
// 树选中事件
|
||||
function onCheck(keys) {
|
||||
if (keys.checked && keys.checked.length > 0) {
|
||||
checkedKeys.value = [...keys.checked];
|
||||
} else {
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 树展开事件
|
||||
@ -140,6 +218,38 @@
|
||||
expandedKeys.value = keys;
|
||||
autoExpandParent.value = false;
|
||||
}
|
||||
|
||||
//成功回调
|
||||
async function handleSuccess() {
|
||||
await loadDepartTreeData();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
*/
|
||||
function getRightMenuList(node: any): ContextMenuItem[] {
|
||||
return [
|
||||
{
|
||||
label: '添加下级',
|
||||
disabled: myDepIds.value.includes(node.key),
|
||||
handler: () => {
|
||||
setSelectedKey(node.key);
|
||||
onAddChildDepart();
|
||||
},
|
||||
icon: 'ant-design:plus-outlined',
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
disabled: myDepIds.value.includes(node.key),
|
||||
handler: () => {
|
||||
setSelectedKey(node.key);
|
||||
const record = { ...node.dataRef };
|
||||
openModal(true, { isUpdate: true, record, isChild: true });
|
||||
},
|
||||
icon: 'ant-design:edit-outlined',
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
/*升级antd3后,查询框与树贴的太近,样式优化*/
|
||||
|
||||
@ -255,6 +255,9 @@ export const formSchema: FormSchema[] = [
|
||||
label: '菜单图标',
|
||||
component: 'IconPicker',
|
||||
ifShow: ({ values }) => !isButton(values.menuType),
|
||||
componentProps: {
|
||||
allowClear: true
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sortNo',
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button preIcon="ant-design:user-add-outlined" type="primary" @click="handleAdd">新增</a-button>
|
||||
<a-button preIcon="ant-design:user-add-outlined" type="primary" @click="handleAdd">+ 默认套餐
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="selectedRowKeys.length > 0"
|
||||
preIcon="ant-design:delete-outlined"
|
||||
@ -25,7 +26,7 @@
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { deleteTenantPack, packList } from '../tenant.api';
|
||||
import { packColumns, packFormSchema } from '../tenant.data';
|
||||
import { defalutPackColumns, defaultPackFormSchema } from "../tenant.data";
|
||||
import TenantPackMenuModal from './TenantPackMenuModal.vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
@ -42,9 +43,9 @@
|
||||
designScope: 'tenant-template',
|
||||
tableProps: {
|
||||
api: packList,
|
||||
columns: packColumns,
|
||||
columns: defalutPackColumns,
|
||||
formConfig: {
|
||||
schemas: packFormSchema,
|
||||
schemas: defaultPackFormSchema,
|
||||
},
|
||||
beforeFetch: (params) => {
|
||||
return Object.assign(params, { packType: 'default' });
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
//update-end---author:wangshuai ---date:20230705 for:【QQYUN-5685】2 套餐包增加一个查看:添加底部有没有按钮及表单禁用------------
|
||||
});
|
||||
//设置标题
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑租户套餐包' : '新增租户套餐包'));
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑 租户套餐' : '新增 租户套餐'));
|
||||
//表单提交事件
|
||||
async function handleSubmit(v) {
|
||||
const values = await validate();
|
||||
|
||||
@ -271,7 +271,33 @@ export const packColumns: BasicColumn[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
title: '备注说明',
|
||||
dataIndex: 'remarks',
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
//套餐包列表
|
||||
export const defalutPackColumns: BasicColumn[] = [
|
||||
{
|
||||
title: '默认套餐名称',
|
||||
dataIndex: 'packName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
customRender: ({ text }) => {
|
||||
if (text === '1') {
|
||||
return '开启';
|
||||
} else {
|
||||
return '关闭';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注说明',
|
||||
dataIndex: 'remarks',
|
||||
width: 150,
|
||||
},
|
||||
@ -281,7 +307,17 @@ export const packColumns: BasicColumn[] = [
|
||||
export const packFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'packName',
|
||||
label: '套餐包名称',
|
||||
label: '套餐包名',
|
||||
component: 'JInput',
|
||||
colProps: { xxl: 8 },
|
||||
},
|
||||
];
|
||||
|
||||
//套餐包搜索表单
|
||||
export const defaultPackFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'packName',
|
||||
label: '默认套餐名',
|
||||
component: 'JInput',
|
||||
colProps: { xxl: 8 },
|
||||
},
|
||||
@ -296,7 +332,7 @@ export const packMenuFormSchema: FormSchema[] = [
|
||||
},
|
||||
{
|
||||
field: 'permissionIds',
|
||||
label: '菜单列表',
|
||||
label: '授权菜单',
|
||||
component: 'JTreeSelect',
|
||||
componentProps: {
|
||||
dict: 'sys_permission,name,id',
|
||||
@ -309,7 +345,7 @@ export const packMenuFormSchema: FormSchema[] = [
|
||||
},
|
||||
{
|
||||
field: 'remarks',
|
||||
label: '描述',
|
||||
label: '备注说明',
|
||||
component: 'InputTextArea',
|
||||
},
|
||||
{
|
||||
|
||||
@ -44,12 +44,12 @@
|
||||
if (unref(isUpdate)) {
|
||||
rowId.value = data.record.id;
|
||||
//租户信息定义成数组
|
||||
if (data.record.relTenantIds && !Array.isArray(data.record.relTenantIds)) {
|
||||
/* if (data.record.relTenantIds && !Array.isArray(data.record.relTenantIds)) {
|
||||
data.record.relTenantIds = data.record.relTenantIds.split(',');
|
||||
} else {
|
||||
//【issues/I56C5I】用户管理中连续点两次编辑租户配置就丢失了
|
||||
//data.record.relTenantIds = [];
|
||||
}
|
||||
}*/
|
||||
|
||||
//查角色/赋值/try catch 处理,不然编辑有问题
|
||||
try {
|
||||
|
||||
@ -236,10 +236,11 @@ export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '租户',
|
||||
field: 'relTenantIds',
|
||||
component: 'JDictSelectTag',
|
||||
component: 'JSearchSelect',
|
||||
componentProps: {
|
||||
dictCode:"sys_tenant,name,id",
|
||||
mode: "multiple"
|
||||
dict:"sys_tenant,name,id",
|
||||
async: true,
|
||||
multiple: true
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -32,6 +32,8 @@
|
||||
<UserReplacePhoneModal @register="registerModal" @success="initUserDetail" />
|
||||
<UserReplaceEmailModal @register="registerEmailModal" @success="initUserDetail" />
|
||||
<UserPasswordModal @register="registerPassModal" @success="initUserDetail" />
|
||||
<UserPasswordNotBindPhone @register="registerPassNotBindPhoneModal" @success="initUserDetail" />
|
||||
<UserCancellationModal @register="registerCancelModal" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, reactive } from 'vue';
|
||||
@ -41,6 +43,8 @@
|
||||
import UserReplacePhoneModal from './commponents/UserPhoneModal.vue';
|
||||
import UserReplaceEmailModal from './commponents/UserEmailModal.vue';
|
||||
import UserPasswordModal from './commponents/UserPasswordModal.vue';
|
||||
import UserPasswordNotBindPhone from './commponents/UserPasswordNotBindPhone.vue';
|
||||
import UserCancellationModal from './commponents/UserCancellationModal.vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { WechatFilled } from '@ant-design/icons-vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
@ -52,6 +56,8 @@
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerEmailModal, { openModal: openEmailModal }] = useModal();
|
||||
const [registerPassModal, { openModal: openPassModal }] = useModal();
|
||||
const [registerPassNotBindPhoneModal, { openModal: openPassNotBindPhoneModal }] = useModal();
|
||||
const [registerCancelModal, { openModal: openCancelModal }] = useModal();
|
||||
|
||||
const wechatData = reactive<any>({
|
||||
bindWechat: false,
|
||||
@ -104,9 +110,17 @@
|
||||
* 密码修改
|
||||
*/
|
||||
function updatePassWord() {
|
||||
openPassModal(true, {
|
||||
record: { username: userDetail.value.username },
|
||||
});
|
||||
//存在手机号手机号修改密码
|
||||
if(userDetail.value.phone){
|
||||
openPassModal(true, {
|
||||
record: { username: userDetail.value.username },
|
||||
});
|
||||
} else {
|
||||
//没有手机号走直接修改密码弹窗
|
||||
openPassNotBindPhoneModal(true, {
|
||||
record: { username: userDetail.value.username },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,6 +18,8 @@ enum Api {
|
||||
changePhone = '/sys/user/changePhone',
|
||||
//用户注销
|
||||
userLogOff = '/sys/user/userLogOff',
|
||||
//没有绑定手机号用的修改密码请求地址
|
||||
updatePasswordNotBindPhone = '/sys/user/updatePasswordNotBindPhone',
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,6 +62,14 @@ export const updateUserPassword = (params) => {
|
||||
return defHttp.get({ url: Api.updateUserPassword, params },{isTransformResponse:false});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param params
|
||||
*/
|
||||
export const updatePasswordNotBindPhone = (params) => {
|
||||
return defHttp.put({ url: Api.updatePasswordNotBindPhone, params },{ isTransformResponse:false, joinParamsToUrl: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户id获取租户列表
|
||||
* @param params
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="修改密码" @ok="handleSubmit" destroyOnClose :width="400">
|
||||
<a-form class="antd-modal-form" ref="formRef" :model="formState" :rules="validatorRules">
|
||||
<a-form-item name="oldPassword">
|
||||
<div class="black font-size-13">旧密码</div>
|
||||
<div class="pass-padding">
|
||||
<a-input-password placeholder="请输入旧密码" v-model:value="formState.oldPassword" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<span class="black font-size-13">新密码</span>
|
||||
<div class="pass-padding">
|
||||
<a-input-password v-model:value="formState.password" placeholder="新密码" autocomplete="new-password" />
|
||||
</div>
|
||||
<span class="gray-9e font-size-13">8-20位,需包含字母和数字</span>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" name="user-pass-word-modal" setup>
|
||||
import { ref, unref, reactive } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { Rule } from '@/components/Form';
|
||||
import { updatePasswordNotBindPhone } from '../UserSetting.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
//用户名
|
||||
const username = ref<string>('');
|
||||
const formRef = ref();
|
||||
const formState = reactive({
|
||||
oldPassword: '',
|
||||
password: '',
|
||||
});
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: false });
|
||||
username.value = data.record.username;
|
||||
Object.assign(formState, { password: '', oldPassword: '' });
|
||||
});
|
||||
const userStore = useUserStore();
|
||||
const validatorRules: Record<string, Rule[]> = {
|
||||
oldPassword: [{ required: true, message: '请输入旧密码' }],
|
||||
password: [
|
||||
{ required: true, validator: checkPassword },
|
||||
{ pattern: /^(?=.*[0-9])(?=.*[a-zA-Z])(.{8,20})$/, message: '8-20位,需包含字母和数字' },
|
||||
],
|
||||
};
|
||||
|
||||
//表单提交事件
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
let values = await formRef.value.validateFields();
|
||||
setModalProps({ confirmLoading: true });
|
||||
//提交表单
|
||||
values.username = unref(username);
|
||||
await updatePasswordNotBindPhone(values).then((res) => {
|
||||
if (res.success) {
|
||||
createMessage.info({
|
||||
content: '密码修改成功,请重新登录!3s后自动退出登录',
|
||||
duration: 3,
|
||||
});
|
||||
//3s后返回登录页面
|
||||
setTimeout(() => {
|
||||
userStore.logout(true);
|
||||
}, 3000);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
} else {
|
||||
createMessage.warn(res.message);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新密码是否为空
|
||||
*/
|
||||
function checkPassword(_rule: Rule, value: string) {
|
||||
if (value === '') {
|
||||
return Promise.reject('请输入新密码');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.black {
|
||||
color: @text-color;
|
||||
}
|
||||
.font-size-13 {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
.gray-9e {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
.pass-padding {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.antd-modal-form {
|
||||
padding: 10px 24px 10px 24px;
|
||||
}
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user