diff --git a/.gitignore b/.gitignore index 9143980..8a27f75 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ package-lock.json pnpm-lock.yaml *.development *.production +*.code-workspace diff --git a/package.json b/package.json index 8a81892..1c380ce 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ] }, "dependencies": { + "@crazydos/vue-markdown": "^1.1.4", "@dnd-kit/core": "^6.1.0", "@element-plus/icons-vue": "^2.3.1", "@onlyoffice/document-editor-vue": "^1.5.0", @@ -73,6 +74,8 @@ "quill-image-resize-custom-module": "^4.1.7", "quill-image-uploader": "^1.3.0", "react-dnd-html5-backend": "^16.0.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "screenfull": "^6.0.0", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", diff --git a/src/api/doc/space.ts b/src/api/doc/space.ts index 2b4aad6..3dd52b7 100644 --- a/src/api/doc/space.ts +++ b/src/api/doc/space.ts @@ -1,6 +1,6 @@ import request from '@/utils/request'; 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 }); +} + +//-------------------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 + }); } \ No newline at end of file diff --git a/src/api/doc/type.ts b/src/api/doc/type.ts index a423475..1ae85eb 100644 --- a/src/api/doc/type.ts +++ b/src/api/doc/type.ts @@ -3,7 +3,8 @@ * @ 时间: 2025-05-12 15:39:13 * @ 备注: 文档管理api 结构定义 */ - +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; /** * 拉去首页文件列表 */ @@ -25,6 +26,7 @@ export interface matterInfo{ puuid?:string; // parent dir uuid path?:string; size?:number; + agent?:boolean; dir?:boolean; deleted?:boolean; expireInfinity?:boolean; @@ -73,4 +75,18 @@ export interface respCreateShare{ expireTime?:string; name?:string; uuid?:string; +} + +/** + * 文件上传 + */ +export function doFileUpload(params:FormData,_url:string): AxiosPromise { + return request({ + url: _url, + method: 'post', + data: params, + headers: { + 'Content-Type': 'multipart/form-data' + } + }); } \ No newline at end of file diff --git a/src/views/doc/agent.vue b/src/views/doc/agent.vue new file mode 100644 index 0000000..9a31a00 --- /dev/null +++ b/src/views/doc/agent.vue @@ -0,0 +1,325 @@ + + + + + + + \ No newline at end of file diff --git a/src/views/doc/manage.vue b/src/views/doc/manage.vue index 5ae4898..e703cfe 100644 --- a/src/views/doc/manage.vue +++ b/src/views/doc/manage.vue @@ -8,7 +8,7 @@ import { getExpirTime, getFileIcon, readableSize} from "./tools" import sharePermission from './sharePermission.vue'; import { useUserStore } from "@/store/modules/user"; 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 { Delete, @@ -22,8 +22,7 @@ import { Avatar, Plus } from '@element-plus/icons-vue' -import {ElSelect,ElOption, ElText,ElInput,TableInstance,ElMessage,UploadFile,UploadFiles,ElPagination,ElTree,TreeInstance} from "element-plus"; -import type { TreeNode } from 'element-plus/es/components/tree/src/tree.type' +import {ElSelect,ElOption, ElText,ElInput,TableInstance,ElMessage,UploadFile,UploadFiles,ElPagination,ElTree,TreeNode} from "element-plus"; import preview from './preview.vue'; import space from './space.vue'; @@ -52,17 +51,24 @@ const currentHoverRow=ref("") //table 行的按钮控制 const selectedValue = ref("sixhour") //分享弹窗的世间变量 const tabSelected=ref([]) //table组件多选数据维护 //to support tree mode refactor -const treeData=ref([{name:'个人空间',uuid:'root',children:[]}]) +const treeData=ref([])//{name:'个人空间',uuid:'root',children:[]} const treeRef = ref(); const currentNode=ref({}) //打开的路径层次 const officeHost=import.meta.env.VITE_OFFICE_HOST const dynamicVNode = ref(null) //permission 组件的父组件 + + const multipleTableRef = ref() const paginInfo = ref({ page: 0, total: 0 }) const PRIVATESPACE = ref(true) //是空间状态的控制 2种:私有云盘和共享空间 const SpaceID= ref<{name:string,uuid:string,userUuid:string}>({}) //当前space的id const SpaceList=ref<{name:string,uuid:string,userUuid:string}[]>([]) +const spaceEleRef = ref() //space组件的引用,它与spaceTreeRef没有父子关系,反而是为了处理spaceTree的操作而创建的该变量 +const spaceTreeData=ref([])//{name:'个人空间',uuid:'root',children:[]} +const spaceTreeRef = ref(); //space的树树组件的引用 +let spaceNodeUid="" //用来判断树组件的展开和关闭,如何只是展开和关闭的点击事件不在刷新,通currentNode的作用 + const Departs = computed(() => { return `${userStore.userInfoCont.company},${userStore.userInfoCont.department},${userStore.userInfoCont.organization}` }) @@ -277,9 +283,11 @@ function onLoadMatterList(){ getMatterList(uid,_page).then((resp)=>{ //page+1 是由于分页的起始index是1,而后端api的分页index起始是0 paginInfo.value={total:resp.data.totalPages,page:resp.data.page} - matterList.value=resp.data.data.filter((item)=>{ - return !item.dir - }) + matterList.value=resp.data.data + //2025-08-07: 保持space一样,文件夹一并显示 + // .filter((item)=>{ + // return !item.dir + // }) }) } //----------for dir----------- @@ -307,7 +315,7 @@ function onCreateDir(){ {uuid:resp.data.uuid,dir:false,name:resp.data.name,puuid:resp.data.puuid}, currentNode.value.uuid ) - onLoadMatterList() + //onLoadMatterList() }) .catch((e)=>{ ElMessage.error(e.msg) @@ -331,14 +339,16 @@ function onNodeExpand(node: TreeNode, resolve: (data: matterTree[]) => void, rej getMatterList(uid, _page).then((resp) => { paginInfo.value = { total: resp.data.totalPages, page: resp.data.page } - matterList.value = resp.data.data.filter((item)=>{ - return !item.dir - }) + matterList.value = resp.data.data + // .filter((item)=>{ + // return !item.dir + // }) let node_data = resp.data.data.filter((item) => { return item.dir - }).map((item) => { - item.dir = !item.dir - return item + }).map((val) => { + const copy = structuredClone(val) + copy.dir = !copy.dir + return copy }) resolve(node_data) @@ -353,6 +363,8 @@ function onNodeClick(data:matterTree,node:TreeNode,self:any,env:any){ if (currentNode.value.uuid === data.uuid) return; const cuuid = data.uuid currentNode.value = data + onLoadMatterList() + return let _page: matterPage = { page: 0, pageSize: 50, @@ -388,6 +400,10 @@ function onNodeClick(data:matterTree,node:TreeNode,self:any,env:any){ //文件预览 function onPrivateView(row:matterInfo){ + if(row.dir){ + alert("目录不支持预览") + return + } let a = `${row.uuid}${row.name}` let _token=document.cookie.match(/hxpan=([\w-]*)/) 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 { + uuid=resp.data.uuid //第一级别的uuid, 是第二级的puuid + list.push(resp.data) + }) + } + return list +} + function handleMouseEnter(row:any){ currentHoverRow.value=row.name } @@ -410,6 +516,7 @@ function handleSingleUpload(response:any){ fileList.value=[] onLoadMatterList() } + interface uploadError{ msg:string } @@ -417,6 +524,9 @@ interface uploadError{ function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){ ElMessage.error(JSON.parse(error.message).msg) } +//----------------------------------------------------------- + + //-------------------space feature--------------------- function onNewSpace(){ 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 //渲染完页面再执行 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)=>{ - 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" :data="treeData" node-key="uuid" + highlight-current lazy :props="{label: 'name',children:'children',isLeaf:'dir'}" :load="onNodeExpand" @@ -473,7 +624,18 @@ const handleSelectionChange = (val:matterInfo[]) => { @node-click="onNodeClick" /> 共享空间 -
    + +
    • {{ sp.name }}
    @@ -483,35 +645,39 @@ const handleSelectionChange = (val:matterInfo[]) => { 根目录/ - {{ currentNode.name }} + {{ currentNode.name }} + + + + + - - 上传 - - +
    + + 文件上传 +
    + +
    + + 文件夹上传 +
    新建目录 分享 删除 - 刷新 分享目录 删除目录
    - - - - -
    @@ -541,7 +707,7 @@ const handleSelectionChange = (val:matterInfo[]) => {