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.
 
 
 
 
 

996 lines
32 KiB

<template>
<div
v-show="visible"
class="sixsteps-overlay"
:class="{ 'is-draggable': draggable }"
>
<div class="overlay-main">
<div class="overlay-header" ref="headerRef">
<div class="header-left">
<span class="overlay-title">{{ currentStepTitle }}</span>
<!-- 理解步骤:子标题(点名、接收解析任务、XXXX、XXXX)放在理解和插入之间 -->
<template v-if="currentStepIndex === 0 && !overrideTitle">
<div
v-for="(sub, idx) in understandingSubTitles"
:key="idx"
class="header-sub-title"
:class="{ active: activeUnderstandingSubIndex === idx }"
@click="activeUnderstandingSubIndex = idx"
@contextmenu.prevent="onSubTitleContextMenu('understanding', idx, $event)"
>
{{ sub }}
</div>
<el-dropdown trigger="click" @command="handleUnderstandingInsertCommand" class="header-insert">
<el-button size="small" type="primary" plain>
<i class="el-icon-plus"></i> 插入
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="subTitle">
<i class="el-icon-menu"></i> 小标题
</el-dropdown-item>
<el-dropdown-item command="background">
<i class="el-icon-picture-outline"></i> 背景
</el-dropdown-item>
<el-dropdown-item command="removeBackground" :disabled="!sixStepsSharedBackground">
<i class="el-icon-delete"></i> 删除背景
</el-dropdown-item>
<el-dropdown-item command="icon">
<i class="el-icon-s-opportunity"></i> 图标
</el-dropdown-item>
<el-dropdown-item command="textbox">
<i class="el-icon-edit-outline"></i> 文本框
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<!-- 后五步(判断、规划、准备、执行、评估):小标题 + 插入按钮 -->
<template v-else-if="currentStepIndex >= 1 && currentStepIndex <= 5">
<div
v-for="(sub, idx) in getStepContent(currentStepIndex).subTitles"
:key="idx"
class="header-sub-title"
:class="{ active: activeStepSubIndex === idx }"
@click="activeStepSubIndex = idx"
@contextmenu.prevent="onSubTitleContextMenu('step', idx, $event)"
>
{{ sub }}
</div>
<el-dropdown trigger="click" @command="handleStepInsertCommand" class="header-insert">
<el-button size="small" type="primary" plain>
<i class="el-icon-plus"></i> 插入
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="subTitle">
<i class="el-icon-menu"></i> 小标题
</el-dropdown-item>
<el-dropdown-item command="background">
<i class="el-icon-picture-outline"></i> 背景
</el-dropdown-item>
<el-dropdown-item command="removeBackground" :disabled="!sixStepsSharedBackground">
<i class="el-icon-delete"></i> 删除背景
</el-dropdown-item>
<el-dropdown-item command="icon">
<i class="el-icon-s-opportunity"></i> 图标
</el-dropdown-item>
<el-dropdown-item command="textbox">
<i class="el-icon-edit-outline"></i> 文本框
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<!-- 任务页:小标题 + 插入按钮 -->
<template v-else-if="overrideTitle === '任务'">
<div
v-for="(sub, idx) in taskSubTitles"
:key="idx"
class="header-sub-title"
:class="{ active: activeTaskSubIndex === idx }"
@click="activeTaskSubIndex = idx"
@contextmenu.prevent="onSubTitleContextMenu('task', idx, $event)"
>
{{ sub }}
</div>
<el-dropdown trigger="click" @command="handleInsertCommand" class="header-insert">
<el-button size="small" type="primary" plain>
<i class="el-icon-plus"></i> 插入
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="subTitle">
<i class="el-icon-menu"></i> 小标题
</el-dropdown-item>
<el-dropdown-item command="background">
<i class="el-icon-picture-outline"></i> 背景
</el-dropdown-item>
<el-dropdown-item command="removeBackground" :disabled="!taskPageBackground">
<i class="el-icon-delete"></i> 删除背景
</el-dropdown-item>
<el-dropdown-item command="icon">
<i class="el-icon-s-opportunity"></i> 图标
</el-dropdown-item>
<el-dropdown-item command="textbox">
<i class="el-icon-edit-outline"></i> 文本框
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</div>
</div>
<div class="overlay-body" :style="overlayBodyStyle">
<div class="overlay-content" :class="{
'task-page': overrideTitle === '任务',
'understanding-page': currentStepIndex === 0 && !overrideTitle,
'step-page': currentStepIndex >= 1 && currentStepIndex <= 5
}">
<!-- 任务页插入工具栏 + 可编辑画布 -->
<task-page-content
v-if="overrideTitle === '任务'"
ref="taskPage"
:room-id="roomId"
:background-image="taskPageBackground"
:task-sub-titles="taskSubTitles"
@background-change="taskPageBackground = $event"
@task-sub-titles-change="taskSubTitles = $event"
@save-request="debouncedSave"
@task-page-data="lastTaskPageData = $event; saveToRedis()"
class="task-page-body"
/>
<!-- 理解步骤:4 子标题 + 可编辑画布 -->
<understanding-step-content
v-else-if="currentStepIndex === 0"
ref="understandingStep"
:background-image="sixStepsSharedBackground"
:active-sub-index="activeUnderstandingSubIndex"
:sub-titles="understandingSubTitles"
@background-change="sixStepsSharedBackground = $event"
@save-request="debouncedSave"
@understanding-data="lastUnderstandingData = $event; saveToRedis()"
class="understanding-page-body"
/>
<!-- 判断、规划、准备、执行、评估:可编辑画布,六步共享背景 -->
<step-canvas-content
v-else-if="currentStepIndex >= 1 && currentStepIndex <= 5"
ref="stepCanvas"
:key="currentStepIndex"
:content="getStepContent(currentStepIndex)"
:active-sub-index="activeStepSubIndex"
:background-image="sixStepsSharedBackground"
@background-change="sixStepsSharedBackground = $event"
class="step-canvas-body"
/>
<div v-else class="blank-placeholder">
<i class="el-icon-document"></i>
<p>{{ currentStepTitle }} - 内容区域</p>
<p class="hint">此处为空白页,后续可添加具体功能</p>
</div>
</div>
</div>
</div>
<!-- 小标题右键菜单 -->
<div
v-if="subTitleContextMenu.visible"
ref="subTitleContextMenuRef"
class="sub-title-context-menu"
:style="{ left: subTitleContextMenu.x + 'px', top: subTitleContextMenu.y + 'px' }"
@click.stop
>
<div class="context-menu-item" @click="editSubTitle">
<i class="el-icon-edit"></i>
<span>编辑</span>
</div>
<div class="context-menu-item" @click="insertNewPage">
<i class="el-icon-document-add"></i>
<span>插入新的一页</span>
</div>
<div class="context-menu-item context-menu-item-danger" @click="deleteSubTitle">
<i class="el-icon-delete"></i>
<span>删除</span>
</div>
</div>
<!-- 右侧栏:任务 + 1-6 步,垂直排列 -->
<div class="overlay-sidebar">
<div class="sidebar-steps">
<div
class="sidebar-task"
:class="{ active: taskBlockActive }"
@click="$emit('select-task')"
>
<span>任务</span>
</div>
<div
v-for="(step, index) in sixStepsData"
:key="index"
class="sidebar-step"
:class="{ active: step.active, completed: step.completed }"
@click="$emit('select-step', index)"
>
<div class="sidebar-step-num">{{ index + 1 }}</div>
<span class="sidebar-step-title">{{ step.title }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import TaskPageContent from './TaskPageContent.vue'
import UnderstandingStepContent from './UnderstandingStepContent.vue'
import StepCanvasContent from './StepCanvasContent.vue'
import {
createRollCallTextBoxes,
createSubTitleTemplate,
createIntentBriefingTemplate,
createTaskPlanningTemplate,
createSimpleTitleTemplate,
SUB_TITLE_TEMPLATE_NAMES,
SIMPLE_TITLE_NAMES
} from './rollCallTemplate'
import { ensurePagesStructure, createEmptySubContent } from './subContentPages'
import { getSixStepsData, saveSixStepsData, getTaskPageData } from '@/api/system/routes'
export default {
name: 'SixStepsOverlay',
components: { TaskPageContent, UnderstandingStepContent, StepCanvasContent },
props: {
roomId: {
type: [Number, String],
default: null
},
visible: {
type: Boolean,
default: false
},
currentStepIndex: {
type: Number,
default: 0
},
stepTitles: {
type: Array,
default: () => ['理解', '判断', '规划', '准备', '执行', '评估']
},
/** 覆盖标题,如「任务」独立步骤 */
overrideTitle: {
type: String,
default: null
},
sixStepsData: {
type: Array,
default: () => []
},
taskBlockActive: {
type: Boolean,
default: false
},
initialActiveUnderstandingSubIndex: { type: Number, default: 0 },
initialActiveTaskSubIndex: { type: Number, default: 0 },
initialActiveStepSubIndex: { type: Number, default: 0 },
draggable: {
type: Boolean,
default: true
}
},
data() {
return {
taskPageBackground: null,
sixStepsSharedBackground: null,
understandingSubTitles: ['点名', '任务目标', '自身任务', '对接任务'],
activeUnderstandingSubIndex: this.initialActiveUnderstandingSubIndex,
taskSubTitles: [],
activeTaskSubIndex: this.initialActiveTaskSubIndex,
activeStepSubIndex: this.initialActiveStepSubIndex,
stepContents: {},
subTitleContextMenu: {
visible: false,
x: 0,
y: 0,
target: null,
index: -1
},
_saveTimer: null,
lastTaskPageData: null,
lastUnderstandingData: null,
taskPageLoadComplete: false,
_isRestoring: false
}
},
created() {
this._isRestoring = this.initialActiveUnderstandingSubIndex !== 0 || this.initialActiveTaskSubIndex !== 0 || this.initialActiveStepSubIndex !== 0
},
watch: {
currentStepIndex(val) {
if (this._isRestoring) {
this.$nextTick(() => { this._isRestoring = false })
return
}
this.activeStepSubIndex = 0
if (val === 0 && !this.overrideTitle) {
this.$nextTick(() => {
if (this.$refs.understandingStep && this.lastUnderstandingData) {
this.$refs.understandingStep.loadFromData(this.lastUnderstandingData)
}
})
}
},
overrideTitle(val) {
if (this._isRestoring) {
this.$nextTick(() => { this._isRestoring = false })
return
}
this.activeTaskSubIndex = 0
if (val === '任务') {
this.taskPageLoadComplete = false
this.$nextTick(() => {
if (this.$refs.taskPage) {
if (this.lastTaskPageData) {
this.$refs.taskPage.loadFromData(this.lastTaskPageData)
}
this.loadFromRedis()
}
})
}
if (!val && this.currentStepIndex === 0) {
this.$nextTick(() => {
if (this.$refs.understandingStep) {
if (this.lastUnderstandingData) {
this.$refs.understandingStep.loadFromData(this.lastUnderstandingData)
}
this.loadFromRedis()
}
})
}
},
roomId: {
handler(val) {
if (val != null && this.visible) this.loadFromRedis()
},
immediate: false
},
visible: {
handler(val) {
if (val && this.roomId != null) {
if (this.overrideTitle === '任务') this.taskPageLoadComplete = false
this.loadFromRedis()
}
},
immediate: false
},
taskPageBackground: { handler() { this.debouncedSave() }, deep: false },
sixStepsSharedBackground: { handler() { this.debouncedSave() }, deep: false },
understandingSubTitles: { handler() { this.debouncedSave() }, deep: true },
taskSubTitles: { handler() { this.debouncedSave() }, deep: true },
stepContents: { handler() { this.debouncedSave() }, deep: true }
},
mounted() {
document.addEventListener('click', this.onDocumentClickForSubTitleMenu)
if (this.roomId != null && this.visible) {
if (this.overrideTitle === '任务') this.taskPageLoadComplete = false
this.loadFromRedis()
}
},
beforeDestroy() {
document.removeEventListener('click', this.onDocumentClickForSubTitleMenu)
if (this._saveTimer) {
clearTimeout(this._saveTimer)
this._saveTimer = null
}
this.saveToRedis()
},
computed: {
currentStepTitle() {
if (this.overrideTitle) return this.overrideTitle
return this.stepTitles[this.currentStepIndex] || '六步法'
},
overlayBodyStyle() {
if (this.overrideTitle === '任务') return {}
if (this.currentStepIndex >= 1 && this.currentStepIndex <= 5) return {}
return {}
}
},
methods: {
getProgress() {
return {
activeUnderstandingSubIndex: this.activeUnderstandingSubIndex,
activeTaskSubIndex: this.activeTaskSubIndex,
activeStepSubIndex: this.activeStepSubIndex
}
},
close() {
this.$emit('close', this.getProgress())
},
async loadFromRedis() {
if (this.roomId == null) return
try {
let res = await getSixStepsData({ roomId: this.roomId })
let data = res && res.data
if (!data) {
res = await getTaskPageData({ roomId: this.roomId })
data = res && res.data
if (data) {
const tp = typeof data === 'string' ? (() => { try { return JSON.parse(data) } catch (_) { return null } })() : data
if (tp) data = { taskPage: tp }
}
}
if (!data) return
const raw = typeof data === 'string' ? (() => { try { return JSON.parse(data) } catch (_) { return null } })() : data
if (!raw) return
if (raw.taskPage) {
this.taskPageBackground = raw.taskPage.background || null
if (Array.isArray(raw.taskPage.taskSubTitles)) this.taskSubTitles = raw.taskPage.taskSubTitles
this.lastTaskPageData = raw.taskPage
this.taskPageLoadComplete = true
this.$nextTick(() => {
if (this.$refs.taskPage) this.$refs.taskPage.loadFromData(raw.taskPage)
})
} else if (this.overrideTitle === '任务') {
this.taskPageLoadComplete = true
}
if (raw.sixStepsSharedBackground) this.sixStepsSharedBackground = raw.sixStepsSharedBackground
if (Array.isArray(raw.understanding?.subTitles)) this.understandingSubTitles = raw.understanding.subTitles
if (raw.understanding?.subContents) {
this.lastUnderstandingData = raw.understanding
this.$nextTick(() => {
if (this.$refs.understandingStep) this.$refs.understandingStep.loadFromData(raw.understanding)
})
}
if (raw.steps && typeof raw.steps === 'object') {
Object.keys(raw.steps).forEach(k => {
const step = raw.steps[k]
if (step && (step.subTitles || step.subContents)) {
this.$set(this.stepContents, parseInt(k, 10), {
subTitles: step.subTitles || [],
subContents: (step.subContents || []).map(sc => {
ensurePagesStructure(sc)
return sc
})
})
}
})
}
} catch (e) {
console.warn('SixSteps loadFromRedis failed:', e)
} finally {
if (this.overrideTitle === '任务') this.taskPageLoadComplete = true
}
},
saveToRedis() {
if (this.roomId == null) return
let taskPageData
if (this.$refs.taskPage) {
if (this.taskPageLoadComplete) {
taskPageData = this.lastTaskPageData = this.$refs.taskPage.getDataForSave()
} else {
taskPageData = this.lastTaskPageData || { background: this.taskPageBackground, icons: [], textBoxes: [], taskSubTitles: this.taskSubTitles }
}
} else {
taskPageData = this.lastTaskPageData || { background: this.taskPageBackground, icons: [], textBoxes: [], taskSubTitles: this.taskSubTitles }
}
const payload = {
taskPage: taskPageData,
sixStepsSharedBackground: this.sixStepsSharedBackground,
understanding: (() => {
if (this.$refs.understandingStep) {
this.lastUnderstandingData = { subTitles: this.understandingSubTitles, subContents: this.$refs.understandingStep.getDataForSave() }
}
return {
subTitles: this.understandingSubTitles,
subContents: this.lastUnderstandingData?.subContents || []
}
})(),
steps: {}
}
Object.keys(this.stepContents).forEach(k => {
const step = this.stepContents[k]
if (step && (step.subTitles || step.subContents)) {
payload.steps[k] = {
subTitles: step.subTitles || [],
subContents: (step.subContents || []).map(sc => {
ensurePagesStructure(sc)
return {
pages: (sc.pages || []).map(p => ({
icons: (p.icons || []).map(i => ({ id: i.id, x: i.x, y: i.y, width: i.width, height: i.height, rotation: i.rotation || 0, src: i.src })),
textBoxes: (p.textBoxes || []).map(t => ({ id: t.id, x: t.x, y: t.y, width: t.width, height: t.height, text: t.text || '', placeholder: t.placeholder, rotation: t.rotation || 0, fontSize: t.fontSize, fontFamily: t.fontFamily, color: t.color, fontWeight: t.fontWeight }))
})),
currentPageIndex: sc.currentPageIndex || 0
}
})
}
}
})
saveSixStepsData({
roomId: this.roomId,
data: JSON.stringify(payload)
}).catch(e => {
console.warn('SixSteps saveToRedis failed:', e)
})
},
debouncedSave() {
if (this._saveTimer) clearTimeout(this._saveTimer)
this._saveTimer = setTimeout(() => {
this._saveTimer = null
this.saveToRedis()
}, 300)
},
getStepContent(stepIndex) {
let step = this.stepContents[stepIndex]
if (step) {
if (!step.subContents && step.subTitles) {
step.subContents = step.subTitles.map(() => createEmptySubContent())
if (step.icons?.length || step.textBoxes?.length) {
step.subContents[0] = createEmptySubContent()
step.subContents[0].pages[0] = { icons: step.icons || [], textBoxes: step.textBoxes || [] }
}
}
return step
}
{
const defaultSubTitles = stepIndex === 1
? ['相关规定', '敌情', '意图通报', '威胁判断']
: stepIndex === 2
? ['任务规划', '职责分工', '点名', '第一次进度检查', '点名', '第二次进度检查', '点名', '产品生成']
: stepIndex === 3
? ['点名', '集体协同']
: stepIndex === 4
? ['任务执行']
: stepIndex === 5
? ['评估']
: []
this.$set(this.stepContents, stepIndex, {
subTitles: defaultSubTitles,
subContents: defaultSubTitles.map(() => createEmptySubContent())
})
return this.stepContents[stepIndex]
}
},
async addSubTitle(target) {
try {
const { value } = await this.$prompt('请输入小标题名称', '插入小标题', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: '新标题',
inputPattern: /\S+/,
inputErrorMessage: '请输入小标题名称'
})
if (value && value.trim()) {
if (target === 'understanding') {
this.understandingSubTitles.push(value.trim())
} else if (target === 'task') {
this.taskSubTitles.push(value.trim())
} else if (target === 'step') {
const step = this.getStepContent(this.currentStepIndex)
step.subTitles.push(value.trim())
step.subContents.push(createEmptySubContent())
}
}
} catch (_) {}
},
handleInsertCommand(cmd) {
if (cmd === 'subTitle') {
this.addSubTitle('task')
return
}
if (this.$refs.taskPage) {
this.$refs.taskPage.handleInsertCommand(cmd)
}
},
handleUnderstandingInsertCommand(cmd) {
if (cmd === 'subTitle') {
this.addSubTitle('understanding')
return
}
if (this.$refs.understandingStep) {
this.$refs.understandingStep.handleInsertCommand(cmd)
}
},
handleStepInsertCommand(cmd) {
if (cmd === 'subTitle') {
this.addSubTitle('step')
return
}
if (this.$refs.stepCanvas) {
this.$refs.stepCanvas.handleInsertCommand(cmd)
}
},
onSubTitleContextMenu(target, index, event) {
const arr = target === 'understanding' ? this.understandingSubTitles
: target === 'task' ? this.taskSubTitles
: this.getStepContent(this.currentStepIndex).subTitles
const sourceName = arr && arr[index] ? arr[index] : ''
this.subTitleContextMenu = {
visible: true,
x: event.clientX,
y: event.clientY,
target,
index,
sourceName
}
},
closeSubTitleContextMenu() {
this.subTitleContextMenu.visible = false
},
onDocumentClickForSubTitleMenu(e) {
if (!this.subTitleContextMenu.visible) return
const el = this.$refs.subTitleContextMenuRef
if (el && el.contains(e.target)) return
this.closeSubTitleContextMenu()
},
getSubTitleArray(target) {
const t = target ?? this.subTitleContextMenu?.target
if (t === 'understanding') return this.understandingSubTitles
if (t === 'task') return this.taskSubTitles
if (t === 'step') return this.getStepContent(this.currentStepIndex).subTitles
return []
},
async editSubTitle() {
const { target, index } = this.subTitleContextMenu
const arr = this.getSubTitleArray(target)
this.closeSubTitleContextMenu()
if (index < 0 || index >= arr.length) return
try {
const { value } = await this.$prompt('请输入小标题名称', '编辑小标题', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: arr[index],
inputPattern: /\S+/,
inputErrorMessage: '请输入小标题名称'
})
if (value && value.trim()) {
this.$set(arr, index, value.trim())
}
} catch (_) {}
},
getTemplateBoxes(sourceName, newName, canvas) {
const w = canvas ? canvas.offsetWidth : 800
const h = canvas ? canvas.offsetHeight : 500
if (sourceName === '点名') return createRollCallTextBoxes(w, h)
if (sourceName === '意图通报') return createIntentBriefingTemplate(w, h)
if (sourceName === '任务规划') return createTaskPlanningTemplate(w, h)
if (SIMPLE_TITLE_NAMES.includes(sourceName)) return createSimpleTitleTemplate(newName, w, h)
if (SUB_TITLE_TEMPLATE_NAMES.includes(sourceName)) return createSubTitleTemplate(newName, w, h)
return []
},
async insertNewPage() {
const { target, index, sourceName } = this.subTitleContextMenu
this.closeSubTitleContextMenu()
if (!sourceName) return
const subIdx = index
if (target === 'task') {
this.$message.info('任务页暂不支持多页')
return
}
if (target === 'understanding') {
this.$nextTick(() => {
const canvas = this.$refs.understandingStep?.$refs?.canvas
const boxes = this.getTemplateBoxes(sourceName, sourceName, canvas)
if (this.$refs.understandingStep) {
this.$refs.understandingStep.addPageToSubIndex(subIdx, boxes)
}
})
} else if (target === 'step') {
const step = this.getStepContent(this.currentStepIndex)
const sub = step.subContents[subIdx]
if (!sub) return
ensurePagesStructure(sub)
const canvas = this.$refs.stepCanvas?.$refs?.canvas
const boxes = this.getTemplateBoxes(sourceName, sourceName, canvas)
sub.pages.push({ icons: [], textBoxes: [...boxes] })
sub.currentPageIndex = sub.pages.length - 1
}
},
deleteSubTitle() {
const { target, index } = { ...this.subTitleContextMenu }
this.closeSubTitleContextMenu()
const arr = this.getSubTitleArray(target)
if (index < 0 || index >= arr.length) return
arr.splice(index, 1)
if (target === 'step') {
const step = this.getStepContent(this.currentStepIndex)
if (step.subContents && step.subContents.length > index) {
step.subContents.splice(index, 1)
}
}
if (target === 'understanding') {
if (this.activeUnderstandingSubIndex >= arr.length && arr.length > 0) {
this.activeUnderstandingSubIndex = arr.length - 1
} else if (arr.length === 0) {
this.activeUnderstandingSubIndex = 0
} else if (this.activeUnderstandingSubIndex > index) {
this.activeUnderstandingSubIndex--
}
} else if (target === 'task') {
if (this.activeTaskSubIndex >= arr.length && arr.length > 0) {
this.activeTaskSubIndex = arr.length - 1
} else if (arr.length === 0) {
this.activeTaskSubIndex = 0
} else if (this.activeTaskSubIndex > index) {
this.activeTaskSubIndex--
}
} else if (target === 'step') {
if (this.activeStepSubIndex >= arr.length && arr.length > 0) {
this.activeStepSubIndex = arr.length - 1
} else if (arr.length === 0) {
this.activeStepSubIndex = 0
} else if (this.activeStepSubIndex > index) {
this.activeStepSubIndex--
}
}
}
}
}
</script>
<style scoped>
/* 悬浮窗:左侧顶到屏幕最左侧,右侧顶到屏幕最右侧,下边框与时间轴上边框对齐 */
.sixsteps-overlay {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 71px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(0, 0, 0, 0.06);
z-index: 999;
display: flex;
flex-direction: row;
overflow: hidden;
animation: overlayFadeIn 0.3s ease;
}
.overlay-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.overlay-sidebar {
width: 100px;
flex-shrink: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%);
border-left: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: 12px 8px;
}
.sidebar-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.sidebar-task {
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #1e293b;
text-align: center;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(226, 232, 240, 0.8);
transition: all 0.3s;
}
.sidebar-task:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(148, 163, 184, 0.4);
}
.sidebar-task.active {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.02) 100%);
border-color: rgba(59, 130, 246, 0.3);
color: #1e40af;
}
.sidebar-step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
cursor: pointer;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(226, 232, 240, 0.8);
transition: all 0.3s;
}
.sidebar-step:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(148, 163, 184, 0.4);
}
.sidebar-step.active {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.02) 100%);
border-color: rgba(59, 130, 246, 0.3);
}
.sidebar-step.completed {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.06) 0%, rgba(34, 197, 94, 0.01) 100%);
border-color: rgba(34, 197, 94, 0.25);
}
.sidebar-step-num {
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: #64748b;
flex-shrink: 0;
}
.sidebar-step.active .sidebar-step-num {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.sidebar-step.completed .sidebar-step-num {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
}
.sidebar-step-title {
font-size: 14px;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-step.active .sidebar-step-title {
color: #1e40af;
}
@keyframes overlayFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.overlay-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, rgba(0, 138, 255, 0.08) 0%, rgba(0, 138, 255, 0.02) 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
cursor: default;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
margin-left: 2px;
}
.header-insert {
cursor: pointer;
}
.header-sub-title {
padding: 6px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
color: #64748b;
transition: all 0.2s;
}
.header-sub-title:hover {
background: rgba(0, 138, 255, 0.08);
color: #008aff;
}
.header-sub-title.active {
background: rgba(0, 138, 255, 0.12);
color: #008aff;
font-weight: 600;
}
.overlay-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.overlay-body {
flex: 1;
overflow: auto;
min-height: 200px;
}
.overlay-content {
padding: 24px;
min-height: 180px;
}
.overlay-content.task-page {
padding: 0;
height: 100%;
}
.overlay-content.understanding-page,
.overlay-content.step-page {
padding: 0;
height: 100%;
}
.task-page-body,
.understanding-page-body,
.step-canvas-body {
height: 100%;
}
.blank-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 160px;
color: #94a3b8;
text-align: center;
}
.blank-placeholder i {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.blank-placeholder p {
margin: 4px 0;
font-size: 14px;
}
.blank-placeholder .hint {
font-size: 12px;
color: #cbd5e1;
}
.sub-title-context-menu {
position: fixed;
z-index: 10000;
min-width: 120px;
padding: 4px 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(0, 0, 0, 0.08);
}
.sub-title-context-menu .context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 14px;
color: #1e293b;
cursor: pointer;
transition: background 0.2s;
}
.sub-title-context-menu .context-menu-item:hover {
background: rgba(0, 138, 255, 0.08);
color: #008aff;
}
.sub-title-context-menu .context-menu-item-danger:hover {
background: rgba(239, 68, 68, 0.08);
color: #ef4444;
}
</style>