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
482 lines
12 KiB
|
6 days ago
|
<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>
|