Browse Source

Merge branch 'han_v0'

# Conflicts:
#	.gitignore
han_v3
herenshan112 4 months ago
parent
commit
28fe2d2e1d
  1. 1
      .gitignore
  2. 3
      package.json
  3. 105
      src/api/doc/space.ts
  4. 18
      src/api/doc/type.ts
  5. 325
      src/views/doc/agent.vue
  6. 246
      src/views/doc/manage.vue
  7. 251
      src/views/doc/space.vue

1
.gitignore

@ -23,3 +23,4 @@ package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
*.development *.development
*.production *.production
*.code-workspace

3
package.json

@ -40,6 +40,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@crazydos/vue-markdown": "^1.1.4",
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@onlyoffice/document-editor-vue": "^1.5.0", "@onlyoffice/document-editor-vue": "^1.5.0",
@ -73,6 +74,8 @@
"quill-image-resize-custom-module": "^4.1.7", "quill-image-resize-custom-module": "^4.1.7",
"quill-image-uploader": "^1.3.0", "quill-image-uploader": "^1.3.0",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"screenfull": "^6.0.0", "screenfull": "^6.0.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",

105
src/api/doc/space.ts

@ -1,6 +1,6 @@
import request from '@/utils/request'; import request from '@/utils/request';
import { AxiosPromise } from 'axios'; import { AxiosPromise } from 'axios';
import { matterPage,createDir,matterTreeList} from './type'; import { matterPage,createDir,matterTreeList,matterInfo} from './type';
/** /**
* *
@ -32,6 +32,18 @@ export function doCreateSpaceDir(uid:string,data?: createDir){
}); });
} }
export function doCreateAiagent(uid:string,data?: {space:string,matter:string}){
return request({
url: '/hxpan/api/space/aiagent',
method: 'post',
headers: {
'Identifier':uid,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
});
}
/** /**
* *
*/ */
@ -75,4 +87,95 @@ export function doDelSpaceMatter(uid:string,data?: any){
}, },
data: data data: data
}); });
}
//-------------------AI backend APIs---------------
/**
*
*/
export function doAiTraining(_url:string,data?: any){
return request({
url: '/aibot'+_url,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
});
}
export interface aiChatData{
inputs:object;
query:string;
response_mode:string;
conversation_id?:string;
user:string,
file?:[]
}
/**
* api
*/
export function doAiChat(_url:string,data: aiChatData,sig?:AbortSignal){
return fetch(
_url,{
method: 'post',
headers: {
'Content-Type': 'application/json'
},
signal:sig,
body: JSON.stringify(data)
}
)
}
/**
* userid获取记录列表
* @requires userid
*/
export function getAiChatList(data:any){
return request({
url: '/aibot/chat/list',
method: 'post',
data: data
});
}
/**
*
* @requires userid
* @requires uuid
*/
export function getAiChat(data: any){
return request({
url: '/aibot/chat',
method: 'post',
data: data
});
}
/**
*
* @requires userid
* @requires uuid
*/
export function delAiChat(data: any){
return request({
url: '/aibot/chat/del',
method: 'post',
data: data
});
}
/**
*
* @requires userid
* @requires uuid
*/
export function setAiChat(data: any){
return request({
url: '/aibot/chat/update',
method: 'post',
data: data
});
} }

18
src/api/doc/type.ts

@ -3,7 +3,8 @@
* @ 时间: 2025-05-12 15:39:13 * @ 时间: 2025-05-12 15:39:13
* @ 备注: 文档管理api * @ 备注: 文档管理api
*/ */
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/** /**
* *
*/ */
@ -25,6 +26,7 @@ export interface matterInfo{
puuid?:string; // parent dir uuid puuid?:string; // parent dir uuid
path?:string; path?:string;
size?:number; size?:number;
agent?:boolean;
dir?:boolean; dir?:boolean;
deleted?:boolean; deleted?:boolean;
expireInfinity?:boolean; expireInfinity?:boolean;
@ -73,4 +75,18 @@ export interface respCreateShare{
expireTime?:string; expireTime?:string;
name?:string; name?:string;
uuid?:string; uuid?:string;
}
/**
*
*/
export function doFileUpload(params:FormData,_url:string): AxiosPromise<matterInfo> {
return request({
url: _url,
method: 'post',
data: params,
headers: {
'Content-Type': 'multipart/form-data'
}
});
} }

325
src/views/doc/agent.vue

