48 changed files with 3458 additions and 784 deletions
File diff suppressed because it is too large
@ -0,0 +1,481 @@ |
|||
<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> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue