Compare commits

...

5 Commits

  1. 1434
      package-lock.json
  2. 3
      package.json
  3. 25
      src/api/doc/space.ts
  4. 10
      src/utils/router/index.ts
  5. 95
      src/views/common/bottom/FloatingBall.vue
  6. 14
      src/views/common/bottom/index.vue
  7. 301
      src/views/doc/agent.vue
  8. 15
      src/views/doc/index.vue
  9. 20
      src/views/doc/manage.vue
  10. 13
      src/views/doc/popup.vue
  11. 376
      src/views/doc/share.vue
  12. 55
      src/views/doc/space.vue
  13. 19
      src/views/doc/spacePermission.vue

1434
package-lock.json

File diff suppressed because it is too large

3
package.json

@ -41,6 +41,7 @@
]
},
"dependencies": {
"@crazydos/vue-markdown": "^1.1.4",
"@element-plus/icons-vue": "^2.3.1",
"@nutui/nutui": "^4.0.0",
"@onlyoffice/document-editor-vue": "^1.6.1",
@ -63,6 +64,8 @@
"pinia": "^2.2.4",
"pinia-plugin-persistedstate": "^4.1.1",
"quill": "^2.0.3",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"sass": "^1.80.3",
"scss": "^0.2.4",
"spark-md5": "^3.0.2",

25
src/api/doc/space.ts

@ -211,6 +211,31 @@ export function setAiChat(data: any){
});
}
/**
*
* @requires key
*
*/
export function getShareChat(key: string){
return request({
url: '/aibot/chat/shared/'+key,
method: 'get',
});
}
/**
*
* @requires key agentID+chatID+index
* @requires data
*/
export function newShareChat(data: any){
return request({
url: '/aibot/chat/share',
method: 'post',
data: data
});
}
/**
*
*/

10
src/utils/router/index.ts

