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.
1173 lines
36 KiB
1173 lines
36 KiB
<template>
|
|
<div
|
|
class="step-canvas-content"
|
|
@mousemove="onPaginationAreaMouseMove"
|
|
@mouseleave="onPaginationAreaMouseLeave"
|
|
>
|
|
<!-- 翻页控件:多页时,鼠标悬停左右边缘才显示 -->
|
|
<div
|
|
v-show="currentPageCount > 1 && showLeftPagination"
|
|
class="pagination-left"
|
|
@click="canPrevPage && prevPage()"
|
|
>
|
|
<div class="pagination-btn pagination-btn-circle" :class="{ disabled: !canPrevPage }">
|
|
<i class="el-icon-arrow-left"></i>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-show="currentPageCount > 1 && showRightPagination"
|
|
class="pagination-right"
|
|
@click="canNextPage && nextPage()"
|
|
>
|
|
<div class="pagination-btn pagination-btn-circle" :class="{ disabled: !canNextPage }">
|
|
<i class="el-icon-arrow-right"></i>
|
|
</div>
|
|
</div>
|
|
<!-- 页码指示:多页时可点击预览并删除页面 -->
|
|
<el-popover
|
|
v-if="currentPageCount > 1"
|
|
ref="pagePreviewPopover"
|
|
placement="top"
|
|
width="320"
|
|
trigger="click"
|
|
popper-class="page-preview-popover"
|
|
>
|
|
<div slot="reference" class="pagination-indicator pagination-indicator-clickable">
|
|
{{ currentPageIndex + 1 }} / {{ currentPageCount }}
|
|
</div>
|
|
<div class="page-preview-list">
|
|
<div class="page-preview-title">本小标题下的所有页面</div>
|
|
<div
|
|
v-for="(page, idx) in allPagesForPreview"
|
|
:key="idx"
|
|
class="page-preview-item"
|
|
:class="{ active: currentPageIndex === idx }"
|
|
>
|
|
<span class="page-preview-label" @click="goToPage(idx)">
|
|
第 {{ idx + 1 }} 页
|
|
<span class="page-preview-meta">{{ (page.icons && page.icons.length) || 0 }} 图标 · {{ (page.textBoxes && page.textBoxes.length) || 0 }} 文本框</span>
|
|
</span>
|
|
<el-button
|
|
type="text"
|
|
size="mini"
|
|
icon="el-icon-delete"
|
|
class="page-delete-btn"
|
|
:disabled="currentPageCount <= 1"
|
|
@click="deletePage(idx)"
|
|
>删除</el-button>
|
|
</div>
|
|
</div>
|
|
</el-popover>
|
|
<input
|
|
ref="bgInput"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none"
|
|
@change="handleBackgroundSelect"
|
|
/>
|
|
<input
|
|
ref="iconImageInput"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none"
|
|
@change="handleIconImageSelect"
|
|
/>
|
|
<div
|
|
ref="canvas"
|
|
class="task-canvas"
|
|
:class="{ 'insert-icon': insertMode === 'icon', 'insert-textbox': insertMode === 'textbox' }"
|
|
:style="canvasStyle"
|
|
@click="onCanvasClick"
|
|
@mousedown="onCanvasMouseDown"
|
|
@mousemove="onCanvasMouseMove"
|
|
@mouseup="onCanvasMouseUp"
|
|
@mouseleave="onCanvasMouseUp"
|
|
>
|
|
<div
|
|
v-for="icon in currentSubContent.icons"
|
|
:key="icon.id"
|
|
class="canvas-icon"
|
|
:class="{ selected: selectedId === icon.id }"
|
|
:style="getIconStyle(icon)"
|
|
@mousedown.stop="selectElement(icon.id, $event)"
|
|
>
|
|
<div class="icon-body" :style="{ transform: `rotate(${icon.rotation || 0}deg)` }">
|
|
<img v-if="icon.src" :src="icon.src" class="icon-image" />
|
|
<i v-else class="el-icon-picture-outline icon-placeholder"></i>
|
|
</div>
|
|
<div class="icon-resize-handle" v-if="selectedId === icon.id">
|
|
<div
|
|
v-for="pos in resizeHandles"
|
|
:key="pos"
|
|
class="handle"
|
|
:class="pos"
|
|
@mousedown.stop="startResize($event, icon, pos)"
|
|
></div>
|
|
<div class="rotate-handle" @mousedown.stop="startRotate($event, icon)"></div>
|
|
<div class="delete-handle" @mousedown.stop="deleteIcon(icon.id)" title="删除">
|
|
<i class="el-icon-delete"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-for="box in currentSubContent.textBoxes"
|
|
:key="box.id"
|
|
class="canvas-textbox"
|
|
:class="{ selected: selectedId === box.id }"
|
|
:style="getTextBoxStyle(box)"
|
|
@mousedown="onTextBoxMouseDown(box, $event)"
|
|
>
|
|
<div class="textbox-drag-bar" @mousedown.stop="selectElement(box.id, $event)"></div>
|
|
<div class="textbox-input-wrapper">
|
|
<div
|
|
v-if="box.placeholder && !box.text && focusedBoxId !== box.id"
|
|
class="textbox-placeholder"
|
|
:style="getTextBoxPlaceholderStyle(box)"
|
|
@mousedown="focusTextBox(box, $event)"
|
|
>{{ box.placeholder }}</div>
|
|
<div
|
|
class="textbox-input"
|
|
contenteditable="true"
|
|
:style="getTextBoxInputStyle(box)"
|
|
@focus="onTextBoxFocus(box)"
|
|
@blur="onTextBoxBlur(box, $event)"
|
|
@mousedown.stop="selectedId = box.id"
|
|
@contextmenu="onTextBoxContextMenu(box, $event)"
|
|
></div>
|
|
</div>
|
|
<div class="textbox-resize-handle" v-if="selectedId === box.id">
|
|
<div
|
|
v-for="pos in resizeHandles"
|
|
:key="pos"
|
|
class="handle"
|
|
:class="pos"
|
|
@mousedown.stop="startResizeTextBox($event, box, pos)"
|
|
></div>
|
|
<div class="textbox-rotate-handle" @mousedown.stop="startRotateTextBox($event, box)"></div>
|
|
<div class="delete-handle textbox-delete" @mousedown.stop="deleteTextBox(box.id)" title="删除">
|
|
<i class="el-icon-delete"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="drawingTextBox"
|
|
class="drawing-textbox"
|
|
:style="drawingTextBoxStyle"
|
|
></div>
|
|
</div>
|
|
<div
|
|
v-if="formatToolbarBox"
|
|
class="textbox-format-toolbar-fixed"
|
|
:style="{ left: formatToolbarPosition.x + 'px', top: formatToolbarPosition.y + 'px' }"
|
|
@mousedown.stop
|
|
>
|
|
<el-select v-model="formatToolbarBox.fontFamily" size="mini" placeholder="字体" class="format-font">
|
|
<el-option v-for="f in fontOptions" :key="f" :label="f" :value="f" />
|
|
</el-select>
|
|
<el-select v-model="formatToolbarBox.fontSize" size="mini" placeholder="字号" class="format-size">
|
|
<el-option v-for="s in fontSizeOptions" :key="s" :label="String(s)" :value="s" />
|
|
</el-select>
|
|
<el-color-picker v-model="formatToolbarBox.color" class="format-color" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import request from '@/utils/request'
|
|
import { resolveImageUrl } from '@/utils/imageUrl'
|
|
import { createRollCallTextBoxes, createSubTitleTemplate, createIntentBriefingTemplate, createTaskPlanningTemplate, createSimpleTitleTemplate, SUB_TITLE_TEMPLATE_NAMES, SIMPLE_TITLE_NAMES } from './rollCallTemplate'
|
|
import { getPageContent, ensurePagesStructure } from './subContentPages'
|
|
|
|
let idCounter = 0
|
|
function genId() {
|
|
return 'el_' + (++idCounter) + '_' + Date.now()
|
|
}
|
|
|
|
const FONT_OPTIONS = ['宋体', '黑体', '微软雅黑', '楷体', '仿宋', 'Arial', 'Times New Roman', 'Verdana', 'Georgia']
|
|
const FONT_SIZE_OPTIONS = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
|
|
const DEFAULT_FONT = { fontSize: 14, fontFamily: '微软雅黑', color: '#333333' }
|
|
|
|
export default {
|
|
name: 'StepCanvasContent',
|
|
props: {
|
|
content: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
activeSubIndex: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
backgroundImage: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
fontOptions: FONT_OPTIONS,
|
|
fontSizeOptions: FONT_SIZE_OPTIONS,
|
|
insertMode: null,
|
|
pendingIconImage: null,
|
|
selectedId: null,
|
|
resizeHandles: ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'],
|
|
dragState: null,
|
|
drawingTextBox: false,
|
|
drawStartX: 0,
|
|
drawStartY: 0,
|
|
drawCurrentX: 0,
|
|
drawCurrentY: 0,
|
|
formatToolbarBoxId: null,
|
|
formatToolbarPosition: { x: 0, y: 0 },
|
|
focusedBoxId: null,
|
|
showLeftPagination: false,
|
|
showRightPagination: false
|
|
}
|
|
},
|
|
computed: {
|
|
rawSubContent() {
|
|
const c = this.content
|
|
return c.subContents && c.subContents[this.activeSubIndex] ? c.subContents[this.activeSubIndex] : null
|
|
},
|
|
currentSubContent() {
|
|
return getPageContent(this.rawSubContent)
|
|
},
|
|
currentPageCount() {
|
|
const sc = this.rawSubContent
|
|
if (!sc) return 0
|
|
ensurePagesStructure(sc)
|
|
return sc.pages?.length || 0
|
|
},
|
|
currentPageIndex() {
|
|
const sc = this.rawSubContent
|
|
if (!sc) return 0
|
|
ensurePagesStructure(sc)
|
|
return sc.currentPageIndex || 0
|
|
},
|
|
canPrevPage() {
|
|
return this.currentPageIndex > 0
|
|
},
|
|
canNextPage() {
|
|
return this.currentPageIndex < this.currentPageCount - 1
|
|
},
|
|
allPagesForPreview() {
|
|
const sc = this.rawSubContent
|
|
if (!sc) return []
|
|
ensurePagesStructure(sc)
|
|
return sc.pages || []
|
|
},
|
|
formatToolbarBox() {
|
|
if (!this.formatToolbarBoxId) return null
|
|
return this.currentSubContent.textBoxes.find(t => t.id === this.formatToolbarBoxId) || null
|
|
},
|
|
canvasStyle() {
|
|
const style = {}
|
|
if (this.backgroundImage) {
|
|
style.backgroundImage = `url(${resolveImageUrl(this.backgroundImage)})`
|
|
style.backgroundSize = '100% 100%'
|
|
style.backgroundPosition = 'center'
|
|
style.backgroundRepeat = 'no-repeat'
|
|
}
|
|
return style
|
|
},
|
|
drawingTextBoxStyle() {
|
|
const left = Math.min(this.drawStartX, this.drawCurrentX)
|
|
const top = Math.min(this.drawStartY, this.drawCurrentY)
|
|
const width = Math.abs(this.drawCurrentX - this.drawStartX)
|
|
const height = Math.abs(this.drawCurrentY - this.drawStartY)
|
|
return {
|
|
left: left + 'px',
|
|
top: top + 'px',
|
|
width: Math.max(width, 20) + 'px',
|
|
height: Math.max(height, 20) + 'px'
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
content: {
|
|
handler() {
|
|
this.$nextTick(() => this.syncTextBoxContent())
|
|
},
|
|
deep: true
|
|
},
|
|
activeSubIndex() {
|
|
this.selectedId = null
|
|
this.$nextTick(() => {
|
|
this.ensureRollCallTextBoxes()
|
|
this.ensureIntentBriefingTemplate()
|
|
this.ensureTaskPlanningTemplate()
|
|
this.ensureSimpleTitleTemplate()
|
|
this.ensureSubTitleTemplate()
|
|
})
|
|
}
|
|
},
|
|
mounted() {
|
|
this._keydownHandler = (e) => this.onKeydown(e)
|
|
this._clickHandler = (e) => {
|
|
if (this.formatToolbarBoxId && !e.target.closest('.textbox-format-toolbar-fixed')) {
|
|
this.formatToolbarBoxId = null
|
|
}
|
|
}
|
|
document.addEventListener('keydown', this._keydownHandler)
|
|
document.addEventListener('click', this._clickHandler)
|
|
this.$nextTick(() => {
|
|
this.ensureRollCallTextBoxes()
|
|
this.ensureIntentBriefingTemplate()
|
|
this.ensureTaskPlanningTemplate()
|
|
this.ensureSimpleTitleTemplate()
|
|
this.ensureSubTitleTemplate()
|
|
this.syncTextBoxContent()
|
|
})
|
|
},
|
|
updated() {
|
|
this.$nextTick(() => this.syncTextBoxContent())
|
|
},
|
|
beforeDestroy() {
|
|
this.syncFromDomToData()
|
|
document.removeEventListener('keydown', this._keydownHandler)
|
|
document.removeEventListener('click', this._clickHandler)
|
|
},
|
|
methods: {
|
|
handleInsertCommand(cmd) {
|
|
if (cmd === 'removeBackground') {
|
|
this.$emit('background-change', null)
|
|
return
|
|
}
|
|
this.insertMode = cmd
|
|
if (cmd === 'background') {
|
|
this.$refs.bgInput.value = ''
|
|
this.$refs.bgInput.click()
|
|
} else if (cmd === 'icon') {
|
|
this.$refs.iconImageInput.value = ''
|
|
this.$refs.iconImageInput.click()
|
|
}
|
|
},
|
|
async handleBackgroundSelect(e) {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
this.insertMode = null
|
|
e.target.value = ''
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
const res = await request.post('/common/upload', formData)
|
|
if (res && (res.code === 200 || res.fileName)) {
|
|
const path = res.fileName || res.url
|
|
if (path) this.$emit('background-change', path)
|
|
} else {
|
|
this.$message.error(res?.msg || '背景图上传失败')
|
|
}
|
|
} catch (err) {
|
|
this.$message.error(err?.response?.data?.msg || err?.message || '背景图上传失败')
|
|
}
|
|
},
|
|
handleIconImageSelect(e) {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
const reader = new FileReader()
|
|
reader.onload = (ev) => {
|
|
this.pendingIconImage = ev.target.result
|
|
this.insertMode = 'icon'
|
|
}
|
|
reader.readAsDataURL(file)
|
|
e.target.value = ''
|
|
},
|
|
onCanvasClick(e) {
|
|
if (this.insertMode === 'icon' && this.pendingIconImage) {
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
const x = e.clientX - rect.left
|
|
const y = e.clientY - rect.top
|
|
this.currentSubContent.icons.push({
|
|
id: genId(),
|
|
x, y, width: 60, height: 60, rotation: 0,
|
|
src: this.pendingIconImage
|
|
})
|
|
this.pendingIconImage = null
|
|
this.insertMode = null
|
|
}
|
|
if (this.insertMode !== 'textbox' && !this.drawingTextBox) {
|
|
if (!e.target.closest('.canvas-textbox') && !e.target.closest('.canvas-icon')) {
|
|
this.selectedId = null
|
|
}
|
|
}
|
|
},
|
|
onCanvasMouseDown(e) {
|
|
if (this.insertMode === 'textbox' && !this.drawingTextBox) {
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
this.drawingTextBox = true
|
|
this.drawStartX = e.clientX - rect.left
|
|
this.drawStartY = e.clientY - rect.top
|
|
this.drawCurrentX = this.drawStartX
|
|
this.drawCurrentY = this.drawStartY
|
|
}
|
|
},
|
|
onCanvasMouseMove(e) {
|
|
if (this.drawingTextBox) {
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
this.drawCurrentX = e.clientX - rect.left
|
|
this.drawCurrentY = e.clientY - rect.top
|
|
}
|
|
if (this.dragState) {
|
|
this.$refs.canvas.style.cursor = this.dragState.cursor || 'move'
|
|
}
|
|
},
|
|
onCanvasMouseUp(e) {
|
|
if (this.drawingTextBox) {
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
const x = Math.min(this.drawStartX, this.drawCurrentX)
|
|
const y = Math.min(this.drawStartY, this.drawCurrentY)
|
|
const w = Math.max(Math.abs(this.drawCurrentX - this.drawStartX), 20)
|
|
const h = Math.max(Math.abs(this.drawCurrentY - this.drawStartY), 20)
|
|
this.currentSubContent.textBoxes.push({
|
|
id: genId(), x, y, width: w, height: h, text: '', rotation: 0,
|
|
fontSize: DEFAULT_FONT.fontSize,
|
|
fontFamily: DEFAULT_FONT.fontFamily,
|
|
color: DEFAULT_FONT.color
|
|
})
|
|
this.drawingTextBox = false
|
|
this.insertMode = null
|
|
}
|
|
this.dragState = null
|
|
this.$refs.canvas && (this.$refs.canvas.style.cursor = '')
|
|
},
|
|
onTextBoxMouseDown(box, e) {
|
|
if (e.target.closest('.textbox-input')) return
|
|
this.selectElement(box.id, e)
|
|
},
|
|
onTextBoxContextMenu(box, e) {
|
|
const sel = window.getSelection()
|
|
const hasSelection = sel && sel.toString().trim().length > 0
|
|
if (!hasSelection) return
|
|
e.preventDefault()
|
|
this.formatToolbarBoxId = box.id
|
|
this.formatToolbarPosition = { x: e.clientX, y: e.clientY }
|
|
},
|
|
selectElement(id, e) {
|
|
this.selectedId = id
|
|
const icon = this.currentSubContent.icons.find(i => i.id === id)
|
|
const textbox = this.currentSubContent.textBoxes.find(t => t.id === id)
|
|
const el = icon || textbox
|
|
if (!el) return
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
const offsetX = e.clientX - rect.left - el.x
|
|
const offsetY = e.clientY - rect.top - el.y
|
|
this.dragState = {
|
|
type: 'drag', id, isIcon: !!icon,
|
|
startX: e.clientX, startY: e.clientY,
|
|
origX: el.x, origY: el.y, offsetX, offsetY
|
|
}
|
|
const onMove = (ev) => {
|
|
if (this.dragState?.type !== 'drag') return
|
|
if (this.dragState.isIcon) return
|
|
const dx = ev.clientX - this.dragState.startX
|
|
const dy = ev.clientY - this.dragState.startY
|
|
this.dragState.origX += dx
|
|
this.dragState.origY += dy
|
|
el.x = this.dragState.origX
|
|
el.y = this.dragState.origY
|
|
this.dragState.startX = ev.clientX
|
|
this.dragState.startY = ev.clientY
|
|
}
|
|
const onUp = (ev) => {
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
if (this.dragState?.type === 'drag' && this.dragState.isIcon) {
|
|
const r = this.$refs.canvas.getBoundingClientRect()
|
|
el.x = ev.clientX - r.left - this.dragState.offsetX
|
|
el.y = ev.clientY - r.top - this.dragState.offsetY
|
|
}
|
|
this.dragState = null
|
|
}
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
},
|
|
startResize(e, icon, pos) {
|
|
e.stopPropagation()
|
|
this.dragState = { type: 'resize', id: icon.id, pos, startX: e.clientX, startY: e.clientY }
|
|
const onMove = (ev) => {
|
|
if (this.dragState?.type !== 'resize') return
|
|
const dx = ev.clientX - this.dragState.startX
|
|
const dy = ev.clientY - this.dragState.startY
|
|
this.dragState.startX = ev.clientX
|
|
this.dragState.startY = ev.clientY
|
|
const { pos } = this.dragState
|
|
let w = icon.width, h = icon.height, x = icon.x, y = icon.y
|
|
if (pos.includes('e')) w = Math.max(20, w + dx)
|
|
if (pos.includes('w')) { w = Math.max(20, w - dx); x = icon.x + dx }
|
|
if (pos.includes('s')) h = Math.max(20, h + dy)
|
|
if (pos.includes('n')) { h = Math.max(20, h - dy); y = icon.y + dy }
|
|
icon.width = w
|
|
icon.height = h
|
|
icon.x = x
|
|
icon.y = y
|
|
}
|
|
const onUp = () => {
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
this.dragState = null
|
|
}
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
},
|
|
startRotate(e, icon) {
|
|
e.stopPropagation()
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
const cx = icon.x + icon.width / 2
|
|
const cy = icon.y + icon.height / 2
|
|
const startAngle = Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx)
|
|
const startRot = icon.rotation || 0
|
|
const onMove = (ev) => {
|
|
const angle = Math.atan2(ev.clientY - rect.top - cy, ev.clientX - rect.left - cx)
|
|
const delta = ((angle - startAngle) * 180 / Math.PI)
|
|
icon.rotation = (startRot + delta + 360) % 360
|
|
}
|
|
const onUp = () => {
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
}
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
},
|
|
startResizeTextBox(e, box, pos) {
|
|
e.stopPropagation()
|
|
this.dragState = { type: 'resize', id: box.id, pos, startX: e.clientX, startY: e.clientY }
|
|
const onMove = (ev) => {
|
|
if (this.dragState?.type !== 'resize') return
|
|
const dx = ev.clientX - this.dragState.startX
|
|
const dy = ev.clientY - this.dragState.startY
|
|
this.dragState.startX = ev.clientX
|
|
this.dragState.startY = ev.clientY
|
|
const { pos } = this.dragState
|
|
let w = box.width, h = box.height, x = box.x, y = box.y
|
|
if (pos.includes('e')) w = Math.max(20, w + dx)
|
|
if (pos.includes('w')) { w = Math.max(20, w - dx); x = box.x + dx }
|
|
if (pos.includes('s')) h = Math.max(20, h + dy)
|
|
if (pos.includes('n')) { h = Math.max(20, h - dy); y = box.y + dy }
|
|
box.width = w
|
|
box.height = h
|
|
box.x = x
|
|
box.y = y
|
|
}
|
|
const onUp = () => {
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
this.dragState = null
|
|
}
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
},
|
|
getIconStyle(icon) {
|
|
return {
|
|
left: icon.x + 'px',
|
|
top: icon.y + 'px',
|
|
width: icon.width + 'px',
|
|
height: icon.height + 'px'
|
|
}
|
|
},
|
|
getTextBoxStyle(box) {
|
|
return {
|
|
left: box.x + 'px',
|
|
top: box.y + 'px',
|
|
width: box.width + 'px',
|
|
height: box.height + 'px',
|
|
transform: `rotate(${box.rotation || 0}deg)`
|
|
}
|
|
},
|
|
getTextBoxInputStyle(box) {
|
|
const style = {
|
|
fontSize: (box.fontSize || DEFAULT_FONT.fontSize) + 'px',
|
|
fontFamily: box.fontFamily || DEFAULT_FONT.fontFamily,
|
|
color: box.color || DEFAULT_FONT.color
|
|
}
|
|
if (box.fontWeight) style.fontWeight = box.fontWeight
|
|
return style
|
|
},
|
|
getTextBoxPlaceholderStyle(box) {
|
|
const style = {
|
|
fontSize: (box.fontSize || DEFAULT_FONT.fontSize) + 'px',
|
|
fontFamily: box.fontFamily || DEFAULT_FONT.fontFamily,
|
|
color: '#999'
|
|
}
|
|
if (box.fontWeight) style.fontWeight = box.fontWeight
|
|
return style
|
|
},
|
|
focusTextBox(box, e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
this.focusedBoxId = box.id
|
|
this.selectedId = box.id
|
|
this.$nextTick(() => {
|
|
const wrapper = e.target.closest('.textbox-input-wrapper')
|
|
const input = wrapper && wrapper.querySelector('.textbox-input')
|
|
if (input) input.focus()
|
|
})
|
|
},
|
|
onTextBoxFocus(box) {
|
|
this.focusedBoxId = box.id
|
|
},
|
|
onTextBoxBlur(box, e) {
|
|
box.text = (e.target.innerText || '').trim()
|
|
this.focusedBoxId = null
|
|
},
|
|
ensureRollCallTextBoxes() {
|
|
const subTitle = this.content.subTitles && this.content.subTitles[this.activeSubIndex]
|
|
if (subTitle !== '点名') return
|
|
const sub = this.currentSubContent
|
|
if (!sub.textBoxes || sub.textBoxes.length > 0) return
|
|
const canvas = this.$refs.canvas
|
|
const w = canvas ? canvas.offsetWidth : 800
|
|
const h = canvas ? canvas.offsetHeight : 500
|
|
sub.textBoxes.push(...createRollCallTextBoxes(w, h))
|
|
},
|
|
ensureIntentBriefingTemplate() {
|
|
const subTitle = this.content.subTitles && this.content.subTitles[this.activeSubIndex]
|
|
if (subTitle !== '意图通报') return
|
|
const sub = this.currentSubContent
|
|
if (!sub.textBoxes || sub.textBoxes.length > 0) return
|
|
const canvas = this.$refs.canvas
|
|
const w = canvas ? canvas.offsetWidth : 800
|
|
const h = canvas ? canvas.offsetHeight : 500
|
|
sub.textBoxes.push(...createIntentBriefingTemplate(w, h))
|
|
},
|
|
ensureTaskPlanningTemplate() {
|
|
const subTitle = this.content.subTitles && this.content.subTitles[this.activeSubIndex]
|
|
if (subTitle !== '任务规划') return
|
|
const sub = this.currentSubContent
|
|
if (!sub.textBoxes || sub.textBoxes.length > 0) return
|
|
const canvas = this.$refs.canvas
|
|
const w = canvas ? canvas.offsetWidth : 800
|
|
const h = canvas ? canvas.offsetHeight : 500
|
|
sub.textBoxes.push(...createTaskPlanningTemplate(w, h))
|
|
},
|
|
ensureSimpleTitleTemplate() {
|
|
const subTitle = this.content.subTitles && this.content.subTitles[this.activeSubIndex]
|
|
if (!subTitle || !SIMPLE_TITLE_NAMES.includes(subTitle)) return
|
|
const sub = this.currentSubContent
|
|
if (!sub.textBoxes || sub.textBoxes.length > 0) return
|
|
const canvas = this.$refs.canvas
|
|
const w = canvas ? canvas.offsetWidth : 800
|
|
const h = canvas ? canvas.offsetHeight : 500
|
|
sub.textBoxes.push(...createSimpleTitleTemplate(subTitle, w, h))
|
|
},
|
|
onPaginationAreaMouseMove(e) {
|
|
if (this.currentPageCount <= 1) return
|
|
const el = this.$el
|
|
if (!el) return
|
|
const rect = el.getBoundingClientRect()
|
|
const x = e.clientX - rect.left
|
|
const edgeZone = 80
|
|
this.showLeftPagination = x < edgeZone
|
|
this.showRightPagination = x > rect.width - edgeZone
|
|
},
|
|
onPaginationAreaMouseLeave() {
|
|
this.showLeftPagination = false
|
|
this.showRightPagination = false
|
|
},
|
|
prevPage() {
|
|
const sc = this.rawSubContent
|
|
if (!sc || !this.canPrevPage) return
|
|
ensurePagesStructure(sc)
|
|
sc.currentPageIndex = Math.max(0, (sc.currentPageIndex || 0) - 1)
|
|
this.selectedId = null
|
|
},
|
|
nextPage() {
|
|
const sc = this.rawSubContent
|
|
if (!sc || !this.canNextPage) return
|
|
ensurePagesStructure(sc)
|
|
sc.currentPageIndex = Math.min((sc.pages?.length || 1) - 1, (sc.currentPageIndex || 0) + 1)
|
|
this.selectedId = null
|
|
},
|
|
goToPage(idx) {
|
|
const sc = this.rawSubContent
|
|
if (!sc) return
|
|
ensurePagesStructure(sc)
|
|
sc.currentPageIndex = Math.max(0, Math.min(idx, (sc.pages?.length || 1) - 1))
|
|
this.selectedId = null
|
|
this.$refs.pagePreviewPopover && this.$refs.pagePreviewPopover.doClose()
|
|
},
|
|
deletePage(idx) {
|
|
const sc = this.rawSubContent
|
|
if (!sc || this.currentPageCount <= 1) return
|
|
ensurePagesStructure(sc)
|
|
const pages = sc.pages
|
|
if (!pages || idx < 0 || idx >= pages.length) return
|
|
pages.splice(idx, 1)
|
|
const cur = sc.currentPageIndex || 0
|
|
if (cur >= pages.length) sc.currentPageIndex = Math.max(0, pages.length - 1)
|
|
else if (idx < cur) sc.currentPageIndex = cur - 1
|
|
this.selectedId = null
|
|
this.$refs.pagePreviewPopover && this.$refs.pagePreviewPopover.doClose()
|
|
},
|
|
ensureSubTitleTemplate() {
|
|
const subTitle = this.content.subTitles && this.content.subTitles[this.activeSubIndex]
|
|
if (!subTitle || subTitle === '点名' || subTitle === '意图通报' || subTitle === '任务规划' || SIMPLE_TITLE_NAMES.includes(subTitle) || !SUB_TITLE_TEMPLATE_NAMES.includes(subTitle)) return
|
|
const sub = this.currentSubContent
|
|
if (!sub.textBoxes || sub.textBoxes.length > 0) return
|
|
const canvas = this.$refs.canvas
|
|
const w = canvas ? canvas.offsetWidth : 800
|
|
const h = canvas ? canvas.offsetHeight : 500
|
|
sub.textBoxes.push(...createSubTitleTemplate(subTitle, w, h))
|
|
},
|
|
deleteIcon(id) {
|
|
const idx = this.currentSubContent.icons.findIndex(i => i.id === id)
|
|
if (idx >= 0) this.currentSubContent.icons.splice(idx, 1)
|
|
if (this.selectedId === id) this.selectedId = null
|
|
},
|
|
deleteTextBox(id) {
|
|
const idx = this.currentSubContent.textBoxes.findIndex(t => t.id === id)
|
|
if (idx >= 0) this.currentSubContent.textBoxes.splice(idx, 1)
|
|
if (this.selectedId === id) this.selectedId = null
|
|
},
|
|
syncTextBoxContent() {
|
|
if (!this.$refs.canvas) return
|
|
const inputs = this.$refs.canvas.querySelectorAll('.textbox-input')
|
|
this.currentSubContent.textBoxes.forEach((box, idx) => {
|
|
const el = inputs[idx]
|
|
if (el && document.activeElement !== el && el.innerText !== box.text) {
|
|
el.innerText = box.text
|
|
}
|
|
})
|
|
},
|
|
syncFromDomToData() {
|
|
if (!this.$refs.canvas) return
|
|
const inputs = this.$refs.canvas.querySelectorAll('.textbox-input')
|
|
const boxes = this.currentSubContent && this.currentSubContent.textBoxes
|
|
if (boxes) {
|
|
boxes.forEach((box, idx) => {
|
|
const el = inputs[idx]
|
|
if (el) box.text = (el.innerText || '').trim()
|
|
})
|
|
}
|
|
},
|
|
onKeydown(e) {
|
|
if (!this.selectedId) return
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
if (document.activeElement?.contentEditable === 'true') return
|
|
e.preventDefault()
|
|
const icon = this.currentSubContent.icons.find(i => i.id === this.selectedId)
|
|
const box = this.currentSubContent.textBoxes.find(t => t.id === this.selectedId)
|
|
if (icon) this.deleteIcon(icon.id)
|
|
else if (box) this.deleteTextBox(box.id)
|
|
}
|
|
},
|
|
startRotateTextBox(e, box) {
|
|
e.stopPropagation()
|
|
const rect = this.$refs.canvas.getBoundingClientRect()
|
|
const cx = box.x + box.width / 2
|
|
const cy = box.y + box.height / 2
|
|
const startAngle = Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx)
|
|
const startRot = box.rotation || 0
|
|
const onMove = (ev) => {
|
|
const angle = Math.atan2(ev.clientY - rect.top - cy, ev.clientX - rect.left - cx)
|
|
const delta = ((angle - startAngle) * 180 / Math.PI)
|
|
box.rotation = (startRot + delta + 360) % 360
|
|
}
|
|
const onUp = () => {
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
}
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pagination-left,
|
|
.pagination-right {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
cursor: pointer;
|
|
}
|
|
.pagination-left {
|
|
left: 0;
|
|
}
|
|
.pagination-right {
|
|
right: 0;
|
|
}
|
|
.pagination-btn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 12px 8px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
color: #165dff;
|
|
transition: all 0.2s;
|
|
}
|
|
.pagination-btn:hover:not(.disabled) {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
border-color: rgba(22, 93, 255, 0.3);
|
|
}
|
|
.pagination-btn.disabled {
|
|
color: #cbd5e1;
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
.pagination-btn i {
|
|
font-size: 18px;
|
|
}
|
|
.pagination-btn-circle {
|
|
width: 36px;
|
|
height: 36px;
|
|
padding: 0;
|
|
border-radius: 50%;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
}
|
|
.pagination-btn-circle i {
|
|
font-size: 16px;
|
|
}
|
|
.pagination-indicator,
|
|
.pagination-indicator-clickable {
|
|
position: absolute;
|
|
bottom: 12px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 4px 12px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
color: #666;
|
|
z-index: 99;
|
|
}
|
|
.pagination-indicator-clickable {
|
|
cursor: pointer;
|
|
}
|
|
.pagination-indicator-clickable:hover {
|
|
background: rgba(255, 255, 255, 1);
|
|
border-color: rgba(22, 93, 255, 0.3);
|
|
color: #165dff;
|
|
}
|
|
.step-canvas-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
.task-canvas {
|
|
flex: 1;
|
|
position: relative;
|
|
min-height: 200px;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.task-canvas.insert-icon,
|
|
.task-canvas.insert-textbox {
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.canvas-icon {
|
|
position: absolute;
|
|
cursor: default;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.canvas-icon.selected {
|
|
border-color: #165dff;
|
|
z-index: 10;
|
|
}
|
|
|
|
.icon-body {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: transparent;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.icon-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.icon-placeholder {
|
|
font-size: 24px;
|
|
color: #165dff;
|
|
}
|
|
|
|
.delete-handle {
|
|
position: absolute;
|
|
top: -28px;
|
|
right: -4px;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #fff;
|
|
border: 1px solid #999;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
color: #f56c6c;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.delete-handle:hover {
|
|
background: #f56c6c;
|
|
color: #fff;
|
|
border-color: #f56c6c;
|
|
}
|
|
|
|
.textbox-delete {
|
|
top: -28px;
|
|
right: -4px;
|
|
}
|
|
|
|
.icon-resize-handle {
|
|
position: absolute;
|
|
left: -4px;
|
|
right: -4px;
|
|
top: -4px;
|
|
bottom: -4px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.icon-resize-handle .handle {
|
|
position: absolute;
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #165dff;
|
|
border: 1px solid #fff;
|
|
border-radius: 2px;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.icon-resize-handle .handle.nw { left: -4px; top: -4px; cursor: nw-resize; }
|
|
.icon-resize-handle .handle.n { left: 50%; top: -4px; margin-left: -4px; cursor: n-resize; }
|
|
.icon-resize-handle .handle.ne { right: -4px; top: -4px; cursor: ne-resize; }
|
|
.icon-resize-handle .handle.e { right: -4px; top: 50%; margin-top: -4px; cursor: e-resize; }
|
|
.icon-resize-handle .handle.se { right: -4px; bottom: -4px; cursor: se-resize; }
|
|
.icon-resize-handle .handle.s { left: 50%; bottom: -4px; margin-left: -4px; cursor: s-resize; }
|
|
.icon-resize-handle .handle.sw { left: -4px; bottom: -4px; cursor: sw-resize; }
|
|
.icon-resize-handle .handle.w { left: -4px; top: 50%; margin-top: -4px; cursor: w-resize; }
|
|
|
|
.rotate-handle {
|
|
position: absolute;
|
|
top: -24px;
|
|
left: 50%;
|
|
margin-left: -6px;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #fff;
|
|
border: 2px solid #165dff;
|
|
border-radius: 50%;
|
|
cursor: grab;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.canvas-textbox {
|
|
position: absolute;
|
|
cursor: default;
|
|
border: 1px solid #333;
|
|
background: transparent;
|
|
overflow: visible;
|
|
}
|
|
|
|
.canvas-textbox.selected {
|
|
border: 1px dashed #333;
|
|
z-index: 10;
|
|
}
|
|
|
|
.textbox-resize-handle {
|
|
position: absolute;
|
|
left: -6px;
|
|
right: -6px;
|
|
top: -6px;
|
|
bottom: -6px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.textbox-resize-handle .handle {
|
|
position: absolute;
|
|
width: 10px;
|
|
height: 10px;
|
|
background: #fff;
|
|
border: 1px solid #999;
|
|
border-radius: 50%;
|
|
pointer-events: auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.textbox-resize-handle .handle.nw { left: -5px; top: -5px; cursor: nw-resize; }
|
|
.textbox-resize-handle .handle.n { left: 50%; top: -5px; margin-left: -5px; cursor: n-resize; }
|
|
.textbox-resize-handle .handle.ne { right: -5px; top: -5px; cursor: ne-resize; }
|
|
.textbox-resize-handle .handle.e { right: -5px; top: 50%; margin-top: -5px; cursor: e-resize; }
|
|
.textbox-resize-handle .handle.se { right: -5px; bottom: -5px; cursor: se-resize; }
|
|
.textbox-resize-handle .handle.s { left: 50%; bottom: -5px; margin-left: -5px; cursor: s-resize; }
|
|
.textbox-resize-handle .handle.sw { left: -5px; bottom: -5px; cursor: sw-resize; }
|
|
.textbox-resize-handle .handle.w { left: -5px; top: 50%; margin-top: -5px; cursor: w-resize; }
|
|
|
|
.textbox-rotate-handle {
|
|
position: absolute;
|
|
top: -28px;
|
|
left: 50%;
|
|
margin-left: -6px;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #fff;
|
|
border: 1px solid #999;
|
|
border-radius: 50%;
|
|
cursor: grab;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.textbox-drag-bar {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 8px;
|
|
cursor: default;
|
|
background: transparent;
|
|
}
|
|
|
|
.textbox-input-wrapper {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
}
|
|
|
|
.textbox-placeholder {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
padding: 8px;
|
|
outline: none;
|
|
overflow: hidden;
|
|
background: transparent;
|
|
color: #999;
|
|
cursor: text;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.textbox-input {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
padding: 8px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
overflow: auto;
|
|
background: transparent;
|
|
color: #333;
|
|
}
|
|
|
|
.textbox-input::selection {
|
|
background: #c8c8c8;
|
|
color: inherit;
|
|
}
|
|
|
|
.textbox-format-toolbar-fixed {
|
|
position: fixed;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
color: #333;
|
|
background: rgba(255, 255, 255, 0.72);
|
|
-webkit-backdrop-filter: blur(15px);
|
|
backdrop-filter: blur(15px);
|
|
border: 1px solid rgba(22, 93, 255, 0.12);
|
|
border-radius: 14px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
z-index: 10000;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.textbox-format-toolbar-fixed .format-font { width: 120px; }
|
|
.textbox-format-toolbar-fixed .format-size { width: 70px; }
|
|
.textbox-format-toolbar-fixed .format-color { vertical-align: middle; }
|
|
.textbox-format-toolbar-fixed .format-color ::v-deep .el-color-picker__trigger {
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 2px;
|
|
}
|
|
|
|
.drawing-textbox {
|
|
position: absolute;
|
|
border: 2px dashed #165dff;
|
|
background: rgba(22, 93, 255, 0.05);
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
/* 页码预览弹窗(popper 在 body,需非 scoped) */
|
|
.page-preview-popover {
|
|
.page-preview-list {
|
|
max-height: 280px;
|
|
overflow-y: auto;
|
|
}
|
|
.page-preview-title {
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.page-preview-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 10px;
|
|
border-radius: 6px;
|
|
margin-bottom: 4px;
|
|
transition: background 0.2s;
|
|
}
|
|
.page-preview-item:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
.page-preview-item.active {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
color: #165dff;
|
|
}
|
|
.page-preview-label {
|
|
flex: 1;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
.page-preview-meta {
|
|
margin-left: 8px;
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
}
|
|
.page-preview-item.active .page-preview-meta {
|
|
color: rgba(22, 93, 255, 0.7);
|
|
}
|
|
.page-delete-btn {
|
|
padding: 4px 8px;
|
|
color: #f56c6c;
|
|
}
|
|
.page-delete-btn:hover:not(:disabled) {
|
|
color: #f56c6c;
|
|
background: rgba(245, 108, 108, 0.1);
|
|
}
|
|
.page-delete-btn:disabled {
|
|
color: #cbd5e1;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
</style>
|
|
|