13 changed files with 2186 additions and 194 deletions
File diff suppressed because it is too large
@ -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> |
|||
@ -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> |
|||
@ -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 //为何要减3,因为系统公司层级的level从3开始,层级递增4,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> |
|||
Loading…
Reference in new issue