数通互联化工云平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
18 KiB

<!--
@ 作者: han2015
@ 时间: 2025-05-12 15:39:13
@ 备注: 共享空间组件
-->
<script lang="ts" setup>
import { getFileIcon, readableSize,fileType} from "./tools"
import sharePermission from './sharePermission.vue';
import { matterPage,matterInfo,matterTree,doFileUpload} from "@/api/doc/type"
4 months ago
import { doAccessManage,getSpaceMatterList,doCreateSpaceDir,doDelSpaceMatter,doDelSpace,
doAiTraining ,doCreateAiagent} from "@/api/doc/space"
import { h } from 'vue'
import {
Delete,
View,
Download,
Plus,
Edit,
Folder,
} from '@element-plus/icons-vue'
import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus";
4 months ago
import aiagent from './agent.vue';
import router from "@/router";
const matterList = ref<matterInfo[]>([])
const newdir=ref("") //创建新目录时的目录名
const currentHoverRow=ref("") //table 行的按钮控制
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) //面包屑导航
const tabSelected=ref<matterInfo[]>([]) //table组件多选数据维护
const currentNode=ref<matterInfo>({}) //打开的路径层次
let isNewNode=true //用来减少父组件树的重置
const dynamicVNode = ref<VNode | null>(null) //permission 组件的父组件
const paginInfo = ref({ page: 0, total: 0 })
4 months ago
//-----------AI---------------------
//const agent=ref<{model:boolean,name:string}>({})
const currentAgent=ref<{model:boolean,name:string,uuid:string}>({})
//---------------------------------
const props = withDefaults(defineProps<{
4 months ago
uid:string, //当前用户的uuid,注意已经且必须通过base64编码,因为后端获取Identifier后,会base64解码
tree:object,
spaceid:string,
spacename:string,
officeHost:string,
siteHost:string,
owner:string,
apiURL:string,
roles:string,
flushSpaceTree:(uuid:string,data:matterTree[])=>void
}>(),{})
//属性变更,特别是为了处理空间切换的状态更新
watch(props,()=>{
currentNode.value.uuid="root"
onLoadMatterList()
})
const uploadFormData = computed(() => {
return {
space: props.spaceid,
puuid: currentNode.value.uuid, // 父目录的uuid,基目录为root
}
});
//--------------权限控制&添加空间成员-------------
function onAccessManage(){
dynamicVNode.value = h(sharePermission, {
uid: props.uid,
uuid: "",
spaceid:props.spaceid, //
confirmFunc: (_list: string[],_infos:string[]) => {
// 组织权限数据
//_len=_list.length
let permited = btoa(_list.join("|"))
doAccessManage(props.uid,{
"space":props.spaceid,
"roles":permited,
"owner":props.owner,
"len":_list.length
}).then(()=>{
})
},
closeFunc: () => {
dynamicVNode.value=null
}
})
}
//删除空间
function onDeleteSpace(){
ElMessageBox.confirm(`确认删除空间 ( ${props.spacename}) ? 空间内所有文件将不可恢复!取消则放弃删除操作。`, "警告", {
confirmButtonText: "确定删除",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
doDelSpace(props.uid,{
"space":props.spaceid,
}).then(()=>{
router.replace({ query: { t: Date.now() } })
})
})
}
//----------------------------------------
//删除
function onDelMatter(row:matterInfo){
if (row.uuid){
ElMessageBox.confirm(`确认删除( ${row.name}) ?删除后不可恢复!取消则放弃删除操作。`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
doDelSpaceMatter(props.uid,{
"uuid":row.uuid,
"space":props.spaceid,
"roles":props.roles,
}).then(()=>{
currentNode.value.uuid = row.puuid ?? ""
currentNode.value.name = row.path ? row.path.replace(`/${row.name}`,'').match(/[^/]+$/g)?.pop() :"上级目录"
onLoadMatterList()
})
})
}
}
function onDownload(row:matterInfo){
ElMessageBox.confirm("确认下载此数据项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
if (row.uuid){
let _url= props.apiURL+`/space/download/${row.uuid}/${row.name}?space=${props.spaceid}`
window.open(_url)
}
})
}
//加载目录文件列表
function onLoadMatterList(){
let _page: matterPage = {
page: paginInfo.value.page,
pageSize: 50,
orderCreateTime: "DESC",
orderDir: "DESC",
puuid:currentNode.value.uuid,
deleted:false,
space:props.spaceid,
roles:props.roles,
};
getSpaceMatterList(props.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
//展开的时候暂时不做目录更新,看以后的使用情况
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) {
//由于支持目录树的原因,所有的空间根目录uuid都是root,这样树组件就有问题,所以用了spaceid作为其uuid
//但是云盘的根目录是root,所以当uuid是spaceid时,自动替换为root
if(currentNode.value.uuid=="root") props.flushSpaceTree(props.spaceid,node_data);
else props.flushSpaceTree(currentNode.value.uuid,node_data);
}
})
}
//----------for dir-----------
//该函数仅操作前端,为新文件夹命名
function createDir(){
matterList.value?.unshift({
name:"",
userUuid:props.spaceid,
puuid:"",
uuid:"",
dir:true,
size:0,
deleted:false,
})
}
//该函数为实际创建文件夹的函数
function onCreateDir(){
doCreateSpaceDir(props.uid,{
puuid:currentNode.value.uuid,
name:newdir.value,
space:props.spaceid,
roles:props.roles,
}).then((resp)=> {
newdir.value=""
onLoadMatterList()
})
.catch((e)=>{
ElMessage.error(e.msg)
})
}
//------------------------------------------
// @cell-dblclick="handleDoubleClick" 取消了目录双击打开功能
//打开一个目录
function handleDoubleClick(row:matterInfo,ind?:number){
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的双击事件也在此方法处理
if(typeof(ind)==="number"){
4 months ago
//返回某一级,面包屑组件的点击处理
if(breadcrumbList.value.length>1){
breadcrumbList.value=breadcrumbList.value.slice(0,ind+1)
currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1]
onLoadMatterList()
}
}else{
//进入下一级
//如果目录是一个智能体,把当前智能体设置为该目录
4 months ago
if(row.agent){
currentAgent.value={name:row.name,model:false,uuid:row.uuid}
}
currentNode.value=row
breadcrumbList.value.push(row)
onLoadMatterList()
}
*/
}
}
function handleMouseEnter(row:any){
currentHoverRow.value=row.name
}
//上传成功
function handleSingleUpload(response:any){
4 months ago
handleAiUpload(response.data)
onLoadMatterList()
}
interface uploadError{
msg:string
}
//上传失败
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
ElMessage.error(JSON.parse(error.message).msg)
}
4 months ago
//自定义上传,目的是支持多文件上传, 使用原生组件,是为了解决并发问题
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)
4 months ago
const res = await doFileUpload(fields,'/hxpan/api/space/upload')
if(res.code!=200){
console.log(ff.name+"上传失败! ")
alert(ff.name+"上传失败! ")
4 months ago
}
//上传后继续AI服务训练
handleAiUpload(res.data)
4 months ago
}
//文件夹上传,原理:自定义的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 +" 上传失败! ")
}
//上传后继续AI服务训练,是不是需要上传,在handleAiUpload里面有检测
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
}
4 months ago
//-------------------------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){
4 months ago
doAiTraining(`/agents/${currentAgent.value.uuid}/updates`,{"matter":info.uuid}).then(resp=>{
console.log(resp)
})
}
}
//-------------------------------------------------
//-------------------edit & preive file for space---------------------
//文件预览
function onPrivateView(row:matterInfo){
const _type=fileType(row.name!)
if(_type!==""){ //office file
const info =btoa(encodeURIComponent(`${row.name}`)) //预览模式不需要绝对路径,只核对一下文件名即可
const _url=`${props.siteHost}${props.apiURL}/space/download/${row.uuid}/${row.name}?space=${props.spaceid}`
//前半部分内容是为了校验信息,主要内容是fileurl
window.open(`/#/onlyoffice?name=${row.name}&dtype=${_type}&info=${info}&fileurl=`+encodeURIComponent(_url),"_blank")
}else{
alert("暂不支持该类型预览")
}
}
//onlyoffice在线编辑
async function onlyOfficeEdit(row:matterInfo){
const _type=fileType(row.name!)
if(_type===""){
alert("暂不支持该类型编辑")
return
}
ElMessageBox.confirm("线上资源有限,确定继续线上编辑吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
//office file
//base64 encode for MASK
const _verify = btoa(row.uuid.match(/(\w+-\w+)/)![0]+"true") //增加一个权限验证的标记
const info =btoa(encodeURIComponent(`${row.userUuid}/root${row.path}`)) //编辑模式必须要全路径
const _url=`${props.siteHost}${props.apiURL}/space/download/${row.uuid}/${row.name}?space=${props.spaceid}`
window.open(`/#/onlyoffice?name=${row.name}&dtype=${_type}&info=${info}&verify=${_verify}&fileurl=`+encodeURIComponent(_url),"_blank")
})
}
//------------------------------------------------------
//渲染完页面再执行
onMounted(() => {
currentNode.value.uuid="root"
4 months ago
//设置默认的AI智能体
currentAgent.value={name:"通用AI",model:false,uuid:"5bd9b0e9-d3f4-4089-670a-880009e925a8"}
onLoadMatterList()
});
const handleSelectionChange = (val:matterInfo[]) => {
tabSelected.value = val
}
defineExpose({handleDoubleClick})
//判断是不是空间的所有者
function isOwner(){
return props.uid===btoa(props.owner)
}
</script>
<template>
<div>
<el-row :gutter="24" style="margin: 12px 0px;">
<span class="el-breadcrumb" style="font-weight: bold;">[ {{ props.spacename }} ] : </span>
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item,index) in breadcrumbList"
:key="index" @click="handleDoubleClick(item,index)">
<span style="font-weight: bold;">{{ item.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</el-row>
<el-row :gutter="24">
<el-col :span="14">
<div class="el-button el-button--default" style="position: relative;">
<input type="file" style="position: absolute;opacity: 0;width: 50px;"
@change="onCustomUpload" multiple />
文件上传
</div>
<div class="el-button el-button--default" style="position: relative;">
<input type="file" style="position: absolute;opacity: 0;width: 50px;"
@change="uploadFolder"
webkitdirectory
multiple
/>
文件夹上传
</div>
<el-button @click="createDir">新建目录</el-button>
<el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true,
puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button>
</el-col>
4 months ago
<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>
4 months ago
<el-button :icon="Plus" @click="onAiAgent">创建智能体</el-button>
<el-button :icon="Delete" @click="onDeleteSpace">删除</el-button>
</el-button-group>
</el-row>
<el-row :gutter="24" style="height: 84%;overflow-y: auto;">
<el-table
stripe
:data="matterList"
:header-cell-style="{ background: '#f5f8fd' }"
style="width: 100%"
row-key="uuid"
:row-style ="() => ({ lineHeight: '36px' })"
@selection-change="handleSelectionChange"
@cell-mouse-enter="handleMouseEnter">
<el-table-column type="selection" width="50" />
<el-table-column width="450" property="name" label="文件名">
<template #default="scope">
<input type="text" autofocus placeholder="文件夹名" style="border:groove;height:30px;" v-model="newdir" @change="onCreateDir" v-if="scope.row.name===''" />
<div v-if="scope.row.name" style="display: flex; align-items: center;">
<el-icon :size="26">
<component v-if="scope.row.dir" :is="Folder" />
<component v-else :is="getFileIcon(scope.row.name)" />
</el-icon>
<span style="margin-left: 10px">{{ scope.row.name }}</span>
</div>
</template>
</el-table-column>
4 months ago
<el-table-column width="250">
<template #default="scope" >
<el-tag v-if="scope.row.agent" effect="dark" size="small" type="success" round >智能体</el-tag>
<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="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="Delete" circle @click="onDelMatter(scope.row)"></el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="size" width="100" :formatter="readableSize" label="大小" />
<el-table-column prop="updateTime" label="修改日期">
<template #default="scope">
<span v-if="scope.row.updateTime">{{ scope.row.updateTime.slice(0,16) }}</span>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row v-if="paginInfo.total>1" style="justify-content: center;">
<el-pagination size="small" background layout="prev, pager, next" :current-page="paginInfo.page+1" @current-change="(value:number)=>{paginInfo.page=value-1;onLoadMatterList();}" :page-count="paginInfo.total" class="mt-4"/>
</el-row>
</div>
4 months ago
<aiagent :agent="currentAgent" :userid="uid" :uuid="currentAgent" :closefunc="()=>{currentAgent.model=false}"></aiagent>
<div v-if="dynamicVNode">
<component :is="dynamicVNode" />
</div>
</template>
<style lang="scss" scoped>
.shareDialog{
--el-messagebox-width:'800px';
padding:40px;
.el-text{
align-self: flex-start;
}
}
.dynamic-width-message-box-byme .el-message-box__message{
width: 100%;
}
</style>