@ -91,6 +91,16 @@ export const staticRouting : RouteRecordRaw[] = [
component: () => import('@/views/doc/onlyoffice.vue'),
meta: { hidden: true },
},
{
path: '/docshare',
component: () => import('@/views/doc/share.vue'),
meta: { hidden: true },
},
{
path: '/agent',
component: () => import('@/views/doc/agent.vue'),
meta: { hidden: true },
},
{
path: '/spaces/:spaceid',
name: 'spaces',

95
src/views/common/bottom/FloatingBall.vue

@ -0,0 +1,95 @@
<!-- FloatingBall.vue -->
<script setup>
import { ref, computed } from 'vue'
const emit = defineEmits(['click'])
const x = ref(window.innerWidth - 60)
const y = ref(window.innerHeight - 250)
const isDragging = ref(false)
const ballStyle = computed(() => ({
position: 'fixed',
left: `${x.value}px`,
top: `${y.value}px`,
zIndex: 99,
userSelect: 'none',
cursor: isDragging.value ? 'grabbing' : 'grab'
}))
const getPoint = e => (e.touches ? e.touches[0] : e)
let startX = 0, startY = 0, initX = 0, initY = 0
const MOVE_THRESHOLD = 5
function startDrag(e) {
const point = getPoint(e)
startX = point.clientX
startY = point.clientY
initX = x.value
initY = y.value
isDragging.value = false
window.addEventListener('mousemove', onMove, { passive: true })
window.addEventListener('mouseup', onUp)
window.addEventListener('touchmove', onMove, { passive: true })
window.addEventListener('touchend', onUp)
e.preventDefault() //
}
function onMove(e) {
const p = getPoint(e)
const dx = p.clientX - startX
const dy = p.clientY - startY
if (!isDragging.value && (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)) {
isDragging.value = true
}
if (isDragging.value) {
x.value = Math.max(0, Math.min(window.innerWidth - 50, initX + dx))
y.value = Math.max(0, Math.min(window.innerHeight - 50, initY + dy))
}
}
function onUp() {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
window.removeEventListener('touchmove', onMove)
window.removeEventListener('touchend', onUp)
if (!isDragging.value) {
emit('click')
}
isDragging.value = false
}
</script>
<template>
<div
class="float-ball"
:style="ballStyle"
@mousedown="startDrag"
@touchstart="startDrag"
@click.stop
>
<slot>AI</slot>
</div>
</template>
<style scoped>
.float-ball {
width: 50px;
height: 50px;
border-radius: 50%;
background: #409eff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
</style>

14
src/views/common/bottom/index.vue

@ -7,6 +7,9 @@
import { useRoute,useRouter } from 'vue-router'
import SvgIcon from '@/components/svgIcon/index.vue'
import FloatingBall from './FloatingBall.vue'
const goTop = () => window.scrollTo({ top: 0, behavior: 'smooth' })
const route = useRoute()
const router = useRouter()
const footerActive = ref<number>(route.query.fa?route.query.fa:1)
@ -36,6 +39,7 @@ const openPage = (val:number) => {
router.push({ path: "/"});
}
}
</script>
<template>
<div class="contentBetween">
@ -59,9 +63,19 @@ const openPage = (val:number) => {
<div class="footSvg"><SvgIcon icon-class="user" :size="20" /></div>
<div>我的</div>
</div>
<FloatingBall @click="router.push({ path: '/agent',query:{fa:8}});">AI</FloatingBall>
</div>
</template>
<style lang='scss' scoped>
.fixBtn{
x: 100px;
y:300px;
width: 50px;
height: 50px;
position: fixed;
}
.footerBox{
width: 20%;
text-align: center;

301
src/views/doc/agent.vue

@ -6,34 +6,40 @@
<script lang="ts" setup>
import {
Promotion,
Remove
Remove,
ArrowLeft,
} 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 { userStror } from "@/utils/pinia/stores/modules/userOrders";
import router from "@/utils/router";
import { doAiChat,getAiChatList,getAiChat,setAiChat,delAiChat,getAiagentList,getShareChat,newShareChat} from "@/api/doc/space"
import {ElText,ElInput} from "element-plus";
import { VueMarkdown } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import { h } from 'vue'
import { useRoute } from 'vue-router'
import BottomPage from '@/views/common/bottom/index.vue'
const userStore = userStror();
const userid=ref("")
const route = useRoute()
//
const checkedModel = ref([])
//
const aimodels = [{name:'联网检索',key:"onlineSearch"}, {name:'公司知识库',key:"useDataset"}]
const baseURL=import.meta.env.VITE_APP_BASE_API
const siteHost=document.location.origin;
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,docinfo?:any[]}[]>([])
const props = withDefaults(defineProps<{
userid:string,
closefunc:()=>void,
agent:{model:boolean,name:string,uuid:string}
}>(),{})
const interact_msg=ref<{ask:boolean,think:string,content:string,docinfo?:any[],share:boolean}[]>([])
const agent=ref<{name:string,uuid:string}>({name:"通用AI",uuid:import.meta.env.VITE_DEFAULT_AI_AGENT})
const agentList=ref<{name:string,uuid:string}[]>([{name:"通用AI",uuid:import.meta.env.VITE_DEFAULT_AI_AGENT}])
const respMsg=ref("")
const drawerModel=ref(true)
//
interface message{
@ -78,12 +84,12 @@ async function onSendTextToAI(){
let docinfo:any=[]
controller.value = new AbortController();
try{
const res= await doAiChat(`${baseURL}/aibot/agents/${props.agent.uuid}/chat`,{
const res= await doAiChat(`${baseURL}/aibot/agents/${agent.value.uuid}/chat`,{
inputs: params,
query:myquestion.value,
response_mode:"streaming",
conversation_id:conversation.value,
user:atob(props.userid),//base64
user:userid.value,//base64
},controller.value.signal
)
@ -137,8 +143,9 @@ async function onSendTextToAI(){
respMsg.value=""
setAiChat({
"userid":atob(props.userid),
"userid":userid.value,
"uuid":conversation.value,
"agentuuid":agent.value.uuid,
"brief":interact_msg.value[0].content,
"content":JSON.stringify(interact_msg.value)
})
@ -147,45 +154,57 @@ async function onSendTextToAI(){
//
function loadKnownLibList(){
//userid base64
getAiChatList({"userid":atob(props.userid)}).then(resp=>{
getAiChatList({"userid":userid.value}).then(resp=>{
conversations.value=resp.data
})
}
//
function showChat(uuid:string){
drawerModel.value=false;
getAiChat({
"userid":atob(props.userid),
"userid":userid.value,
"uuid":uuid
}).then(resp=>{
interact_msg.value = JSON.parse(resp.data.content)
conversation.value=resp.data.uuid
if(resp.data.agentuuid!=""){
agent.value={name:"会话",uuid:resp.data.agentuuid}
}
})
}
//chat
function showSharedChat(uuid:string){
drawerModel.value=false;
getShareChat(uuid).then(resp=>{
let data=JSON.parse(resp.data.content)
data.share=true
interact_msg.value = data
// if(resp.data.agentuuid!=""){
// agent.value={name:"",uuid:resp.data.agentuuid}
// }
}).catch(err=>{
alert(err)
})
}
//
function onDelChat(uuid:string){
delAiChat({
"userid":atob(props.userid),
"userid":userid.value,
"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(){
drawerModel.value=false;
const c =conversations.value.find(c=>c.uuid==conversation.value)
if(!c){
if(!c && interact_msg.value.length>0){
conversations.value.push({
"agentuuid":props.agent.uuid,
"agentuuid":agent.value.uuid,
"uuid":conversation.value,
"brief":interact_msg.value[0].content,
"messages":interact_msg.value
@ -201,109 +220,179 @@ function newContext(){
function resetContext(){
interact_msg.value=[]
conversation.value=""
props.closefunc()
}
//ai
function formatRefContent(content:string){
let result=content.replaceAll(/"/g,'')
result=result.replaceAll(/Unnamed: /g,' ')
let result=content.replace(/"/g,'')
result=result.replace(/Unnamed: /g,' ')
return result
}
//
function onShareChat(agID:string,chatID:string,idx:number){
const arr=<{ask:boolean,content:string}[]>([])
//
arr.push({
ask:interact_msg.value[idx-1].ask,
content:interact_msg.value[idx-1].content
})
arr.push({
ask:interact_msg.value[idx].ask,
content:interact_msg.value[idx].content
})
newShareChat({
key:agID+chatID+idx.toString(),
data:JSON.stringify(arr)
}).then(resp=>{
let _shareURL=`${siteHost}/#/agent?shared=${resp.key}`
ElMessageBox({
title: '分享链接',
customStyle: { padding:'20px'},
message: () => h('div',{style:{display:'flex','flex-direction':'column','line-height':'34px'}},[
h(ElText,{style:{'align-self':'flex-start','font-weight': 'bold'}},()=>"链接:"+_shareURL),
]),
confirmButtonText: '复制分享链接',
showCancelButton: true
}).then(()=>{
if(!navigator.clipboard) alert("clipboard 不可用")
navigator.clipboard.writeText(_shareURL)
})
}).catch(err=>{
console.log(err)
})
}
//
onMounted(() => {
if(userStore.userInfoCont == ""){
userStore.getInfo().then(()=>{
userid.value="p0"+userStore.userInfoCont.userId;
})
}else{
userid.value="p0"+userStore.userInfoCont.userId;
}
getAiagentList().then(resp=>{
agentList.value.push(...resp.data)
})
loadKnownLibList()
const query = route.query
//
if (query.shared && query.shared!=""){
showSharedChat(query.shared as string)
}
});
</script>
<template>
<el-drawer
:model-value="agent.model"
:model-value="drawerModel"
: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)">
<span>{{ item.brief }}</span>
<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 v-if="msg.docinfo?.length>0" class="doc_ref">
引用<hr>
<el-tooltip v-for="doc in msg.docinfo" placement="top" effect="dark">
<template #content>
<div v-html="formatRefContent(doc.content)" />
</template>
<span>{{doc.document_name}}</span>
</el-tooltip>
</div>
</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: 56px;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>
direction="ltr"
size="86%"
@close="drawerModel=false;"
>
<div style="overflow-y: auto;padding: 8px;">
<ul>
<li class="action_menu" @click="newContext">
新建会话
</li>
<li class="agent-item" v-for="ag in agentList" @click="agent=ag;newContext()">
<el-icon><Star /></el-icon> <span>{{ ag.name }}</span>
</li>
<span><el-icon><Clock /></el-icon> </span>
<li class="list_item" v-for="item in conversations" @click="showChat(item.uuid)">
<span>{{ item.brief }}</span>
<el-button icon="Delete" size="small" circle @click="(e)=>{e.stopPropagation();onDelChat(item.uuid)}"></el-button>
</li>
</ul>
</div>
</el-drawer>
<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 class="navBtn">
<el-button type="text" :icon="ArrowLeft" @click="router.back()"> 返回</el-button>
<el-button icon="expand" size="small" @click="drawerModel=true;"></el-button>
</div>
<div class="app_container">
<div class="reply_area" >
<template v-for="msg,index 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 v-if="conversation" class="actions">
<el-button :icon="Promotion" size="small" circle @click="onShareChat(agent.uuid,conversation,index)"></el-button> 分享
</div>
<div v-if="msg.docinfo?.length>0" class="doc_ref">
引用<hr>
<el-tooltip v-for="doc in msg.docinfo" placement="top" effect="dark">
<template #content>
<div v-html="formatRefContent(doc.content)" />
</template>
<span>{{doc.document_name}}</span>
</el-tooltip>
</div>
</div>
</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: 32px;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-drawer>
<el-input placeholder="问灵犀..." v-model="myquestion" input-style="border-radius: 18px;"
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>
<BottomPage />
</template>
<style lang="scss" scoped>
.app_container {
padding: 10px 30px 0px 30px;
height: 100%;
height: calc(100vh - 65px);
padding: 10px;
width: 100%;
overflow-y:auto;
background-color: white;
}
.navBtn{
position: fixed;
background-color: #ffffff94;
width: 100%;
z-index: 55;
}
.newquestion{
bottom: 20px;
bottom: 62px;
}
.question_com{
position: fixed;
width: 52%;
margin: 0 50px;
padding: 0 13px;
text-align: center;
display: block;
button{
position: absolute;
bottom: 25px;
right: 5px;
bottom: 27px;
right: 20px;
}
}
.reply_area{
display: flex;
min-height: 20%;
flex-direction: column;
margin: 15px 15px 110px 0px;
margin: 15px 15px 210px 15px;
}
.t_ask{
align-self: end;
@ -311,15 +400,20 @@ onMounted(() => {
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: 16px;
line-height: 23px;
font-size: 13px;
color: black;
}
.actions{
display: flex;
width: 100%;
margin: 5px 0;
background-color: #f3f3f3;
justify-content: center;
}
.dynamic-width-message-box-byme .el-message-box__message{
width: 100%;
}
@ -330,11 +424,16 @@ onMounted(() => {
border-radius: 8px;
border: 1px solid #c9c6c6;
}
.agent-item{
padding: 4px 8px;
margin: 3px 14px 3px 0;
border-radius: 8px;
}
.list_item{
display: flex;
background-color: #dddddd;
padding: 10px 8px;
margin: 3px 14px 3px 0;
padding: 8px;
margin: 3px 14px 3px 10px;
border-radius: 8px;
span{
width: 90%;
@ -347,14 +446,14 @@ onMounted(() => {
}
}
.doc_ref{
margin: 16px;
margin: 5px 0 20px 0px;
display: flex;
flex-wrap: wrap;
hr{
width: 90%;
margin: inherit;
width: 70%;
margin: 11px;
border: none;
border-top: .5px solid rgb(26 25 25 / 23%);
border-top: 0px solid rgb(26 25 25 / 23%);
}
span{
width: 300px;

15
src/views/doc/index.vue

@ -29,7 +29,7 @@ const Departs = computed(() => {
const dynamicVNode = ref<VNode | null>(null) //permission
const CutLevelPermit=ref(0)
function onSelectSpace(data:matterInfo,recycling?:boolean){
function onSelectSpace(data:matterInfo,recycling?:number){
if(data){
router.push({
name: "spaces",
@ -39,10 +39,14 @@ function onSelectSpace(data:matterInfo,recycling?:boolean){
}
});
}else{
if(recycling) {
if(recycling==3) {
router.push({ path: "/mysapce",query:{fa:7,recycling:recycling}});
return
}
if(recycling==2) {
router.push({ path: "/docshare",query:{fa:7}});
return
}
router.push({ path: "/mysapce",query:{fa:7}});
}
}
@ -201,7 +205,12 @@ onMounted(()=>{
<span>我的空间</span>
</div>
</li>
<li @click="onSelectSpace(null,true)">
<li @click="onSelectSpace(null,2)">
<div style="display: flex;align-items: center;"><svg-icon icon-class="folder-icon" :size="30" />
<span>我的分享</span>
</div>
</li>
<li @click="onSelectSpace(null,3)">
<div style="display: flex;align-items: center;"><svg-icon icon-class="folder-icon" :size="30" /> <span>回收站</span>
</div>
</li>

20
src/views/doc/manage.vue

@ -71,7 +71,7 @@ function onShareMatter(row?:matterInfo){
let _len=0
ElMessageBox({
title: row?.name+' 请选择分享有效时间',
message: () => h('div',{style:{ width:'660px'}},[
message: () => h('div',{style:{ width:'100%'}},[
h(ElSelect,
{
defaultFirstOption:true,
@ -81,7 +81,7 @@ function onShareMatter(row?:matterInfo){
},
valueKey: "value",
fallbackPlacements:['bottom-start'],
style: { width:'360px' }
style: { width:'280px' }
},() => [
h(ElOption, { label: '六小时',key: 'sixhour', value: 'sixhour' }),
h(ElOption, { label: '一 天', key: 'oneday', value: 'oneday' }),
@ -146,11 +146,11 @@ function onShareMatter(row?:matterInfo){
}
function showShareMessage(row:respCreateShare){
let _shareURL=`${siteHost}/#/doc/share/?uuid=${row.uuid}&code=${row.code}`
let _shareURL=`${siteHost}/#/docshare/?uuid=${row.uuid}&code=${row.code}`
ElMessageBox({
title: '分享详情',
customStyle: { '--el-messagebox-width':'800px',padding:'40px'},
message: () => h('div',{style:{display:'flex','flex-direction':'column','line-height':'34px', width:'600px'}},[
customStyle: { padding:'20px'},
message: () => h('div',{style:{display:'flex','flex-direction':'column','line-height':'34px'}},[
h(ElText,{style:{'align-self':'flex-start'}},()=>row.name),
h(ElText,{style:{'align-self':'flex-start'}},()=>"失效时间:"+row.expireTime),
h(ElText,{style:{'align-self':'flex-start'}},()=>"链接:"+_shareURL)
@ -493,14 +493,14 @@ onMounted(() => {
<el-row v-if="!modRecycling" :gutter="24">
<el-col>
<div class="el-button el-button--default" style="position: relative;">
<div class="el-button el-button--default" style="position: relative;height: 24px;">
<el-icon><Upload /></el-icon>
<input type="file" style="position: absolute;opacity: 0;width: 50px;"
@change="onCustomUpload" multiple />
上传
</div>
<el-button icon="plus" @click="createDir">新建</el-button>
<el-button size="small" icon="plus" @click="createDir">新建</el-button>
<span v-if="tabSelected.length>1" style="margin:12px">
<el-button @click="onShareMatter()">分享</el-button>
<el-button @click="onDelMatBatch">删除</el-button>
@ -593,7 +593,7 @@ onMounted(() => {
<span class="blocker" @click="onPrivateView(currentHoverRow)">
<View class="plus-icon"></View>预览</span>
<span class="blocker" @click="onShareMatter(currentHoverRow)">
<View class="plus-icon"></View>分享</span>
<Share class="plus-icon"></Share>分享</span>
<span class="blocker" @click="onDownload(currentHoverRow)">
<Download class="plus-icon"></Download>下载</span>
<span class="blocker" @click="onDelMatter(currentHoverRow)">
@ -614,11 +614,11 @@ onMounted(() => {
.app_container {
padding: 10px 15px 0px 15px;
height: calc(100vh - 65px);
overflow: hidden;
overflow: auto;
position: relative;
}
.nav-header{
margin-top: 21px;
margin-top: 12px;
width: 100%;
display: flex;
span{

13
src/views/doc/popup.vue

@ -1,13 +0,0 @@
<template #default="scope">
<div v-show="currentHoverRow === scope.row.uuid">
<span v-if="scope.row.deleted">
<el-button type="text" @click="restoreMatter(scope.row)">恢复</el-button>
</span>
<span v-else>
<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="Download" circle @click="onDownload(scope.row)"></el-button>
<el-button size="small" :icon="Delete" circle @click="onDelMatter(scope.row)"></el-button>
</span>
</div>
</template>

376
src/views/doc/share.vue

@ -0,0 +1,376 @@
<!--
@ 作者: han2015
@ 时间: 2025-05-12 15:39:13
@ 备注: 文档管理组件
-->
<script lang="ts" setup>
import { getShareList,getShareBrowse,postShareDelete} from "@/api/doc/index"
import { matterPage,matterInfo,respCreateShare } from "@/api/doc/type"
import { h } from 'vue'
import router from "@/utils/router";
import { useRoute } from 'vue-router'
import { userStror } from "@/utils/pinia/stores/modules/userOrders";
import {
ArrowLeft,
Avatar,
} from '@element-plus/icons-vue'
import {ElText,ElButton } from "element-plus";
import {getFileIcon,checkExpirTime,fileType } from "./tools"
import sharePermission from './sharePermission.vue';
import preview from './preview.vue';
const userStore = userStror();
const uid=btoa("p0"+userStore.userInfoCont.userId);
const siteHost=document.location.origin;
const apiURL=import.meta.env.VITE_APP_BASE_API+"/hxpan/api"
const udprt=btoa("d"+userStore.userInfoCont.department);
const route = useRoute()
const officeHost=import.meta.env.VITE_OFFICE_HOST
const matterList = ref<matterInfo[]>() //
const browerMode=ref(false) //share 1)self-list(default) 2)brower
// const drawerModel=ref(false) //
// const permitListRef=ref("") //key
//------------------------
const showPopup=ref(false)
const currentHoverRow=ref<matterInfo>({}) //table
//----------------------------------
import type { VNode } from 'vue'
const dynamicVNode = ref<VNode | null>(null) //permission
function showShareMessage(row:{uuid:string,code:string,name:string,expireTime:string}){
let _shareURL=`${siteHost}/#/docshare?uuid=${row.uuid}&code=${row.code}`
ElMessageBox({
title: '分享详情',
customStyle: { padding:'20px'},
message: () => h('div',{style:{display:'flex','flex-direction':'column','line-height':'34px'}},[
h(ElText,{style:{'align-self':'flex-start'}},()=>row.name),
h(ElText,{style:{'align-self':'flex-start'}},()=>"失效时间:"+row.expireTime),
h(ElText,{style:{'align-self':'flex-start'}},()=>"链接:"+_shareURL),
h('div',[
h(ElButton, {
type: 'primary',
style: { width: '100px' },
onClick: () => {
let _url=apiURL+`/share/zip?shareUuid=${row.uuid}&code=${row.code}&dprt=${udprt}&puuid=root&rootUuid=root`
window.open(_url)
}
},()=>'下载'),
h(ElButton, {
type: 'primary',
style: { width: '100px',margin:'0 10px' },
onClick: () => {
onShareView(row)
}
},()=>'预览')
])
]),
confirmButtonText: '复制分享链接',
showCancelButton: true
}).then(()=>{
if(!navigator.clipboard) alert("clipboard 不可用")
navigator.clipboard.writeText(_shareURL)
}).catch(() => {
if (browerMode.value){
location.href=`/#/docshare`
}
});
}
//----------------------------------------
//
function onShareDelete(row:matterInfo){
if (row.uuid){
ElMessageBox.confirm("确认要取消此文件分享!", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
postShareDelete(uid,{
"uuid":row.uuid
}).then(()=>onLoadShareList())
})
}
}
//
function onShareMember(row:matterInfo){
dynamicVNode.value=h(sharePermission,{
uid:uid,
uuid:row.uuid,
closeFunc:(refresh?:boolean)=>{
dynamicVNode.value=null
if (refresh) {
permitListRef.value=""
drawerModel.value=false
onLoadShareList()
}
}
})
}
//
function onShareView(row:matterInfo){
const _type=fileType(row.name!)
if(_type!==""){ //office file
const info =btoa(encodeURIComponent(`${row.name}`)) //
const _url=`${siteHost}${apiURL}/share/zip?shareUuid=${row.uuid}&code=${row.code}&uid=${uid}&dprt=${udprt}&puuid=root&rootUuid=root`
//window.open(`/#/onlyoffice?name=${row.name}&dtype=${_type}&info=${info}&fileurl=`+window.btoa(encodeURIComponent(_url)),"_blank")
router.push({ path: "/onlyoffice",query:{fa:7,name:row.name,dtype:_type,info:info,fileurl: window.btoa(encodeURIComponent(_url)) }});
}else{
//by kkFilePreview
let a = row.name ?? '';
if(a.endsWith('...')){
a=`${row.uuid}-${row.code}.zip`
}
//
a=a.match("(\.[a-zA-Z]+)$")
if (a && a.length>0) {
a=`${row.uuid}-${row.code}${a[0]}`
}else{
a=`${row.uuid}${row.name}`
}
let _url=`${siteHost}${apiURL}/share/zip?shareUuid=${row.uuid}&code=${row.code}&uid=${uid}&dprt=${udprt}&puuid=root&rootUuid=root&fullfilename=${a}`
//window.open(`${officeHost}/kkpreview/onlinePreview?url=`+window.btoa(encodeURIComponent(_url)),"_blank")
dynamicVNode.value=h(preview,{
url:`${officeHost}/kkpreview/onlinePreview?url=`+window.btoa(unescape(encodeURIComponent(_url))),
closeFunc:()=>dynamicVNode.value=null
})
}
}
function onDownload(row:matterInfo){
ElMessageBox.confirm("确认下载此数据项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
if (row.uuid){
let _url=apiURL+`/share/zip?shareUuid=${row.uuid}&code=${row.code}&dprt=${udprt}&puuid=root&rootUuid=root`
window.open(_url)
}
})
}
//
function onLoadShareList(){
let _page: matterPage = {
page: 0,
pageSize: 100,
orderCreateTime: "DESC",
orderDir: "DESC",
}
getShareList(uid,_page).then((resp)=>{
matterList.value=resp.data.data
})
}
function handleMouseEnter(row:any){
currentHoverRow.value=row.uuid
}
function getItemSpan(str:string){
let span=parseInt(str.split(":")[0])-3 //3level34,5,6...
if(span<0) span=0
return `margin-left:${span*20}px`
}
onMounted(() => {
const query = route.query
//
if (query.uuid && query.code){
browerMode.value=true
getShareBrowse("",{shareUuid:query.uuid,code:query.code,puuid:'root',rootUuid:'root',dprt:udprt}).then((resp)=>{
showShareMessage(resp.data)
// const request = indexedDB.open('visitList')
// request.onsuccess = (e) => {
// const db = e.target.result;
// const store = db.transaction('vlist','readwrite').objectStore("vlist");
// store.put(resp.data)
// db.close()
// }
})
return
}
browerMode.value=false
onLoadShareList()
})
</script>
<template>
<div class="navBtn">
<el-button type="text" :icon="ArrowLeft" @click="router.back()">返回</el-button>
</div>
<div class="app_container">
<el-row :gutter="24" v-if="!browerMode">
<el-table
stripe
:data="matterList"
:header-cell-style="{ background: '#f5f8fd' }"
style="width: 100%"
row-key="uuid"
:row-style ="() => ({ height: '55px' })"
>
<el-table-column property="name" label="我的文件分享">
<template #default="scope">
<div style="display: flex; align-items: center">
<svg-icon v-if="scope.row.dir" icon-class="folder-icon" :size="26"/>
<svg-icon v-else :icon-class="getFileIcon(scope.row.name)+'-icon'" :size="26" />
<span style="margin-left: 10px">{{ checkExpirTime(scope.row)?scope.row.name+' (已过期)':scope.row.name }}</span>
</div>
</template>
</el-table-column>
<!-- <el-table-column width="250" align="center">
<template #default="scope">
<div v-show="currentHoverRow === scope.row.uuid">
<el-button size="small" :icon="Avatar" circle @click="()=>{drawerModel=true; permitListRef=scope.row.permitInfos}"></el-button>
<el-button size="small" :icon="View" circle @click="onShareView(scope.row)"></el-button>
<el-button size="small" :icon="Share" circle @click="showShareMessage(scope.row)"></el-button>
<el-button size="small" :icon="Delete" circle @click="onShareDelete(scope.row)"></el-button>
</div>
</template>
</el-table-column> -->
<el-table-column width="100" prop="expireTime" label="失效日期">
<template #default="scope">
<span v-if="scope.row.expireTime">{{ scope.row.expireInfinity? '永久': scope.row.expireTime.slice(5,10) }}</span>
<el-button class="setBtn" type="text" icon="MoreFilled" size="small"
@click="(e)=>{e.stopPropagation(); showPopup=true; currentHoverRow=scope.row;}"></el-button>
</template>
</el-table-column>
</el-table>
</el-row>
</div>
<div v-if="showPopup" class="mask" @click="showPopup = false"></div>
<!-- 主体 -->
<Transition name="popuper">
<div v-if="showPopup" class="bs-wrapper">
<div class="popupTitle">
<svg-icon v-if="currentHoverRow.dir" icon-class="folder-icon" size="30px"/>
<svg-icon v-else :icon-class="getFileIcon(currentHoverRow.name)+'-icon'" size="30px"/>
{{ currentHoverRow.name }}
<el-button type="text" @click="showPopup=false">关闭</el-button>
</div>
<hr>
<div class="blocker-list">
<span class="blocker" @click="onShareView(currentHoverRow)">
<View class="plus-icon"></View>预览</span>
<span class="blocker" @click="showShareMessage(currentHoverRow)">
<View class="plus-icon"></View>详情</span>
<span class="blocker" @click="onDownload(currentHoverRow)">
<Download class="plus-icon"></Download>下载</span>
<span class="blocker" @click="onShareDelete(currentHoverRow)">
<Delete class="plus-icon"></Delete>删除</span>
</div>
</div>
</Transition>
<div v-if="dynamicVNode" style="height: inherit;">
<component :is="dynamicVNode" />
</div>
</template>
<style lang="scss" scoped>
.app_container {
padding: 10px;
height: calc(100% - 10px);
overflow-y: auto;
width: 100%;
position: relative;
}
.navBtn{
position: fixed;
background-color: #ffffff94;
width: 100%;
z-index: 55;
}
//---------------animation
/* 遮罩: popup 的遮罩 */
.mask{
position: fixed;
inset: 0;
background:rgba(0,0,0,.4);
z-index:999;
}
.bs-wrapper{
position: fixed;
display: flex;
flex-direction: column;
left:0;
right:0;
bottom:0;
height:36vh; /* 半屏停住 */
background:#f1f1f1;
border-radius:16px 16px 0 0;
z-index:1000;
overflow-y:auto;
padding: 8px 16px;
hr{
margin: 8px 0;
border: none;
width: 88%;
align-self: center;
background: #63616145;
}
}
.popupTitle{
display: flex;
align-items: center;
button{
margin-left: auto;
margin-right: 5px;
}
}
.blocker-list{
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
font-size: small;
.blocker{
display: flex;
flex-direction: column;
align-items: center;
padding-top: 15px;
background-color: white;
border-radius: 5px;
margin: 6px;
width: 70px;
height: 70px;
}
}
.popuper-enter-from{
transform:translateY(100%);
opacity:0;
}
/* 进入过程 */
.popuper-enter-active{
transition:all .3s ease-out;
}
/* 离开后:回到下方 */
.popuper-leave-to{
transform:translateY(50px);
opacity:0;
}
.popuper-leave-active{
transition:all .3s ease-in;
}
.plus-icon{
width: 20px;
height: 20px;
}
//---------------------------
</style>

55
src/views/doc/space.vue

@ -46,7 +46,7 @@ const matterList = ref<matterInfo[]>([])
const searchname=ref("") //
const newdirName=ref("") //
const currentHoverRow=ref<matterInfo>({}) //table
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) //
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true,permits:{}}]) //
const currentNode=ref<matterInfo>({}) //
const dynamicVNode = ref<VNode | null>(null) //permission
@ -98,31 +98,6 @@ function updateListOrGrid(val:boolean){
}
//--------------&-------------
function onAccessManage(){
//manage
dynamicVNode.value = h(sharePermission, {
uid: uid,
uuid: "",
spaceid:props.spaceid, //
confirmFunc: (_list: string[],_infos:string[]) => {
//
//_len=_list.length
let permited = btoa(_list.join("|"))
doAccessManage(uid,{
"space":props.spaceid,
"roles":permited,
"owner":owner.value,
"len":_list.length
}).then(()=>{
})
},
closeFunc: () => {
dynamicVNode.value=null
}
})
}
//
function onSpacePManage(row:matterInfo){
dynamicVNode.value=h(spacePermission,{
@ -333,21 +308,9 @@ function handleDoubleClick(row:matterInfo,ind?:number){
}
}
function handleMouseEnter(row:any){
currentHoverRow.value=row.name
}
//
function handleSingleUpload(response:any){
handleAiUpload(response.data)
onLoadMatterList()
}
interface uploadError{
msg:string
}
//
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
ElMessage.error(JSON.parse(error.message).msg)
}
//, 使
async function onCustomUpload(e:Event){
@ -555,6 +518,7 @@ onMounted(() => {
spaceName.value=c_space.name
owner.value=c_space.userUuid
breadcrumbList.value[0].permits=c_space.permits
if (c_space.manager) {
ismanager.value=true
@ -582,7 +546,7 @@ onMounted(() => {
<template>
<div class="app_container">
<el-row :gutter="24" style="margin: 12px 0px;">
<el-row :gutter="24" style="margin: 4px 0px;">
<el-col class="search" style="padding:0px;width: 80%;">
<el-button type="text" :icon="ArrowLeft" @click="router.back()"> </el-button>
<el-input placeholder="搜索文件" v-model="searchname" @blur="searchname===''?onLoadMatterList():''"/>
@ -605,18 +569,15 @@ onMounted(() => {
<el-row :gutter="24">
<el-col :span="14" v-if="CutLevelPermit>=PERMITS.UPLOAD">
<div class="el-button el-button--default" style="position: relative;">
<div class="el-button el-button--default" style="position: relative;height: 24px;">
<el-icon><Upload /></el-icon>
<input type="file" style="position: absolute;opacity: 0;width: 50px;"
@change="onCustomUpload" multiple />
上传
</div>
<el-button icon="plus" @click="createDir">新建</el-button>
<el-button size="small" icon="plus" @click="createDir">新建</el-button>
</el-col>
<el-button style="margin-left: auto;" @click="()=>currentAgent.model=true">AI助手</el-button>
</el-row>
<el-row :gutter="24" style="overflow-y: auto;height: 80%;">
@ -627,7 +588,7 @@ onMounted(() => {
style="width: 100%"
row-key="uuid"
:row-style ="() => ({ lineHeight: '36px' })">
<el-table-column property="name" label="文件名">
<el-table-column property="name">
<template #default="scope">
<input v-if="scope.row.name===''" v-model="newdirName" type="text" autofocus placeholder="文件夹名" style="border:groove;height:30px;" @change="onCreateDir" />
<div v-else style="display: flex; align-items: center;" @click="handleDoubleClick(scope.row)" >
@ -719,11 +680,11 @@ onMounted(() => {
.app_container {
padding: 10px 15px 0px 15px;
height: calc(100vh - 65px);
overflow: hidden;
overflow: auto;
position: relative;
}
.nav-header{
margin-top: 21px;
margin-top: 12px;
width: 100%;
display: flex;
span{

19
src/views/doc/spacePermission.vue

@ -270,8 +270,8 @@ onMounted(()=>{
</script>
<template>
<el-dialog :model-value="true" :style="{'max-height': '750px'}" v-on:close="closeFunc()">
<template #header>
<el-dialog :model-value="true" :style="{'max-height': '750px','padding':'2px'}" v-on:close="closeFunc()">
<template #header style="height: 27px;">
<el-button link @click="managerMode=false;">文档权限管理</el-button>
<el-button v-if="spacePermit.matterUid==spaceid&&uid==suid" link @click="managerMode=true; onShowManagers()">管理员管理</el-button>
</template>
@ -280,7 +280,7 @@ onMounted(()=>{
<div class="tree-node" style="font-weight: bold;position: sticky;">
<span style="width: 100px;text-align: center;">名称</span>
<div v-if="managerMode" class="buttons">
<div class="box-title">管理员</div>
<div >管理员</div>
</div>
<div v-else style="margin: 0 0 0 auto;display: flex;">
<el-tooltip placement="top" effect="dark"
@ -317,7 +317,7 @@ onMounted(()=>{
>
<template #default="{ node, data }">
<div class="tree-node">
<span style="width: 130px;overflow: hidden;">{{ data.name }}</span>
<span style="width: 100px;overflow: hidden;">{{ data.name }}</span>
<div v-if="managerMode" class="buttons">
<el-checkbox v-model="data.ismanager" :indeterminate="data.indeterminate" />
@ -348,7 +348,8 @@ onMounted(()=>{
<style lang="scss" scoped>
.tablelist{
height: 565px;
overflow-y: scroll
overflow-y: scroll;
width: 100%;
}
.tips{
display: flex;
@ -385,4 +386,12 @@ onMounted(()=>{
/* 让整个弹出窗口位置更高一些*/
--el-tree-node-content-height:33px;
}
</style>
<style>
.el-tree-node.is-expanded>.el-tree-node__children{
margin-left: -10px;
}
</style>
Loading…
Cancel
Save