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.

482 lines
12 KiB

<template>
<!-- 截图展示 4T 相同的全屏透明层 + 可拖动/缩放面板不阻挡地图操作 -->
<div v-show="visible" class="screenshot-gallery-root" :class="{ 'sg-ready': layoutReady }">
<div class="sg-panel" :style="panelStyle">
<div class="sg-toolbar" @mousedown="onDragStart">
<span class="sg-title">截图展示</span>
<div class="sg-toolbar-btns" @mousedown.stop>
<input
ref="fileInput"
type="file"
accept="image/*"
multiple
class="sg-hidden-file"
@change="onFilesSelected"
/>
<button type="button" class="sg-txt-btn" @click="triggerPick">添加</button>
<button type="button" class="sg-txt-btn" :disabled="!hasImages" @click="removeCurrent">删除</button>
<button type="button" class="sg-icon-btn" :disabled="!canPrev" title="上一张" @click="prev"></button>
<span class="sg-counter">{{ pageLabel }}</span>
<button type="button" class="sg-icon-btn" :disabled="!canNext" title="下一张" @click="next"></button>
<button type="button" class="sg-close" title="关闭" @click="$emit('update:visible', false)">×</button>
</div>
</div>
<div class="sg-body">
<div v-if="!hasImages" class="sg-empty">点击添加插入图片多图可使用两侧箭头翻页</div>
<div v-else class="sg-img-frame">
<img :key="currentIndex" :src="currentSrc" alt="" class="sg-img" draggable="false" />
</div>
</div>
<div class="sg-resize" @mousedown="onResizeStart" title="拖动调整大小"></div>
</div>
</div>
</template>
<script>
import { saveScreenshotGalleryData, getScreenshotGalleryData } from '@/api/system/routes'
const MAX_IMAGES = 80
export default {
name: 'ScreenshotGalleryPanel',
props: {
visible: { type: Boolean, default: false },
roomId: { type: [String, Number], default: null }
},
data() {
return {
images: [],
currentIndex: 0,
layoutReady: false,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
panelLeft: null,
panelTop: null,
panelWidth: 520,
panelHeight: 400,
isResizing: false,
resizeStartX: 0,
resizeStartY: 0,
resizeStartW: 0,
resizeStartH: 0
}
},
computed: {
panelStyle() {
const left = this.panelLeft != null ? this.panelLeft : 24
const top = this.panelTop != null ? this.panelTop : 100
return {
left: `${left}px`,
top: `${top}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
}
},
hasImages() {
return Array.isArray(this.images) && this.images.length > 0
},
currentSrc() {
if (!this.hasImages) return ''
return this.images[this.currentIndex] || ''
},
canPrev() {
return this.hasImages && this.currentIndex > 0
},
canNext() {
return this.hasImages && this.currentIndex < this.images.length - 1
},
pageLabel() {
if (!this.hasImages) return '0 / 0'
return `${this.currentIndex + 1} / ${this.images.length}`
}
},
watch: {
visible: {
handler(val) {
if (val && this.roomId) {
this.loadData()
}
},
immediate: true
},
roomId: {
handler(val) {
if (val && this.visible) {
this.loadData()
}
},
immediate: true
}
},
beforeDestroy() {
document.removeEventListener('mousemove', this.onDragMove)
document.removeEventListener('mouseup', this.onDragEnd)
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
},
methods: {
async loadData() {
this.layoutReady = false
if (!this.roomId) {
this.layoutReady = true
return
}
try {
const res = await getScreenshotGalleryData({ roomId: this.roomId })
let d = res && res.data
if (d) {
if (typeof d === 'string') {
try {
d = JSON.parse(d)
} catch (e) {
this.layoutReady = true
return
}
}
const imgs = d.images
this.images = Array.isArray(imgs) ? imgs.filter(Boolean) : []
this.currentIndex = this.images.length > 0 ? Math.min(this.currentIndex, this.images.length - 1) : 0
if (d.panelSize) {
const w = Number(d.panelSize.width)
const h = Number(d.panelSize.height)
if (!isNaN(w) && w >= 280 && w <= 1000) this.panelWidth = w
if (!isNaN(h) && h >= 200 && h <= window.innerHeight - 40) this.panelHeight = h
}
if (d.panelPosition) {
const left = Number(d.panelPosition.left)
const top = Number(d.panelPosition.top)
if (!isNaN(left) && left >= 0) this.panelLeft = Math.min(left, window.innerWidth - this.panelWidth)
if (!isNaN(top) && top >= 0) this.panelTop = Math.min(top, window.innerHeight - this.panelHeight)
}
}
} catch (e) {
console.warn('加载截图展示失败:', e)
} finally {
this.layoutReady = true
}
},
triggerPick() {
this.$refs.fileInput && this.$refs.fileInput.click()
},
readFileAsDataURL(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (ev) => resolve(ev.target.result)
reader.onerror = () => resolve(null)
reader.readAsDataURL(file)
})
},
async onFilesSelected(e) {
const files = e.target.files ? Array.from(e.target.files) : []
e.target.value = ''
const imageFiles = files.filter((f) => f.type && f.type.startsWith('image/'))
if (imageFiles.length === 0) return
const remain = MAX_IMAGES - this.images.length
if (remain <= 0) {
this.$message.warning(`最多保存 ${MAX_IMAGES} 张图片`)
return
}
const take = imageFiles.slice(0, remain)
if (take.length < imageFiles.length) {
this.$message.warning(`已达上限,本次仅添加 ${take.length}`)
}
const urls = []
for (const f of take) {
const u = await this.readFileAsDataURL(f)
if (u) urls.push(u)
}
if (urls.length === 0) return
this.images = [...this.images, ...urls]
this.currentIndex = this.images.length - 1
if (this.roomId) this.saveData().catch(() => {})
},
removeCurrent() {
if (!this.hasImages) return
this.images.splice(this.currentIndex, 1)
if (this.currentIndex >= this.images.length) {
this.currentIndex = Math.max(0, this.images.length - 1)
}
if (this.roomId) this.saveData().catch(() => {})
},
prev() {
if (this.canPrev) this.currentIndex -= 1
},
next() {
if (this.canNext) this.currentIndex += 1
},
async saveData() {
if (!this.roomId) {
this.$message.warning('请先进入任务房间后再保存')
return
}
try {
const payload = {
images: this.images,
panelSize: { width: this.panelWidth, height: this.panelHeight }
}
if (this.panelLeft != null && this.panelTop != null) {
payload.panelPosition = { left: this.panelLeft, top: this.panelTop }
}
await saveScreenshotGalleryData({
roomId: this.roomId,
data: JSON.stringify(payload)
})
} catch (e) {
console.error('保存截图展示失败:', e)
this.$message.error('保存截图展示失败,请检查网络或权限')
}
},
onDragStart(e) {
e.preventDefault()
this.isDragging = true
const currentLeft = this.panelLeft != null ? this.panelLeft : 24
const currentTop = this.panelTop != null ? this.panelTop : 100
this.dragStartX = e.clientX - currentLeft
this.dragStartY = e.clientY - currentTop
document.addEventListener('mousemove', this.onDragMove)
document.addEventListener('mouseup', this.onDragEnd)
},
onDragMove(e) {
if (!this.isDragging) return
e.preventDefault()
let left = e.clientX - this.dragStartX
let top = e.clientY - this.dragStartY
left = Math.max(0, Math.min(window.innerWidth - this.panelWidth, left))
top = Math.max(0, Math.min(window.innerHeight - this.panelHeight, top))
this.panelLeft = left
this.panelTop = top
},
onDragEnd() {
this.isDragging = false
document.removeEventListener('mousemove', this.onDragMove)
document.removeEventListener('mouseup', this.onDragEnd)
if (this.roomId) this.saveData()
},
onResizeStart(e) {
e.preventDefault()
e.stopPropagation()
this.isResizing = true
this.resizeStartX = e.clientX
this.resizeStartY = e.clientY
this.resizeStartW = this.panelWidth
this.resizeStartH = this.panelHeight
document.addEventListener('mousemove', this.onResizeMove)
document.addEventListener('mouseup', this.onResizeEnd)
},
onResizeMove(e) {
if (!this.isResizing) return
e.preventDefault()
const dx = e.clientX - this.resizeStartX
const dy = e.clientY - this.resizeStartY
let w = Math.max(280, Math.min(1000, this.resizeStartW + dx))
let h = Math.max(200, Math.min(window.innerHeight - 40, this.resizeStartH + dy))
this.panelWidth = w
this.panelHeight = h
},
onResizeEnd() {
this.isResizing = false
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
if (this.roomId) this.saveData()
}
}
}
</script>
<style scoped>
.screenshot-gallery-root {
position: fixed;
inset: 0;
z-index: 200;
background: transparent;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-out;
}
.screenshot-gallery-root.sg-ready {
opacity: 1;
pointer-events: none;
}
.screenshot-gallery-root.sg-ready .sg-panel {
pointer-events: auto;
}
.sg-panel {
position: fixed;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(22, 93, 255, 0.15);
border: 1px solid #e0edff;
overflow: hidden;
z-index: 201;
}
.sg-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px 8px 14px;
background: linear-gradient(180deg, #f9fcff 0%, #eef5ff 100%);
border-bottom: 1px solid #ddeaff;
cursor: move;
user-select: none;
}
.sg-title {
font-size: 14px;
font-weight: 600;
color: #165dff;
}
.sg-toolbar-btns {
display: flex;
align-items: center;
gap: 6px;
cursor: default;
}
.sg-hidden-file {
display: none;
}
.sg-txt-btn {
background: #ebf3ff;
color: #165dff;
border: 1px solid #cce5ff;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: background 0.15s;
}
.sg-txt-btn:hover:not(:disabled) {
background: #e0edff;
}
.sg-txt-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.sg-icon-btn {
width: 26px;
height: 26px;
padding: 0;
border: 1px solid #cce5ff;
background: #fff;
color: #165dff;
border-radius: 4px;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.sg-icon-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.sg-counter {
font-size: 12px;
color: #546e7a;
min-width: 52px;
text-align: center;
}
.sg-close {
width: 28px;
height: 28px;
margin-left: 4px;
border: 1px solid #cce5ff;
background: #ebf3ff;
color: #165dff;
border-radius: 50%;
font-size: 16px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.sg-close:hover {
background: #e0edff;
}
.sg-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background: #f5f9ff;
}
.sg-empty {
font-size: 13px;
color: #78909c;
text-align: center;
padding: 24px 16px;
line-height: 1.6;
}
.sg-img-frame {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.sg-img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
vertical-align: middle;
}
.sg-resize {
position: absolute;
right: 0;
bottom: 0;
width: 18px;
height: 18px;
cursor: nwse-resize;
user-select: none;
z-index: 5;
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.25) 50%);
}
.sg-resize:hover {
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.45) 50%);
}
</style>