Compare commits
7 Commits
696e395a02
...
f03181867d
| Author | SHA1 | Date |
|---|---|---|
|
|
f03181867d | 5 days ago |
|
|
8f1694ea51 | 5 days ago |
|
|
8eaad72df3 | 6 days ago |
|
|
7aa0690206 | 6 days ago |
|
|
11c69186c7 | 6 days ago |
|
|
0f54e385c7 | 2 weeks ago |
|
|
e8b9fc6cc8 | 2 weeks ago |
56 changed files with 3784 additions and 843 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
File diff suppressed because it is too large
Loading…
Reference in new issue