@ -0,0 +1,325 @@
<!--
@ 作者: han2015
@ 时间: 2025-05-12 15:39:13
@ 备注: aibot组件
-->
<script lang="ts" setup>
import {
Promotion,
Remove
} from '@element-plus/icons-vue'
import { doAiTraining,doAiChat,aiChatData,getAiChatList,getAiChat,setAiChat,delAiChat} from "@/api/doc/space"
import {ElText,ElInput, ButtonInstance} from "element-plus";
import { VueMarkdown } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
//
const checkedModel = ref([])
//
const aimodels = [{name:'联网检索',key:"onlineSearch"}, {name:'公司知识库',key:"useDataset"}]
const baseURL=import.meta.env.VITE_APP_BASE_API
const conversation=ref("") //uuid
const myquestion=ref('')
const controller = ref<AbortController | null>(null)
const inputState=ref(true)
const conversations=ref<chatRecord[]>([])
const centHoverItem=ref("")
const interact_msg=ref<{ask:boolean,think:string,content:string}[]>([])
const props = withDefaults(defineProps<{
userid:string,
closefunc:()=>void,
agent:{model:boolean,name:string,uuid:string}
}>(),{})
const respMsg=ref("")
//
interface message{
ask:boolean,
think:string,
content:string
}
//
interface chatRecord{
uuid:string,
agentuuid:string,
brief:string,
messages:message[]
}
/* 中断函数,供按钮调用 */
function abortFetch() {
if (controller.value) {
controller.value.abort()
controller.value = null
}
}
async function onSendTextToAI(){
if(myquestion.value==="")return;
inputState.value=false
const params={
"onlineSearch":"否",
"useDataset":"否"
}
for (let item of checkedModel.value){
if(item==="onlineSearch") params.onlineSearch="是"
if(item==="useDataset") params.useDataset="是"
}
if (conversation.value==""){
//
interact_msg.value=[{ask:true,think:"", content:myquestion.value}]
}else{
interact_msg.value.push({ask:true,think:"", content:myquestion.value})
}
controller.value = new AbortController();
try{
const res= await doAiChat(`${baseURL}/aibot/agents/${props.agent.uuid}/chat`,{
inputs: params,
query:myquestion.value,
response_mode:"streaming",
conversation_id:conversation.value,
user:atob(props.userid),//base64
},controller.value.signal
)
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
myquestion.value=""
const reader = res.body!.getReader();
const decoder = new TextDecoder('utf-8');
let chunk = ''; //
while (true) {
const {done, value} = await reader.read()
if (done) break;
//
chunk += decoder.decode(value, {stream: true});
const lines = chunk.split(/\n/);
chunk = lines.pop() || ''
for (const line of lines) {
if (!line.trim()) continue; //
if (line.startsWith('data: ')) {
const data = line.slice(6);
const json = JSON.parse(data);
if(json.event==="message"){
conversation.value=json.conversation_id
respMsg.value+=json.answer
}
}
}
}
}catch (e: any) {
if (e.name === 'AbortError') {
console.log('用户手动中断')
}
}finally{
inputState.value=true
}
const arr=respMsg.value.split("</think>")
if(arr.length===1){
interact_msg.value.push({ask:false,think:"",content:arr[0]})
}else{
//
interact_msg.value.push({ask:false,think:arr[0],content:arr[1]})
}
respMsg.value=""
setAiChat({
"userid":atob(props.userid),
"uuid":conversation.value,
"brief":interact_msg.value[0].content,
"content":JSON.stringify(interact_msg.value)
})
}
//
function loadKnownLibList(){
//userid base64
getAiChatList({"userid":atob(props.userid)}).then(resp=>{
conversations.value=resp.data
})
}
//
function showChat(uuid:string){
getAiChat({
"userid":atob(props.userid),
"uuid":uuid
}).then(resp=>{
interact_msg.value = JSON.parse(resp.data.content)
conversation.value=resp.data.uuid
})
}
//
function onDelChat(uuid:string){
delAiChat({
"userid":atob(props.userid),
"uuid":uuid
}).then(resp=>{
loadKnownLibList()
})
}
function handleMouseEnter(row:any){
if(centHoverItem.value==row.uuid) return;
centHoverItem.value=row.uuid
}
function handleMouseLeave(){
centHoverItem.value=""
}
function newContext(){
inputState.value=true
interact_msg.value=[]
conversation.value=""
loadKnownLibList() //
}
function resetContext(){
interact_msg.value=[]
conversation.value=""
props.closefunc()
}
//
onMounted(() => {
loadKnownLibList()
});
</script>
<template>
<el-drawer
:model-value="agent.model"
:title="agent.name+' : 知识库'"
direction="rtl"
size="80%"
@close="resetContext"
:style="{padding:'17px',backgroundColor:'#f3f3f3'}">
<div style="display:grid;grid-template-columns:1fr 4fr; width: 100%;height: 100%;">
<div style="overflow-y: auto;">
<ul>
<li class="action_menu" @click="newContext">
新建会话
</li>
<li class="list_item" v-for="item in conversations" @mouseover="handleMouseEnter(item)" @mouseleave="handleMouseLeave()" @click="showChat(item.uuid)">{{ item.brief }}
<el-button v-show="centHoverItem == item.uuid" icon="Delete" size="small" circle @click="(e)=>{e.stopPropagation();onDelChat(item.uuid)}"></el-button>
</li>
</ul>
</div>
<div style="position: relative;background: white;overflow-y: auto;">
<div class="reply_area" >
<template v-for="msg of interact_msg">
<el-text v-if="msg.ask" class="t_ask" >{{ msg.content }}</el-text>
<div v-else class="t_resp">
<el-text style="white-space: pre-line" v-html="msg.think"></el-text>
<VueMarkdown :markdown="msg.content" :rehype-plugins="[rehypeRaw]" :remark-plugins="[remarkGfm]" ></VueMarkdown>
</div>
</template>
<VueMarkdown :markdown="respMsg" :rehype-plugins="[rehypeRaw]" class="t_resp"></VueMarkdown>
</div>
<div class="question_com" :class="{newquestion:conversation!='' || !inputState}">
<h1 v-show="conversation =='' && inputState" style="font-size: 78px;margin: 10px;">恒信高科AI平台</h1>
<el-checkbox-group v-model="checkedModel" style="display:flex; margin-bottom: 10px;">
<el-checkbox-button v-for="mod in aimodels" :value="mod.key">
{{ mod.name }}
</el-checkbox-button>
</el-checkbox-group>
<el-input placeholder="问灵犀..." v-model="myquestion" input-style="border-radius: 20px;"
resize="none" :autosize="{minRows: 4}" type="textarea" />
<el-button :style="{display :inputState ? '':'none'}" type="primary" :icon="Promotion" circle @click="onSendTextToAI"/>
<el-button :style="{display :inputState ? 'none':''}" type="primary" :icon="Remove" circle @click="abortFetch"/>
<span>内容由 AI 生成请仔细甄别</span>
</div>
</div>
</div>
</el-drawer>
</template>
<style lang="scss" scoped>
.app_container {
padding: 10px 30px 0px 30px;
height: 100%;
width: 100%;
}
.newquestion{
bottom: 20px;
}
.question_com{
position: fixed;
width: 52%;
margin: 0 50px;
text-align: center;
display: block;
button{
position: absolute;
bottom: 25px;
right: 5px;
}
}
.reply_area{
display: flex;
min-height: 20%;
flex-direction: column;
margin: 15px 15px 110px 0px;
}
.t_ask{
align-self: end;
line-height: 34px;
background-color: rgb(188 211 241);
padding: 0 30px;
border-radius:10px;
margin: 15px;
}
.t_resp{
align-self: start;
line-height: 30px;
margin: 20px 33px;
font-size: 18px;
color: black;
}
.dynamic-width-message-box-byme .el-message-box__message{
width: 100%;
}
.action_menu{
background-color: white;
padding: 10px 8px;
margin: 3px 14px 18px 0;
border-radius: 8px;
border: 1px solid #c9c6c6;
}
.list_item{
display: flex;
background-color: #dddddd;
padding: 10px 8px;
margin: 3px 14px 3px 0;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 8px;
text-wrap-mode: nowrap;
button{
margin-left: auto;
}
}
</style>
<style>
think {
color: #939393;
margin-bottom: 8px;
display: block;
}
</style>

246
src/views/doc/manage.vue

@ -8,7 +8,7 @@ import { getExpirTime, getFileIcon, readableSize} from "./tools"
import sharePermission from './sharePermission.vue'; import sharePermission from './sharePermission.vue';
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { getMatterList,postCreateDir,postDelMatter,postCreateShare,postMatterRename,postDelMatBatch,getMySpaces,doCreateSpace} from "@/api/doc/index" import { getMatterList,postCreateDir,postDelMatter,postCreateShare,postMatterRename,postDelMatBatch,getMySpaces,doCreateSpace} from "@/api/doc/index"
import { matterPage,matterInfo,respCreateShare,matterTree } from "@/api/doc/type" import { matterPage,matterInfo,respCreateShare,matterTree, doFileUpload} from "@/api/doc/type"
import { h } from 'vue' import { h } from 'vue'
import { import {
Delete, Delete,
@ -22,8 +22,7 @@ import {
Avatar, Avatar,
Plus Plus
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import {ElSelect,ElOption, ElText,ElInput,TableInstance,ElMessage,UploadFile,UploadFiles,ElPagination,ElTree,TreeInstance} from "element-plus"; import {ElSelect,ElOption, ElText,ElInput,TableInstance,ElMessage,UploadFile,UploadFiles,ElPagination,ElTree,TreeNode} from "element-plus";
import type { TreeNode } from 'element-plus/es/components/tree/src/tree.type'
import preview from './preview.vue'; import preview from './preview.vue';
import space from './space.vue'; import space from './space.vue';
@ -52,17 +51,24 @@ const currentHoverRow=ref("") //table 行的按钮控制
const selectedValue = ref("sixhour") // const selectedValue = ref("sixhour") //
const tabSelected=ref<matterInfo[]>([]) //table const tabSelected=ref<matterInfo[]>([]) //table
//to support tree mode refactor //to support tree mode refactor
const treeData=ref<matterTree[]>([{name:'个人空间',uuid:'root',children:[]}]) const treeData=ref<matterTree[]>([])//{name:'',uuid:'root',children:[]}
const treeRef = ref(); const treeRef = ref();
const currentNode=ref<matterTree>({}) // const currentNode=ref<matterTree>({}) //
const officeHost=import.meta.env.VITE_OFFICE_HOST const officeHost=import.meta.env.VITE_OFFICE_HOST
const dynamicVNode = ref<VNode | null>(null) //permission const dynamicVNode = ref<VNode | null>(null) //permission
const multipleTableRef = ref<TableInstance>() const multipleTableRef = ref<TableInstance>()
const paginInfo = ref({ page: 0, total: 0 }) const paginInfo = ref({ page: 0, total: 0 })
const PRIVATESPACE = ref(true) // 2 const PRIVATESPACE = ref(true) // 2
const SpaceID= ref<{name:string,uuid:string,userUuid:string}>({}) //spaceid const SpaceID= ref<{name:string,uuid:string,userUuid:string}>({}) //spaceid
const SpaceList=ref<{name:string,uuid:string,userUuid:string}[]>([]) const SpaceList=ref<{name:string,uuid:string,userUuid:string}[]>([])
const spaceEleRef = ref() //space,spaceTreeRefspaceTree
const spaceTreeData=ref<matterTree[]>([])//{name:'',uuid:'root',children:[]}
const spaceTreeRef = ref(); //space
let spaceNodeUid="" //currentNode
const Departs = computed(() => { const Departs = computed(() => {
return `${userStore.userInfoCont.company},${userStore.userInfoCont.department},${userStore.userInfoCont.organization}` return `${userStore.userInfoCont.company},${userStore.userInfoCont.department},${userStore.userInfoCont.organization}`
}) })
@ -277,9 +283,11 @@ function onLoadMatterList(){
getMatterList(uid,_page).then((resp)=>{ getMatterList(uid,_page).then((resp)=>{
//page+1 index1apiindex0 //page+1 index1apiindex0
paginInfo.value={total:resp.data.totalPages,page:resp.data.page} paginInfo.value={total:resp.data.totalPages,page:resp.data.page}
matterList.value=resp.data.data.filter((item)=>{ matterList.value=resp.data.data
return !item.dir //2025-08-07: space
}) // .filter((item)=>{
// return !item.dir
// })
}) })
} }
//----------for dir----------- //----------for dir-----------
@ -307,7 +315,7 @@ function onCreateDir(){
{uuid:resp.data.uuid,dir:false,name:resp.data.name,puuid:resp.data.puuid}, {uuid:resp.data.uuid,dir:false,name:resp.data.name,puuid:resp.data.puuid},
currentNode.value.uuid currentNode.value.uuid
) )
onLoadMatterList() //onLoadMatterList()
}) })
.catch((e)=>{ .catch((e)=>{
ElMessage.error(e.msg) ElMessage.error(e.msg)
@ -331,14 +339,16 @@ function onNodeExpand(node: TreeNode, resolve: (data: matterTree[]) => void, rej
getMatterList(uid, _page).then((resp) => { getMatterList(uid, _page).then((resp) => {
paginInfo.value = { total: resp.data.totalPages, page: resp.data.page } paginInfo.value = { total: resp.data.totalPages, page: resp.data.page }
matterList.value = resp.data.data.filter((item)=>{ matterList.value = resp.data.data
return !item.dir // .filter((item)=>{
}) // return !item.dir
// })
let node_data = resp.data.data.filter((item) => { let node_data = resp.data.data.filter((item) => {
return item.dir return item.dir
}).map((item) => { }).map((val) => {
item.dir = !item.dir const copy = structuredClone(val)
return item copy.dir = !copy.dir
return copy
}) })
resolve(node_data) resolve(node_data)
@ -353,6 +363,8 @@ function onNodeClick(data:matterTree,node:TreeNode,self:any,env:any){
if (currentNode.value.uuid === data.uuid) return; if (currentNode.value.uuid === data.uuid) return;
const cuuid = data.uuid const cuuid = data.uuid
currentNode.value = data currentNode.value = data
onLoadMatterList()
return
let _page: matterPage = { let _page: matterPage = {
page: 0, page: 0,
pageSize: 50, pageSize: 50,
@ -388,6 +400,10 @@ function onNodeClick(data:matterTree,node:TreeNode,self:any,env:any){
// //
function onPrivateView(row:matterInfo){ function onPrivateView(row:matterInfo){
if(row.dir){
alert("目录不支持预览")
return
}
let a = `${row.uuid}${row.name}` let a = `${row.uuid}${row.name}`
let _token=document.cookie.match(/hxpan=([\w-]*)/) let _token=document.cookie.match(/hxpan=([\w-]*)/)
if (_token&&_token.length>1){ if (_token&&_token.length>1){
@ -402,6 +418,96 @@ function onPrivateView(row:matterInfo){
}) })
} }
//--------------UPGRADE: multy file upload section----------------
//, 使html
async function onCustomUpload(e:Event){
const files = e.target!.files??[]
for(let ff of files){
await handleSingleFile(ff)
}
onLoadMatterList() //
}
async function handleSingleFile(ff:File){
const fields=new FormData()
fields.append("userUuid",uploadFormData.value.userUuid)
fields.append("puuid",uploadFormData.value.puuid)
fields.append("file",ff)
const res = await doFileUpload(fields,'/hxpan/api/matter/upload')
if(res.code!=200){
console.log(ff.name+"上传失败! ")
alert(ff.name+"上传失败! ")
}
}
//input
//
async function uploadFolder(e:Event){
const files = e.target!.files??[]
for(let f of files){
await handleFolderFile(f)
}
onLoadMatterList() //
}
async function handleFolderFile(option:File){
//
const _path = option.webkitRelativePath
const _dir=_path.replace(/\/[^/]+\w+$/,"") //,[^/]
const node = matterList.value.filter((item)=>{
return item.dir && item.path?.endsWith(_dir)
})
let puuid=""
//
if(node.length==0){
const subs= await doCreateMultyDir(_dir,currentNode.value.uuid)
matterList.value.push(...subs) //
const newnodes=matterList.value.filter((item)=>{
return item.dir && item.path?.endsWith(_dir)
})
if(newnodes.length>0){
puuid=newnodes[0].uuid
}else{
console.log(_path+"上传失败! ")
alert(_path +" 上传失败! ")
return
}
}else{
puuid=node[0].uuid
}
const fields = new FormData()
fields.append('file', option)
fields.append("space",uploadFormData.value.space)
fields.append("puuid",puuid)
const res = await doFileUpload(fields,'/hxpan/api/matter/upload')
if(res.code!=200){
console.log(_path+"上传失败! ")
alert(_path +" 上传失败! ")
}
}
//matterinfos
async function doCreateMultyDir(path:string,uuid:string){
const list=[];
const dirs=path.split("/")
for(let i=0;i<dirs.length;i++){
await postCreateDir(uid,{
userUuid:uid,
puuid:uuid,
name:dirs[i],
}).then((resp)=> {
uuid=resp.data.uuid //uuid, puuid
list.push(resp.data)
})
}
return list
}
function handleMouseEnter(row:any){ function handleMouseEnter(row:any){
currentHoverRow.value=row.name currentHoverRow.value=row.name
} }
@ -410,6 +516,7 @@ function handleSingleUpload(response:any){
fileList.value=[] fileList.value=[]
onLoadMatterList() onLoadMatterList()
} }
interface uploadError{ interface uploadError{
msg:string msg:string
} }
@ -417,6 +524,9 @@ interface uploadError{
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){ function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
ElMessage.error(JSON.parse(error.message).msg) ElMessage.error(JSON.parse(error.message).msg)
} }
//-----------------------------------------------------------
//-------------------space feature--------------------- //-------------------space feature---------------------
function onNewSpace(){ function onNewSpace(){
const newname=ref("") const newname=ref("")
@ -441,14 +551,54 @@ function onNewSpace(){
}) })
} }
function onSpaceNodeClick(data:matterTree,node:TreeNode,self:any,env:any){
if(PRIVATESPACE.value) { //
PRIVATESPACE.value=false
}
//
if(spaceNodeUid==data.uuid) return;
spaceNodeUid=data.uuid
if(data.uuid.startsWith("s0") && data.uuid!=SpaceID.value.uuid){ //
SpaceID.value={
name: data.name ?? "",
uuid: data.uuid ?? "",
userUuid: data.userUuid ?? ""
};
PRIVATESPACE.value=false;
}else{
let matter= {
uuid:data.uuid==SpaceID.value.uuid?"root":data.uuid,
puuid: data.puuid,
name:data.name,
agent:data.agent,
dir:true
}
//
spaceEleRef.value.handleDoubleClick(matter)
}
}
function flushSpaceTree(uuid:string,data:matterTree[]){
if(uuid==="root") uuid=SpaceID.value.uuid
spaceTreeRef.value.updateKeyChildren(uuid,data)
}
//------------------------------------------------------ //------------------------------------------------------
//http://172.20.2.87:6010/api/alien/preview/5a10aaf6-396e-4d9a-7e87-3c5c8029d4db/123.png?ir=fill_100_100 //http://172.20.2.87:6010/api/alien/preview/5a10aaf6-396e-4d9a-7e87-3c5c8029d4db/123.png?ir=fill_100_100
// //
onMounted(() => { onMounted(() => {
onNodeClick(treeData.value[0], null as unknown as TreeNode, null, null) treeRef.value.append(
{name:'个人空间',uuid:'root',dir:false},
currentNode.value.uuid
)
// //
getMySpaces(uid,{roles:Departs}).then((resp)=>{ getMySpaces(uid,{roles:Departs}).then((resp)=>{
SpaceList.value=resp.data //SpaceList.value=resp.data
resp.data.forEach((item)=>{
spaceTreeRef.value.append({name:item.name,uuid:item.uuid,dir:false,userUuid:item.userUuid})
})
}) })
}); });
@ -466,6 +616,7 @@ const handleSelectionChange = (val:matterInfo[]) => {
style="max-width: 600px" style="max-width: 600px"
:data="treeData" :data="treeData"
node-key="uuid" node-key="uuid"
highlight-current
lazy lazy
:props="{label: 'name',children:'children',isLeaf:'dir'}" :props="{label: 'name',children:'children',isLeaf:'dir'}"
:load="onNodeExpand" :load="onNodeExpand"
@ -473,7 +624,18 @@ const handleSelectionChange = (val:matterInfo[]) => {
@node-click="onNodeClick" @node-click="onNodeClick"
/> />
<el-button style="margin: 10px 0;" :icon="Plus" @click="onNewSpace"> 共享空间</el-button> <el-button style="margin: 10px 0;" :icon="Plus" @click="onNewSpace"> 共享空间</el-button>
<ul style="background-color: white;"> <el-tree
ref="spaceTreeRef"
style="max-width: 600px"
:data="spaceTreeData"
node-key="uuid"
accordion
highlight-current
lazy
:props="{label: 'name',children:'children',isLeaf:'dir'}"
@node-click="onSpaceNodeClick"
/>
<ul style="background-color: white;max-width: 600px;">
<li class="spaceitem" v-for="sp in SpaceList" @click="()=>{SpaceID=sp; PRIVATESPACE=false;}">{{ sp.name }}</li> <li class="spaceitem" v-for="sp in SpaceList" @click="()=>{SpaceID=sp; PRIVATESPACE=false;}">{{ sp.name }}</li>
</ul> </ul>
</div> </div>
@ -483,35 +645,39 @@ const handleSelectionChange = (val:matterInfo[]) => {
<el-link v-if="currentNode.name!=='root'" @click="onNodeClick(treeData[0], null as unknown as TreeNode, null, null)"> <el-link v-if="currentNode.name!=='root'" @click="onNodeClick(treeData[0], null as unknown as TreeNode, null, null)">
<span style="font-weight: bold;margin-right: 5px;">根目录</span>/ <span style="font-weight: bold;margin-right: 5px;">根目录</span>/
</el-link> </el-link>
<span style="font-weight: bold;margin:0 5px;">{{ currentNode.name }}</span> <span style="font-weight: bold;margin:0 5px;align-content:center;">{{ currentNode.name }}</span>
<el-col :span="6" class="search">
<el-input placeholder="搜索文件" v-model="searchname" @blur="searchname===''?onLoadMatterList():''"/>
<el-button :icon="Search" @click="onSearchFile(searchname)"></el-button>
</el-col>
</el-row> </el-row>
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="14"> <el-col :span="14">
<el-upload class="el-button el-button--default" :file-list="fileList" <div class="el-button el-button--default" style="position: relative;">
:data="uploadFormData" <input type="file" style="position: absolute;opacity: 0;width: 50px;"
:on-success="handleSingleUpload" @change="onCustomUpload" multiple />
:on-error="handleSigLoadErr" 文件上传
:show-file-list="false" </div>
:action="apiURL+'/matter/upload'" :limit="1">
<span>上传</span> <div class="el-button el-button--default" style="position: relative;">
</el-upload> <input type="file" style="position: absolute;opacity: 0;width: 50px;"
<!-- <el-button>上传文件夹</el-button> --> @change="uploadFolder"
webkitdirectory
multiple
/>
文件夹上传
</div>
<el-button @click="createDir">新建目录</el-button> <el-button @click="createDir">新建目录</el-button>
<span v-if="tabSelected.length>1" style="margin:12px"> <span v-if="tabSelected.length>1" style="margin:12px">
<el-button @click="onShareMatter()">分享</el-button> <el-button @click="onShareMatter()">分享</el-button>
<el-button @click="onDelMatBatch">删除</el-button> <el-button @click="onDelMatBatch">删除</el-button>
</span> </span>
<el-button @click="onLoadMatterList()">刷新</el-button>
<el-button type="danger" plain @click="onShareMatter({uuid:currentNode.uuid,name:currentNode.name})">分享目录</el-button> <el-button type="danger" plain @click="onShareMatter({uuid:currentNode.uuid,name:currentNode.name})">分享目录</el-button>
<el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true, <el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true,
puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button> puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button>
</el-col> </el-col>
<el-col :span="8" class="search">
<el-input placeholder="搜索文件" v-model="searchname" @blur="searchname===''?onLoadMatterList():''"/>
<el-button :icon="Search" @click="onSearchFile(searchname)"></el-button>
</el-col>
</el-row> </el-row>
<el-row :gutter="24" style="height: 84%;overflow-y: auto;"> <el-row :gutter="24" style="height: 84%;overflow-y: auto;">
@ -541,7 +707,7 @@ const handleSelectionChange = (val:matterInfo[]) => {
<el-table-column width="250" align="center"> <el-table-column width="250" align="center">
<template #default="scope"> <template #default="scope">
<div v-show="currentHoverRow === scope.row.name"> <div v-show="currentHoverRow === scope.row.name">
<el-button size="small" :icon="Promotion" circle ></el-button> <!-- <el-button size="small" :icon="Promotion" circle ></el-button> -->
<el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button> <el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button>
<el-button size="small" :icon="Share" circle @click="onShareMatter(scope.row)"></el-button> <el-button size="small" :icon="Share" circle @click="onShareMatter(scope.row)"></el-button>
<el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button> <el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button>
@ -565,8 +731,10 @@ const handleSelectionChange = (val:matterInfo[]) => {
</el-row> </el-row>
</div> </div>
<div v-else class="app_container"> <div v-else class="app_container">
<space :uid="uid" :spaceid="SpaceID.uuid" :roles="Departs" :spacename="SpaceID.name" :owner="SpaceID.userUuid" <space ref="spaceEleRef" :uid="uid" :tree="spaceTreeRef"
:officeHost="officeHost" :site-host="siteHost" :api-u-r-l="apiURL"></space> :spaceid="SpaceID.uuid" :roles="Departs" :spacename="SpaceID.name" :owner="SpaceID.userUuid"
:officeHost="officeHost" :site-host="siteHost" :api-u-r-l="apiURL"
:flushSpaceTree="flushSpaceTree"></space>
</div> </div>
@ -582,7 +750,8 @@ const handleSelectionChange = (val:matterInfo[]) => {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
.el-tree{ .el-tree{
--el-tree-node-hover-bg-color:#d9dadb; --el-color-primary-light-9:#6eb3f8;
--el-tree-node-hover-bg-color:#a1c7ee;
--el-tree-node-content-height:43px; --el-tree-node-content-height:43px;
--el-tree-expand-icon-color:#4c4c4e; --el-tree-expand-icon-color:#4c4c4e;
} }
@ -593,8 +762,11 @@ const handleSelectionChange = (val:matterInfo[]) => {
height: calc(100% - 10px); height: calc(100% - 10px);
overflow: hidden; overflow: hidden;
overflow-y: auto; overflow-y: auto;
position: relative;
} }
.search{ .search{
margin-left: auto;
margin-right: 20px; margin-right: 20px;
display:inherit; display:inherit;
} }

251
src/views/doc/space.vue

@ -1,25 +1,25 @@
<!-- <!--
@ 作者: han2015 @ 作者: han2015
@ 时间: 2025-05-12 15:39:13 @ 时间: 2025-05-12 15:39:13
@ 备注: 文档管理组件 @ 备注: 共享空间组件
--> -->
<script lang="ts" setup> <script lang="ts" setup>
import { getFileIcon, readableSize,fileType} from "./tools" import { getFileIcon, readableSize,fileType} from "./tools"
import sharePermission from './sharePermission.vue'; import sharePermission from './sharePermission.vue';
import { matterPage,matterInfo} from "@/api/doc/type" import { matterPage,matterInfo,matterTree,doFileUpload} from "@/api/doc/type"
import { doAccessManage,getSpaceMatterList,doCreateSpaceDir,doDelSpaceMatter,doDelSpace} from "@/api/doc/space" import { doAccessManage,getSpaceMatterList,doCreateSpaceDir,doDelSpaceMatter,doDelSpace,
import { h, proxyRefs } from 'vue' doAiTraining ,doCreateAiagent} from "@/api/doc/space"
import { h } from 'vue'
import { import {
Delete, Delete,
View, View,
Download, Download,
Plus, Plus,
Edit, Edit,
Promotion,
Folder, Folder,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus"; import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus";
import preview from './preview.vue'; import aiagent from './agent.vue';
import router from "@/router"; import router from "@/router";
const matterList = ref<matterInfo[]>([]) const matterList = ref<matterInfo[]>([])
@ -27,26 +27,36 @@ const newdir=ref("") //创建新目录时的目录名
const currentHoverRow=ref("") //table const currentHoverRow=ref("") //table
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) // const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) //
const selectedValue = ref("sixhour") //
const tabSelected=ref<matterInfo[]>([]) //table const tabSelected=ref<matterInfo[]>([]) //table
const currentNode=ref<matterInfo>({}) // const currentNode=ref<matterInfo>({}) //
let isNewNode=true //
const dynamicVNode = ref<VNode | null>(null) //permission const dynamicVNode = ref<VNode | null>(null) //permission
const paginInfo = ref({ page: 0, total: 0 }) const paginInfo = ref({ page: 0, total: 0 })
const props = withDefaults(defineProps<{ //-----------AI---------------------
uid:string, //uuid,base64 //const agent=ref<{model:boolean,name:string}>({})
const currentAgent=ref<{model:boolean,name:string,uuid:string}>({})
//---------------------------------
const props = withDefaults(defineProps<{
uid:string, //uuid,base64,Identifierbase64
tree:object,
spaceid:string, spaceid:string,
spacename:string, spacename:string,
officeHost:string, officeHost:string,
siteHost:string, siteHost:string,
owner:string, owner:string,
apiURL:string, apiURL:string,
roles:string roles:string,
flushSpaceTree:(uuid:string,data:matterTree[])=>void
}>(),{}) }>(),{})
//
watch(props,()=>{ watch(props,()=>{
currentNode.value.uuid="root"
onLoadMatterList() onLoadMatterList()
}) })
@ -56,7 +66,6 @@ const uploadFormData = computed(() => {
puuid: currentNode.value.uuid, // uuidroot puuid: currentNode.value.uuid, // uuidroot
} }
}); });
const fileList=ref([])//upload files
//--------------&------------- //--------------&-------------
function onAccessManage(){ function onAccessManage(){
@ -151,6 +160,22 @@ function onLoadMatterList(){
//page+1 index1apiindex0 //page+1 index1apiindex0
paginInfo.value={total:resp.data.totalPages, page:resp.data.page} paginInfo.value={total:resp.data.totalPages, page:resp.data.page}
matterList.value=resp.data.data matterList.value=resp.data.data
//使
let node_data = resp.data.data.filter((item) => {
return item.dir
}).map(val => {
const copy = structuredClone(val)
copy.dir = !copy.dir
return copy
})
if(isNewNode) {
//uuidrootspaceiduuid
//rootuuidspaceidroot
if(currentNode.value.uuid=="root") props.flushSpaceTree(props.spaceid,node_data);
else props.flushSpaceTree(currentNode.value.uuid,node_data);
}
}) })
} }
//----------for dir----------- //----------for dir-----------
@ -182,13 +207,24 @@ function onCreateDir(){
}) })
} }
//------------------------------------------ //------------------------------------------
// @cell-dblclick="handleDoubleClick"
// //
function handleDoubleClick(row:matterInfo,ind?:number){ function handleDoubleClick(row:matterInfo,ind?:number){
if(row.dir){ if(row.dir){
if(row.agent){
currentAgent.value={name:row.name,model:false,uuid:row.uuid}
}
isNewNode=true
//1
//2:
if(currentNode.value.puuid==row.uuid || row.uuid=="root") isNewNode=false
currentNode.value=row
onLoadMatterList()
/* breadcrumblis
//table //table
if(typeof(ind)==="number"){ if(typeof(ind)==="number"){
// //,
if(breadcrumbList.value.length>1){ if(breadcrumbList.value.length>1){
breadcrumbList.value=breadcrumbList.value.slice(0,ind+1) breadcrumbList.value=breadcrumbList.value.slice(0,ind+1)
currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1] currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1]
@ -196,10 +232,16 @@ function handleDoubleClick(row:matterInfo,ind?:number){
} }
}else{ }else{
// //
//
if(row.agent){
currentAgent.value={name:row.name,model:false,uuid:row.uuid}
}
currentNode.value=row currentNode.value=row
breadcrumbList.value.push(row) breadcrumbList.value.push(row)
onLoadMatterList() onLoadMatterList()
} }
*/
} }
} }
@ -208,7 +250,7 @@ function handleMouseEnter(row:any){
} }
// //
function handleSingleUpload(response:any){ function handleSingleUpload(response:any){
fileList.value=[] handleAiUpload(response.data)
onLoadMatterList() onLoadMatterList()
} }
interface uploadError{ interface uploadError{
@ -218,6 +260,138 @@ interface uploadError{
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){ function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
ElMessage.error(JSON.parse(error.message).msg) ElMessage.error(JSON.parse(error.message).msg)
} }
//, 使
async function onCustomUpload(e:Event){
const files = e.target!.files??[]
for(let ff of files){
await handleSingleFile(ff)
}
onLoadMatterList() //
}
async function handleSingleFile(ff:File){
const fields=new FormData()
fields.append("space",uploadFormData.value.space)
fields.append("puuid",uploadFormData.value.puuid)
fields.append("file",ff)
const res = await doFileUpload(fields,'/hxpan/api/space/upload')
if(res.code!=200){
console.log(ff.name+"上传失败! ")
alert(ff.name+"上传失败! ")
}
//AI
handleAiUpload(res.data)
}
//input
//
async function uploadFolder(e:Event){
const files = e.target!.files??[]
for(let f of files){
await handleFolderFile(f)
}
onLoadMatterList() //
}
async function handleFolderFile(option:File){
//
const _path = option.webkitRelativePath
const _dir=_path.replace(/\/[^/]+\w+$/,"") //,[^/]
const node = matterList.value.filter((item)=>{
return item.dir && item.path?.endsWith(_dir)
})
let puuid=""
//
if(node.length==0){
const subs= await doCreateMultyDir(_dir,currentNode.value.uuid)
matterList.value.push(...subs) //
const newnodes=matterList.value.filter((item)=>{
return item.dir && item.path?.endsWith(_dir)
})
if(newnodes.length>0){
puuid=newnodes[0].uuid
}else{
console.log(_path+"上传失败! ")
alert(_path +" 上传失败! ")
return
}
}else{
puuid=node[0].uuid
}
const fields = new FormData()
fields.append('file', option)
fields.append("space",uploadFormData.value.space)
fields.append("puuid",puuid)
const res = await doFileUpload(fields,'/hxpan/api/space/upload')
if(res.code!=200){
console.log(_path+"上传失败! ")
alert(_path +" 上传失败! ")
}
//AIhandleAiUpload
handleAiUpload(res.data)
}
//matterinfos
async function doCreateMultyDir(path:string,uuid:string){
const list=[];
const dirs=path.split("/")
for(let i=0;i<dirs.length;i++){
await doCreateSpaceDir(props.uid,{
puuid:uuid,
name:dirs[i],
space:props.spaceid,
roles:props.roles,
}).then((resp)=> {
uuid=resp.data.uuid //uuid, puuid
list.push(resp.data)
})
}
return list
}
//-------------------------AI section--------------
//
function onAiAgent(){
if(currentNode.value.uuid=="root"){
alert("根目录不支持创建智能体")
return
}
if(currentNode.value.agent){
alert("当前目录已经是智能体目录")
return
}
ElMessageBox.confirm(`确认创建智能体( ${currentNode.value.name}) ?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
doCreateAiagent(props.uid,{
space:props.spaceid,
matter:currentNode.value.uuid
}).then(()=>{
router.replace({ query: { t: Date.now() } })
})
})
}
function handleAiUpload(info:matterInfo){
//
if (currentNode.value.agent){
doAiTraining(`/agents/${currentAgent.value.uuid}/updates`,{"matter":info.uuid}).then(resp=>{
console.log(resp)
})
}
}
//-------------------------------------------------
//-------------------edit & preive file for space--------------------- //-------------------edit & preive file for space---------------------
// //
function onPrivateView(row:matterInfo){ function onPrivateView(row:matterInfo){
@ -259,6 +433,9 @@ async function onlyOfficeEdit(row:matterInfo){
// //
onMounted(() => { onMounted(() => {
currentNode.value.uuid="root" currentNode.value.uuid="root"
//AI
currentAgent.value={name:"通用AI",model:false,uuid:"5bd9b0e9-d3f4-4089-670a-880009e925a8"}
onLoadMatterList() onLoadMatterList()
}); });
@ -266,6 +443,8 @@ const handleSelectionChange = (val:matterInfo[]) => {
tabSelected.value = val tabSelected.value = val
} }
defineExpose({handleDoubleClick})
// //
function isOwner(){ function isOwner(){
return props.uid===btoa(props.owner) return props.uid===btoa(props.owner)
@ -273,12 +452,13 @@ function isOwner(){
</script> </script>
<template> <template>
<div> <div>
<el-row :gutter="24" style="margin: 12px 0px;"> <el-row :gutter="24" style="margin: 12px 0px;">
<span class="el-breadcrumb" style="font-weight: bold;">[ {{ props.spacename }} ] : </span> <span class="el-breadcrumb" style="font-weight: bold;">[ {{ props.spacename }} ] : </span>
<el-breadcrumb separator="/"> <el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item,index) in breadcrumbList" <el-breadcrumb-item v-for="(item,index) in breadcrumbList"
:key="index" @click="index===breadcrumbList.length-1?'': handleDoubleClick(item,index)"> :key="index" @click="handleDoubleClick(item,index)">
<span style="font-weight: bold;">{{ item.name }}</span> <span style="font-weight: bold;">{{ item.name }}</span>
</el-breadcrumb-item> </el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
@ -286,20 +466,28 @@ function isOwner(){
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="14"> <el-col :span="14">
<el-upload class="el-button el-button--default" :file-list="fileList" <div class="el-button el-button--default" style="position: relative;">
:data="uploadFormData" <input type="file" style="position: absolute;opacity: 0;width: 50px;"
:on-success="handleSingleUpload" @change="onCustomUpload" multiple />
:on-error="handleSigLoadErr" 文件上传
:show-file-list="false" </div>
:action="apiURL+'/space/upload'" :limit="1">
<span>上传文件</span> <div class="el-button el-button--default" style="position: relative;">
</el-upload> <input type="file" style="position: absolute;opacity: 0;width: 50px;"
@change="uploadFolder"
webkitdirectory
multiple
/>
文件夹上传
</div>
<el-button @click="createDir">新建目录</el-button> <el-button @click="createDir">新建目录</el-button>
<el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true, <el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true,
puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button> puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button>
</el-col> </el-col>
<el-button-group class="control" v-if="isOwner()" style="margin-right: 10px;margin-left: auto;"> <el-button style="margin-left: auto;" @click="()=>currentAgent.model=true">AI助手</el-button>
<el-button-group v-if="isOwner()" class="control" style="margin: 0 10px;">
<el-button :icon="Plus" @click="onAccessManage">成员</el-button> <el-button :icon="Plus" @click="onAccessManage">成员</el-button>
<el-button :icon="Plus" @click="onAiAgent">创建智能体</el-button>
<el-button :icon="Delete" @click="onDeleteSpace">删除</el-button> <el-button :icon="Delete" @click="onDeleteSpace">删除</el-button>
</el-button-group> </el-button-group>
</el-row> </el-row>
@ -313,7 +501,6 @@ function isOwner(){
row-key="uuid" row-key="uuid"
:row-style ="() => ({ lineHeight: '36px' })" :row-style ="() => ({ lineHeight: '36px' })"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@cell-dblclick="handleDoubleClick"
@cell-mouse-enter="handleMouseEnter"> @cell-mouse-enter="handleMouseEnter">
<el-table-column type="selection" width="50" /> <el-table-column type="selection" width="50" />
<el-table-column width="450" property="name" label="文件名"> <el-table-column width="450" property="name" label="文件名">
@ -328,10 +515,10 @@ function isOwner(){
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="250" align="center"> <el-table-column width="250">
<template #default="scope"> <template #default="scope" >
<div v-show="currentHoverRow === scope.row.name"> <el-tag v-if="scope.row.agent" effect="dark" size="small" type="success" round >智能体</el-tag>
<el-button size="small" :icon="Promotion" circle ></el-button> <div v-show="currentHoverRow === scope.row.name" style="display:inline; margin-left:15px">
<el-button size="small" :icon="Edit" circle @click="onlyOfficeEdit(scope.row)"></el-button> <el-button size="small" :icon="Edit" circle @click="onlyOfficeEdit(scope.row)"></el-button>
<el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button> <el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button>
<el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button> <el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button>
@ -354,6 +541,8 @@ function isOwner(){
</el-row> </el-row>
</div> </div>
<aiagent :agent="currentAgent" :userid="uid" :uuid="currentAgent" :closefunc="()=>{currentAgent.model=false}"></aiagent>
<div v-if="dynamicVNode"> <div v-if="dynamicVNode">
<component :is="dynamicVNode" /> <component :is="dynamicVNode" />
</div> </div>

Loading…
Cancel
Save