|
|
@@ -104,45 +104,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="workflowStep === 3 && draftConverted" class="result-preview">
|
|
|
- <div :class="['draft-status', draftPreparationState]">
|
|
|
- <span class="draft-status-icon">{{ draftPreparationState === "ready" ? "✓" : draftPreparationState === "failed" ? "!" : "" }}</span>
|
|
|
- <div>
|
|
|
- <strong>{{ draftStatusTitle }}</strong>
|
|
|
- <span>{{ draftStatusDescription }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="draft-workspace">
|
|
|
- <div class="draft-page-list" aria-label="课件页面">
|
|
|
- <button
|
|
|
- v-for="(slide, index) in draftConverted.slides"
|
|
|
- :key="slide.id"
|
|
|
- type="button"
|
|
|
- :class="['draft-page-item', { active: selectedDraftPageIndex === index }]"
|
|
|
- @click="selectedDraftPageIndex = index"
|
|
|
- >
|
|
|
- <ThumbnailSlide :slide="slide" :size="148" />
|
|
|
- <span>{{ formatPageNo(index + 1) }} · {{ draftOutline?.pages[index]?.title || "未命名页面" }}</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="selectedDraftSlide" class="draft-preview-pane">
|
|
|
- <div class="draft-preview-toolbar">
|
|
|
- <div>
|
|
|
- <span>第{{ selectedDraftPageIndex + 1 }} / {{ draftConverted.slides.length }}页</span>
|
|
|
- <strong>{{ selectedDraftPage?.title || "课件页面" }}</strong>
|
|
|
- </div>
|
|
|
- <button type="button" :disabled="working" @click="regenerateDraftPage(selectedDraftPageIndex)">重新生成本页</button>
|
|
|
- </div>
|
|
|
- <div class="draft-canvas-shell">
|
|
|
- <ThumbnailSlide :slide="selectedDraftSlide" :size="680" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-else-if="workflowStep === 2 && outline" class="outline">
|
|
|
+ <div v-else-if="workflowStep === 2 && outline" class="outline unified-review">
|
|
|
<div class="sample-stage-head">
|
|
|
<div class="sample-template-current">
|
|
|
<span>当前样式</span>
|
|
|
@@ -154,41 +116,6 @@
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- <div class="sample-carousel-shell">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="sample-carousel-arrow previous"
|
|
|
- aria-label="查看前面的页面"
|
|
|
- :disabled="!canScrollSamplePrevious"
|
|
|
- @click="scrollSampleCarousel(-1)"
|
|
|
- >
|
|
|
- ‹
|
|
|
- </button>
|
|
|
- <div ref="sampleCarouselRef" class="sample-thumbnail-track" @scroll="updateSampleCarouselNavigation">
|
|
|
- <button
|
|
|
- v-for="(slide, index) in previewSlides"
|
|
|
- :key="slide.id"
|
|
|
- type="button"
|
|
|
- :class="['sample-thumbnail-item', { active: selectedPageNo === outline.pages[index]?.pageNo }]"
|
|
|
- :disabled="working"
|
|
|
- :title="outline.pages[index]?.title || `第${index + 1}页`"
|
|
|
- @click="selectSamplePage(outline.pages[index]?.pageNo || index + 1, index)"
|
|
|
- >
|
|
|
- <ThumbnailSlide :slide="slide" :size="116" />
|
|
|
- <span>{{ outline.pages[index]?.title || `第${index + 1}页` }}</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="sample-carousel-arrow next"
|
|
|
- aria-label="查看后面的页面"
|
|
|
- :disabled="!canScrollSampleNext"
|
|
|
- @click="scrollSampleCarousel(1)"
|
|
|
- >
|
|
|
- ›
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
<div v-if="templatePickerExpanded" class="compact-template-panel">
|
|
|
<div class="sample-template-picker">
|
|
|
<button
|
|
|
@@ -213,84 +140,75 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="sampleSlide && selectedPage" class="sample-detail-section">
|
|
|
- <div class="sample-detail-heading">
|
|
|
- <div>
|
|
|
- <span>第{{ samplePageNo }}页</span>
|
|
|
- <strong>{{ samplePageTitle }}</strong>
|
|
|
- </div>
|
|
|
- <small>{{ samplePageReason }}</small>
|
|
|
+ <div v-if="draftConverted" :class="['compact-process-state', draftPreparationState]">
|
|
|
+ <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
|
+ <span v-else class="process-state-icon">{{ draftPreparationState === "ready" ? "✓" : "!" }}</span>
|
|
|
+ <div>
|
|
|
+ <strong>{{ draftStatusTitle }}</strong>
|
|
|
+ <span>{{ draftStatusDescription }}</span>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="workspaceSlide" class="unified-review-workspace">
|
|
|
+ <aside class="workspace-page-list" aria-label="课件页面">
|
|
|
+ <button
|
|
|
+ v-for="(slide, index) in workspaceSlides"
|
|
|
+ :key="slide.id"
|
|
|
+ type="button"
|
|
|
+ :class="['workspace-page-item', { active: selectedWorkspacePageIndex === index }]"
|
|
|
+ :disabled="working"
|
|
|
+ @click="selectWorkspacePage(index)"
|
|
|
+ >
|
|
|
+ <ThumbnailSlide :slide="slide" :size="136" />
|
|
|
+ <span>{{ formatPageNo(index + 1) }} · {{ workspacePages[index]?.title || "未命名页面" }}</span>
|
|
|
+ </button>
|
|
|
+ </aside>
|
|
|
|
|
|
- <div class="sample-detail-layout">
|
|
|
- <section class="sample-outline-panel" aria-label="页面内容编辑">
|
|
|
- <div class="page-editor simplified-page-editor">
|
|
|
- <div class="editor-head">
|
|
|
- <div class="title-editor">
|
|
|
- <label class="outline-field-label">页面主题</label>
|
|
|
- <input v-model="selectedPage.title" class="page-title" :disabled="working" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="field-card">
|
|
|
- <div class="field-title">学生看到的内容</div>
|
|
|
- <input
|
|
|
- :value="studentHeadline"
|
|
|
- :disabled="working"
|
|
|
- class="headline-input"
|
|
|
- placeholder="这一页给学生看的核心标题"
|
|
|
- @input="event => updateStudentHeadline(selectedPage!, event)"
|
|
|
- />
|
|
|
- <textarea
|
|
|
- :value="studentPoints.join('\n')"
|
|
|
- :disabled="working"
|
|
|
- class="page-bullets"
|
|
|
- placeholder="每行一条学生可见要点,避免写老师操作步骤"
|
|
|
- @input="event => updateStudentPoints(selectedPage!, event)"
|
|
|
- ></textarea>
|
|
|
- <div v-if="studentContentReadabilityHint" class="content-readability-hint">
|
|
|
- {{ studentContentReadabilityHint }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <section class="workspace-preview-column" aria-label="页面预览及调整">
|
|
|
+ <div class="workspace-preview-heading">
|
|
|
+ <div>
|
|
|
+ <span>第{{ selectedWorkspacePageIndex + 1 }} / {{ workspaceSlides.length }}页</span>
|
|
|
+ <strong>{{ workspacePage?.title || "课件页面" }}</strong>
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="sample-preview-column" aria-label="页面预览及调整">
|
|
|
- <div class="sample-canvas">
|
|
|
- <ThumbnailSlide
|
|
|
- :slide="sampleSlide"
|
|
|
- :size="600"
|
|
|
- :show-background-error="true"
|
|
|
- @background-load-state="handleSampleBackgroundLoadState"
|
|
|
- />
|
|
|
+ <small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
|
|
|
+ </div>
|
|
|
+ <div class="workspace-canvas-shell">
|
|
|
+ <ThumbnailSlide
|
|
|
+ :slide="workspaceSlide"
|
|
|
+ :size="720"
|
|
|
+ :show-background-error="true"
|
|
|
+ :editable-text="true"
|
|
|
+ :edit-disabled="!canEditWorkspaceText"
|
|
|
+ @background-load-state="handleSampleBackgroundLoadState"
|
|
|
+ @text-change="handleWorkspaceTextChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
|
+ <div class="sample-feedback workspace-feedback">
|
|
|
+ <div class="feedback-label">图片或版式需要调整?</div>
|
|
|
+ <div class="feedback-chips">
|
|
|
+ <button
|
|
|
+ v-for="item in sampleFeedbackOptions"
|
|
|
+ :key="item"
|
|
|
+ type="button"
|
|
|
+ :class="{ active: sampleFeedbackTags.includes(item) }"
|
|
|
+ :disabled="working"
|
|
|
+ @click="toggleSampleFeedback(item)"
|
|
|
+ >
|
|
|
+ {{ item }}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
|
- <div class="sample-feedback">
|
|
|
- <div class="feedback-label">调整当前页面</div>
|
|
|
- <div class="feedback-chips">
|
|
|
- <button
|
|
|
- v-for="item in sampleFeedbackOptions"
|
|
|
- :key="item"
|
|
|
- type="button"
|
|
|
- :class="{ active: sampleFeedbackTags.includes(item) }"
|
|
|
- :disabled="working"
|
|
|
- @click="toggleSampleFeedback(item)"
|
|
|
- >
|
|
|
- {{ item }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div class="sample-feedback-action">
|
|
|
- <textarea
|
|
|
- v-model="sampleFeedbackText"
|
|
|
- :disabled="working"
|
|
|
- placeholder="也可以直接说明你希望怎么调整"
|
|
|
- @input="invalidateVisualApproval"
|
|
|
- ></textarea>
|
|
|
- <Button @click="regenerateSamplePage" :disabled="working">调整当前页面</Button>
|
|
|
- </div>
|
|
|
+ <div class="sample-feedback-action">
|
|
|
+ <textarea
|
|
|
+ v-model="sampleFeedbackText"
|
|
|
+ :disabled="working"
|
|
|
+ placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"
|
|
|
+ @input="invalidateVisualApproval"
|
|
|
+ ></textarea>
|
|
|
+ <Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
|
</div>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -302,14 +220,14 @@
|
|
|
</template>
|
|
|
<template v-else-if="isStyleStep">
|
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
|
- <Button type="primary" @click="confirmSamplesAndGenerate" :disabled="working || sampleBackgroundState.status === 'error'"
|
|
|
- >使用此样式并生成课件</Button
|
|
|
+ <span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在自动检查并优化,无需操作…</span>
|
|
|
+ <Button
|
|
|
+ v-if="!draftConverted"
|
|
|
+ type="primary"
|
|
|
+ @click="confirmSamplesAndGenerate"
|
|
|
+ :disabled="working || sampleBackgroundState.status === 'error'"
|
|
|
+ >生成并自动优化</Button
|
|
|
>
|
|
|
- </template>
|
|
|
- <template v-else>
|
|
|
- <Button v-if="draftPreparationState !== 'optimizing'" @click="backToOutline">返回调整</Button>
|
|
|
- <span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在准备可用课件…</span>
|
|
|
- <Button v-else-if="draftPreparationState === 'failed'" type="primary" @click="retryAutomaticOptimization">重新自动优化</Button>
|
|
|
<Button v-else-if="draftPreparationState === 'ready'" type="primary" @click="applyDraft">应用到当前课件</Button>
|
|
|
</template>
|
|
|
</div>
|
|
|
@@ -318,7 +236,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { ref, reactive, computed, watch, nextTick } from "vue"
|
|
|
+import { ref, reactive, computed, watch } from "vue"
|
|
|
import { ElMessage } from "element-plus"
|
|
|
import Modal from "@/components/Modal.vue"
|
|
|
import Button from "@/components/Button.vue"
|
|
|
@@ -368,13 +286,12 @@ const { addHistorySnapshot } = useHistorySnapshot()
|
|
|
const extraPrompt = ref("")
|
|
|
const outline = ref<AiCoursewareOutline>()
|
|
|
const taskInfo = ref<AiCoursewareTaskInfo>()
|
|
|
-const workflowStep = ref<1 | 2 | 3>(1)
|
|
|
+const workflowStep = ref<1 | 2>(1)
|
|
|
const isSetupStep = computed(() => workflowStep.value === 1)
|
|
|
const isStyleStep = computed(() => workflowStep.value === 2)
|
|
|
const workflowSteps = [
|
|
|
{ value: 1, label: "设置需求" },
|
|
|
- { value: 2, label: "选择样式" },
|
|
|
- { value: 3, label: "预览并应用" }
|
|
|
+ { value: 2, label: "预览、编辑并应用" }
|
|
|
] as const
|
|
|
const taskMode = ref<"full" | "sample">("full")
|
|
|
const coursewareTemplates = AI_COURSEWARE_TEMPLATES
|
|
|
@@ -398,7 +315,7 @@ const preferences = reactive<AiCoursewarePreferences>({
|
|
|
})
|
|
|
const approvedSamplePages = ref<number[]>([])
|
|
|
const sampleBackgroundState = ref<{ status: "none" | "loading" | "loaded" | "error"; url: string }>({ status: "none", url: "" })
|
|
|
-const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "文字太多"]
|
|
|
+const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "层级不清晰"]
|
|
|
const sampleFeedbackTags = ref<string[]>([])
|
|
|
const sampleFeedbackText = ref("")
|
|
|
const draftOutline = ref<AiCoursewareOutline>()
|
|
|
@@ -406,14 +323,21 @@ const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides:
|
|
|
const draftQuality = ref<AiPptQualityReport>()
|
|
|
const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
|
|
|
const automaticRepairAttempts = ref(0)
|
|
|
-const MAX_AUTOMATIC_REPAIR_ATTEMPTS = 1
|
|
|
+const MAX_AUTOMATIC_REPAIR_ATTEMPTS = 3
|
|
|
+type ProtectedPageContent = {
|
|
|
+ title: string
|
|
|
+ subtitle?: string
|
|
|
+ bullets: string[]
|
|
|
+ studentContent?: AiCoursewarePage["studentContent"]
|
|
|
+ studentTask?: string
|
|
|
+ learningEvidence?: string
|
|
|
+ interaction?: AiCoursewarePage["interaction"]
|
|
|
+}
|
|
|
+const protectedPageContent = new Map<number, ProtectedPageContent>()
|
|
|
const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
|
|
|
const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
|
|
|
const pollTimer = ref<number>()
|
|
|
const selectedPageNo = ref(1)
|
|
|
-const sampleCarouselRef = ref<HTMLElement>()
|
|
|
-const canScrollSamplePrevious = ref(false)
|
|
|
-const canScrollSampleNext = ref(false)
|
|
|
const selectedDraftPageIndex = ref(0)
|
|
|
const outlineLoading = ref(false)
|
|
|
const working = computed(
|
|
|
@@ -462,86 +386,27 @@ const pageMeta = computed(() => {
|
|
|
if (!outline.value) return `根据当前教材生成${preferences.minPages}至${preferences.maxPages}页音乐课件`
|
|
|
return [...[outline.value.gradeName, outline.value.unitName].filter(Boolean), `共${outline.value.pages.length}页`].join(" · ")
|
|
|
})
|
|
|
-const selectedPage = computed(() => {
|
|
|
- const pages = outline.value?.pages || []
|
|
|
- return pages.find(page => page.pageNo === selectedPageNo.value) || pages[0]
|
|
|
-})
|
|
|
-const studentHeadline = computed(() => selectedPage.value?.studentContent?.headline || selectedPage.value?.subtitle || "")
|
|
|
-const studentPoints = computed(() => {
|
|
|
- if (!selectedPage.value) return []
|
|
|
- return selectedPage.value.studentContent?.points?.length ? selectedPage.value.studentContent.points : selectedPage.value.bullets || []
|
|
|
-})
|
|
|
-const studentContentReadabilityHint = computed(() => {
|
|
|
- const contentCount = studentPoints.value.length + (studentHeadline.value ? 1 : 0)
|
|
|
- return contentCount > 6 ? `当前有${contentCount}条学生内容,预览会完整显示,但投屏字号可能变小,建议适当精简。` : ""
|
|
|
-})
|
|
|
const samplePageIndex = computed(() => {
|
|
|
const pages = outline.value?.pages || []
|
|
|
const selectedIndex = pages.findIndex(page => page.pageNo === selectedPageNo.value)
|
|
|
return selectedIndex >= 0 ? selectedIndex : 0
|
|
|
})
|
|
|
const samplePageNo = computed(() => outline.value?.pages[samplePageIndex.value]?.pageNo || samplePageIndex.value + 1)
|
|
|
-const samplePageTitle = computed(() => outline.value?.pages[samplePageIndex.value]?.title || "代表页面")
|
|
|
-const representativeSamples = computed(() => {
|
|
|
- const pages = outline.value?.pages || []
|
|
|
- if (!pages.length) return []
|
|
|
- const selected: Array<{ pageNo: number; label: string }> = []
|
|
|
- const add = (page: AiCoursewarePage | undefined, label: string) => {
|
|
|
- if (page && !selected.some(item => item.pageNo === page.pageNo)) selected.push({ pageNo: page.pageNo, label })
|
|
|
- }
|
|
|
- add(pages[0], "封面主视觉")
|
|
|
- add(
|
|
|
- pages.find(
|
|
|
- page =>
|
|
|
- [
|
|
|
- "hero_focus",
|
|
|
- "media_focus",
|
|
|
- "split_content",
|
|
|
- "diagram_focus",
|
|
|
- "story_stage",
|
|
|
- "full_image_question",
|
|
|
- "split_image",
|
|
|
- "observe_question"
|
|
|
- ].includes(page.visualPlan?.layout || "") && page.pageNo !== pages[0].pageNo
|
|
|
- ),
|
|
|
- "内容主视觉"
|
|
|
- )
|
|
|
- add(
|
|
|
- pages.find(page =>
|
|
|
- ["activity", "summary", "action_demo", "group_mission", "performance_stage", "activity_steps", "practice_check"].includes(
|
|
|
- page.visualPlan?.layout || ""
|
|
|
- )
|
|
|
- ),
|
|
|
- "课堂活动"
|
|
|
- )
|
|
|
- pages.forEach(page => {
|
|
|
- if (selected.length < 3) add(page, selected.length === 1 ? "内容主视觉" : "课堂活动")
|
|
|
- })
|
|
|
- return selected.slice(0, 3).map((item, index) => ({ ...item, order: index + 1 }))
|
|
|
-})
|
|
|
-const selectedDraftPage = computed(() => draftOutline.value?.pages[selectedDraftPageIndex.value])
|
|
|
-const selectedDraftSlide = computed(() => draftConverted.value?.slides[selectedDraftPageIndex.value])
|
|
|
+const workspaceSlides = computed(() => draftConverted.value?.slides || previewSlides.value || [])
|
|
|
+const workspacePages = computed(() => draftOutline.value?.pages || outline.value?.pages || [])
|
|
|
+const selectedWorkspacePageIndex = computed(() => (draftConverted.value ? selectedDraftPageIndex.value : samplePageIndex.value))
|
|
|
+const workspacePage = computed(() => workspacePages.value[selectedWorkspacePageIndex.value])
|
|
|
+const workspaceSlide = computed(() => workspaceSlides.value[selectedWorkspacePageIndex.value])
|
|
|
+const canEditWorkspaceText = computed(() => !working.value && (!draftConverted.value || draftPreparationState.value === "ready"))
|
|
|
const draftStatusTitle = computed(() => {
|
|
|
if (draftPreparationState.value === "ready") return "课件已准备完成"
|
|
|
- if (draftPreparationState.value === "failed") return "部分页面还未处理完成"
|
|
|
+ if (draftPreparationState.value === "failed") return "自动优化未能完成"
|
|
|
return "正在自动检查和优化课件"
|
|
|
})
|
|
|
const draftStatusDescription = computed(() => {
|
|
|
- if (draftPreparationState.value === "ready") return "系统已完成内容、排版和教学可用性检查,可以直接预览并应用。"
|
|
|
- if (draftPreparationState.value === "failed") return "无需逐项修改,点击重新自动优化即可继续。"
|
|
|
- return "正在清理重复内容、调整版式并修复问题页面,请稍候。"
|
|
|
-})
|
|
|
-const requiredSamplePages = computed(() => representativeSamples.value.map(item => item.pageNo))
|
|
|
-const allSamplesApproved = computed(
|
|
|
- () => requiredSamplePages.value.length === 3 && requiredSamplePages.value.every(pageNo => approvedSamplePages.value.includes(pageNo))
|
|
|
-)
|
|
|
-const samplePageReason = computed(() => {
|
|
|
- const layout = outline.value?.pages[samplePageIndex.value]?.visualPlan?.layout || ""
|
|
|
- if (["activity_steps", "action_demo", "group_mission", "performance_stage"].includes(layout)) return "这是课堂活动页,适合判断互动方式和信息层级"
|
|
|
- if (["cover_scene", "story_stage", "full_image_question", "observe_question"].includes(layout))
|
|
|
- return "这是主视觉页面,适合判断画风、配色和场景表现"
|
|
|
- if (["summary_checklist", "visual_summary", "practice_check"].includes(layout)) return "这是总结检测页,适合判断文字密度和阅读效率"
|
|
|
- return "这是你当前选中的页面,可用来判断版式和学生阅读体验"
|
|
|
+ if (draftPreparationState.value === "ready") return "可双击预览中的文字修改,确认后直接应用。"
|
|
|
+ if (draftPreparationState.value === "failed") return "请更换模板或返回修改需求后重新生成。"
|
|
|
+ return `系统正在自动处理问题页面(${Math.max(1, automaticRepairAttempts.value)}/${MAX_AUTOMATIC_REPAIR_ATTEMPTS})。`
|
|
|
})
|
|
|
const previewSlides = computed(() => {
|
|
|
if (!outline.value?.pages.length) return undefined
|
|
|
@@ -552,33 +417,91 @@ const previewSlides = computed(() => {
|
|
|
templateId: selectedTemplateId.value
|
|
|
}).slides
|
|
|
})
|
|
|
-const sampleSlide = computed(() => previewSlides.value?.[samplePageIndex.value])
|
|
|
-
|
|
|
-function updateSampleCarouselNavigation() {
|
|
|
- const carousel = sampleCarouselRef.value
|
|
|
- if (!carousel) {
|
|
|
- canScrollSamplePrevious.value = false
|
|
|
- canScrollSampleNext.value = false
|
|
|
+function selectWorkspacePage(index: number) {
|
|
|
+ sampleFeedbackTags.value = []
|
|
|
+ sampleFeedbackText.value = ""
|
|
|
+ if (draftConverted.value) {
|
|
|
+ selectedDraftPageIndex.value = index
|
|
|
return
|
|
|
}
|
|
|
- canScrollSamplePrevious.value = carousel.scrollLeft > 2
|
|
|
- canScrollSampleNext.value = carousel.scrollLeft + carousel.clientWidth < carousel.scrollWidth - 2
|
|
|
+ selectedPageNo.value = outline.value?.pages[index]?.pageNo || index + 1
|
|
|
}
|
|
|
|
|
|
-function scrollSampleCarousel(direction: -1 | 1) {
|
|
|
- const carousel = sampleCarouselRef.value
|
|
|
- if (!carousel) return
|
|
|
- carousel.scrollBy({ left: direction * Math.max(320, carousel.clientWidth * 0.72), behavior: "smooth" })
|
|
|
-}
|
|
|
+function handleWorkspaceTextChange(payload: { elementId: string; content: string; previousContent: string; text: string; previousText: string }) {
|
|
|
+ const slide = workspaceSlide.value
|
|
|
+ const page = workspacePage.value
|
|
|
+ if (!slide || !page || !canEditWorkspaceText.value) return
|
|
|
|
|
|
-function selectSamplePage(pageNo: number, index: number) {
|
|
|
- if (selectedPageNo.value !== pageNo) {
|
|
|
- sampleFeedbackTags.value = []
|
|
|
- sampleFeedbackText.value = ""
|
|
|
+ const element = slide.elements.find(item => item.id === payload.elementId)
|
|
|
+ if (element?.type === "text") element.content = payload.content
|
|
|
+ if (!replacePageVisibleText(page, payload.previousText, payload.text)) {
|
|
|
+ if (element?.type === "text") element.content = payload.previousContent
|
|
|
+ ElMessage.warning("这段文字由模板生成,请调整页面内容后再试")
|
|
|
+ return
|
|
|
}
|
|
|
- selectedPageNo.value = pageNo
|
|
|
- const item = sampleCarouselRef.value?.children.item(index) as HTMLElement | null
|
|
|
- item?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })
|
|
|
+
|
|
|
+ protectPageContent(page)
|
|
|
+ invalidateVisualApproval()
|
|
|
+ ElMessage.success("文字已更新")
|
|
|
+}
|
|
|
+
|
|
|
+function replacePageVisibleText(page: AiCoursewarePage, previousText: string, nextText: string) {
|
|
|
+ const target = normalizeDraftText(previousText)
|
|
|
+ if (!target) return false
|
|
|
+ let changed = false
|
|
|
+ const replaceValue = (value?: string) => {
|
|
|
+ if (normalizeDraftText(value) !== target) return value
|
|
|
+ changed = true
|
|
|
+ return nextText
|
|
|
+ }
|
|
|
+ const replaceList = (values?: string[]) => (values || []).map(value => replaceValue(value) || "")
|
|
|
+
|
|
|
+ page.title = replaceValue(page.title) || page.title
|
|
|
+ page.subtitle = replaceValue(page.subtitle)
|
|
|
+ page.bullets = replaceList(page.bullets)
|
|
|
+ if (page.studentContent) {
|
|
|
+ page.studentContent.headline = replaceValue(page.studentContent.headline)
|
|
|
+ page.studentContent.points = replaceList(page.studentContent.points)
|
|
|
+ }
|
|
|
+ if (page.classroomAction) page.classroomAction.studentTask = replaceValue(page.classroomAction.studentTask)
|
|
|
+ page.learningEvidence = replaceValue(page.learningEvidence)
|
|
|
+ if (page.interaction) {
|
|
|
+ page.interaction.question = replaceValue(page.interaction.question) || page.interaction.question
|
|
|
+ page.interaction.options = replaceList(page.interaction.options)
|
|
|
+ }
|
|
|
+ return changed
|
|
|
+}
|
|
|
+
|
|
|
+function protectPageContent(page: AiCoursewarePage) {
|
|
|
+ protectedPageContent.set(page.pageNo, {
|
|
|
+ title: page.title,
|
|
|
+ subtitle: page.subtitle,
|
|
|
+ bullets: [...(page.bullets || [])],
|
|
|
+ studentContent: page.studentContent ? { headline: page.studentContent.headline, points: [...(page.studentContent.points || [])] } : undefined,
|
|
|
+ studentTask: page.classroomAction?.studentTask,
|
|
|
+ learningEvidence: page.learningEvidence,
|
|
|
+ interaction: page.interaction ? { ...page.interaction, options: page.interaction.options ? [...page.interaction.options] : undefined } : undefined
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function restoreProtectedPageContent(targetOutline?: AiCoursewareOutline) {
|
|
|
+ if (!targetOutline || !protectedPageContent.size) return
|
|
|
+ targetOutline.pages.forEach(page => {
|
|
|
+ const protectedContent = protectedPageContent.get(page.pageNo)
|
|
|
+ if (!protectedContent) return
|
|
|
+ page.title = protectedContent.title
|
|
|
+ page.subtitle = protectedContent.subtitle
|
|
|
+ page.bullets = [...protectedContent.bullets]
|
|
|
+ page.studentContent = protectedContent.studentContent
|
|
|
+ ? { headline: protectedContent.studentContent.headline, points: [...(protectedContent.studentContent.points || [])] }
|
|
|
+ : undefined
|
|
|
+ if (!page.classroomAction) page.classroomAction = { teacherInstruction: "", studentTask: "", interactionType: "" }
|
|
|
+ page.classroomAction.studentTask = protectedContent.studentTask
|
|
|
+ page.learningEvidence = protectedContent.learningEvidence
|
|
|
+ page.interaction = protectedContent.interaction
|
|
|
+ ? { ...protectedContent.interaction, options: protectedContent.interaction.options ? [...protectedContent.interaction.options] : undefined }
|
|
|
+ : undefined
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
watch(selectedTemplateId, () => {
|
|
|
@@ -612,15 +535,6 @@ watch(
|
|
|
}
|
|
|
)
|
|
|
|
|
|
-watch(
|
|
|
- [workflowStep, selectedTemplateId, () => outline.value?.pages.length],
|
|
|
- async () => {
|
|
|
- await nextTick()
|
|
|
- updateSampleCarouselNavigation()
|
|
|
- },
|
|
|
- { immediate: true }
|
|
|
-)
|
|
|
-
|
|
|
async function handleOutline() {
|
|
|
if (!pptWork.id) {
|
|
|
ElMessage.error("当前课件ID为空")
|
|
|
@@ -636,6 +550,11 @@ async function handleOutline() {
|
|
|
outlineLoading.value = false
|
|
|
})
|
|
|
if (res.code === 200) {
|
|
|
+ protectedPageContent.clear()
|
|
|
+ draftOutline.value = undefined
|
|
|
+ draftConverted.value = undefined
|
|
|
+ draftQuality.value = undefined
|
|
|
+ draftPreparationState.value = "idle"
|
|
|
outline.value = res.data as AiCoursewareOutline
|
|
|
if (templateSelectionMode.value === "auto") selectedTemplateId.value = recommendAiCoursewareTemplate(outline.value).id
|
|
|
outline.value.style = selectedStyle.value
|
|
|
@@ -650,10 +569,6 @@ async function handleOutline() {
|
|
|
async function handleGenerate() {
|
|
|
const sourceOutline = draftOutline.value || outline.value
|
|
|
if (!sourceOutline || !pptWork.id) return
|
|
|
- if (!allSamplesApproved.value) {
|
|
|
- ElMessage.error("请先确认三张代表样稿")
|
|
|
- return
|
|
|
- }
|
|
|
|
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
|
@@ -661,6 +576,7 @@ async function handleGenerate() {
|
|
|
automaticRepairAttempts.value = 0
|
|
|
sourceOutline.style = selectedStyle.value
|
|
|
sourceOutline.templateId = selectedTemplateId.value
|
|
|
+ restoreProtectedPageContent(sourceOutline)
|
|
|
taskMode.value = "full"
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
|
@@ -719,10 +635,10 @@ async function fetchTask(taskId: string) {
|
|
|
clearPollTimer()
|
|
|
const message = taskInfo.value.message || "AI课件生成失败"
|
|
|
clearTask()
|
|
|
- if (workflowStep.value === 3) {
|
|
|
+ if (taskMode.value === "full" && draftOutline.value) {
|
|
|
draftPreparationState.value = "failed"
|
|
|
console.error("AI课件自动优化失败", message)
|
|
|
- ElMessage.error("自动优化未完成,请重新尝试")
|
|
|
+ await continueAutomaticRepairAfterFailure()
|
|
|
} else {
|
|
|
ElMessage.error(message)
|
|
|
}
|
|
|
@@ -750,9 +666,10 @@ async function loadDraft(taskId: string) {
|
|
|
|
|
|
const result = res.data as AiCoursewareGenerateResult
|
|
|
draftOutline.value = result.outline
|
|
|
+ restoreProtectedPageContent(draftOutline.value)
|
|
|
draftOutline.value.templateId = selectedTemplateId.value
|
|
|
selectedDraftPageIndex.value = 0
|
|
|
- workflowStep.value = 3
|
|
|
+ workflowStep.value = 2
|
|
|
clearTask()
|
|
|
await prepareDraftForUse()
|
|
|
}
|
|
|
@@ -765,6 +682,7 @@ async function loadSampleResult(taskId: string) {
|
|
|
}
|
|
|
const result = res.data as AiCoursewareGenerateResult
|
|
|
outline.value = result.outline
|
|
|
+ restoreProtectedPageContent(outline.value)
|
|
|
outline.value.style = selectedStyle.value
|
|
|
outline.value.templateId = selectedTemplateId.value
|
|
|
sampleFeedbackTags.value = []
|
|
|
@@ -795,17 +713,20 @@ async function regenerateDraftPage(index: number) {
|
|
|
draftPreparationState.value = "optimizing"
|
|
|
automaticRepairAttempts.value = 0
|
|
|
taskMode.value = "full"
|
|
|
+ restoreProtectedPageContent(draftOutline.value)
|
|
|
+ if (sampleFeedbackTags.value.includes("画风不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
|
|
|
+ const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
outline: draftOutline.value,
|
|
|
templateId: selectedTemplateId.value,
|
|
|
- visualPrompt: `${buildVisualPrompt()}请重新生成本页并主动修正学生可见内容、文字密度和版式问题。`,
|
|
|
+ visualPrompt: `${buildVisualPrompt()}请只调整第${index + 1}页的图片和版式,保持页面文字完全不变。${feedback ? `用户反馈:${feedback}。` : "请优化视觉组织和信息层级。"}`,
|
|
|
preferences: { ...preferences },
|
|
|
regeneratePageNo: index + 1
|
|
|
})
|
|
|
if (res.code !== 200) {
|
|
|
- draftPreparationState.value = "failed"
|
|
|
+ await continueAutomaticRepairAfterFailure()
|
|
|
return
|
|
|
}
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
@@ -834,6 +755,14 @@ function backToOutline() {
|
|
|
workflowStep.value = 2
|
|
|
}
|
|
|
|
|
|
+async function adjustCurrentPage() {
|
|
|
+ if (draftConverted.value) {
|
|
|
+ await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await regenerateSamplePage()
|
|
|
+}
|
|
|
+
|
|
|
function invalidateVisualApproval() {
|
|
|
approvedSamplePages.value = []
|
|
|
}
|
|
|
@@ -856,7 +785,7 @@ async function confirmSamplesAndGenerate() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- approvedSamplePages.value = [...requiredSamplePages.value]
|
|
|
+ approvedSamplePages.value = outline.value.pages.map(page => page.pageNo)
|
|
|
await handleGenerate()
|
|
|
}
|
|
|
|
|
|
@@ -881,6 +810,7 @@ function toggleSampleFeedback(item: string) {
|
|
|
}
|
|
|
|
|
|
function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
|
|
|
+ if (draftOutline.value) backToOutline()
|
|
|
templateSelectionMode.value = "manual"
|
|
|
selectedTemplateId.value = templateId
|
|
|
if (fromSample) {
|
|
|
@@ -891,6 +821,7 @@ function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false)
|
|
|
}
|
|
|
|
|
|
function useRecommendedTemplate() {
|
|
|
+ if (draftOutline.value) backToOutline()
|
|
|
templateSelectionMode.value = "auto"
|
|
|
selectedTemplateId.value = templateRecommendation.value.id
|
|
|
ElMessage.success(`已使用系统推荐的${recommendedTemplate.value.name}`)
|
|
|
@@ -924,6 +855,7 @@ async function prepareDraftForUse() {
|
|
|
if (!draftOutline.value) return
|
|
|
draftPreparationState.value = "optimizing"
|
|
|
normalizeDraftLocally()
|
|
|
+ restoreProtectedPageContent(draftOutline.value)
|
|
|
rebuildDraftPreview()
|
|
|
|
|
|
if (draftQuality.value?.passed) {
|
|
|
@@ -961,16 +893,21 @@ async function requestAutomaticRepair() {
|
|
|
preferences: { ...preferences }
|
|
|
})
|
|
|
if (res.code !== 200) {
|
|
|
- draftPreparationState.value = "failed"
|
|
|
+ await continueAutomaticRepairAfterFailure()
|
|
|
return
|
|
|
}
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
await pollTask(taskInfo.value.taskId)
|
|
|
}
|
|
|
|
|
|
-async function retryAutomaticOptimization() {
|
|
|
- automaticRepairAttempts.value = 0
|
|
|
- await prepareDraftForUse()
|
|
|
+async function continueAutomaticRepairAfterFailure() {
|
|
|
+ clearTask()
|
|
|
+ if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ return
|
|
|
+ }
|
|
|
+ automaticRepairAttempts.value += 1
|
|
|
+ await requestAutomaticRepair()
|
|
|
}
|
|
|
|
|
|
function normalizeDraftLocally() {
|
|
|
@@ -981,14 +918,14 @@ function normalizeDraftLocally() {
|
|
|
if (!page.studentContent) page.studentContent = { headline: "", points: [] }
|
|
|
|
|
|
const movedTeacherNotes: string[] = []
|
|
|
- const headline = cleanDraftText(page.studentContent.headline, 24)
|
|
|
+ const headline = cleanDraftText(page.studentContent.headline)
|
|
|
if (isTeacherOnlyText(headline)) movedTeacherNotes.push(headline)
|
|
|
page.studentContent.headline = isTeacherOnlyText(headline) ? "" : headline
|
|
|
|
|
|
const excluded = new Set([page.title, page.studentContent.headline].map(normalizeDraftText).filter(Boolean))
|
|
|
const pointKeys = new Set<string>()
|
|
|
const points = (page.studentContent.points?.length ? page.studentContent.points : page.bullets || [])
|
|
|
- .map(item => cleanDraftText(item, 22))
|
|
|
+ .map(item => cleanDraftText(item))
|
|
|
.filter(item => {
|
|
|
if (!item) return false
|
|
|
if (isTeacherOnlyText(item)) {
|
|
|
@@ -1000,7 +937,6 @@ function normalizeDraftLocally() {
|
|
|
pointKeys.add(key)
|
|
|
return true
|
|
|
})
|
|
|
- .slice(0, 4)
|
|
|
page.studentContent.points = points
|
|
|
page.bullets = [...points]
|
|
|
page.visualPlan.visualDensity = points.length <= 2 ? "low" : points.length >= 4 ? "high" : "medium"
|
|
|
@@ -1013,7 +949,7 @@ function normalizeDraftLocally() {
|
|
|
if (!page.classroomAction) {
|
|
|
page.classroomAction = { teacherInstruction: "", studentTask: page.studentContent.headline || page.title, interactionType: "" }
|
|
|
}
|
|
|
- const studentTask = cleanDraftText(page.classroomAction.studentTask, 24)
|
|
|
+ const studentTask = cleanDraftText(page.classroomAction.studentTask)
|
|
|
page.classroomAction.studentTask = !studentTask || isTeacherOnlyText(studentTask) ? page.studentContent.headline || page.title : studentTask
|
|
|
|
|
|
const layout = page.visualPlan.layout || ""
|
|
|
@@ -1041,11 +977,10 @@ function normalizeDraftLocally() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function cleanDraftText(value?: string, limit?: number) {
|
|
|
- const cleaned = String(value || "")
|
|
|
+function cleanDraftText(value?: string) {
|
|
|
+ return String(value || "")
|
|
|
.replace(/\s+/g, " ")
|
|
|
.trim()
|
|
|
- return limit ? cleaned.slice(0, limit) : cleaned
|
|
|
}
|
|
|
|
|
|
function normalizeDraftText(value?: string) {
|
|
|
@@ -1069,21 +1004,6 @@ function isTeacherOnlyText(value?: string) {
|
|
|
return Boolean(text && (TEACHER_VISIBLE_PATTERN.test(text) || PLACEHOLDER_PATTERN.test(text)))
|
|
|
}
|
|
|
|
|
|
-function updateStudentHeadline(page: AiCoursewarePage, event: Event) {
|
|
|
- if (!page.studentContent) page.studentContent = { headline: "", points: [] }
|
|
|
- page.studentContent.headline = (event.target as HTMLInputElement).value.trim()
|
|
|
-}
|
|
|
-
|
|
|
-function updateStudentPoints(page: AiCoursewarePage, event: Event) {
|
|
|
- if (!page.studentContent) page.studentContent = { headline: "", points: [] }
|
|
|
- const points = (event.target as HTMLTextAreaElement).value
|
|
|
- .split("\n")
|
|
|
- .map(item => item.trim())
|
|
|
- .filter(Boolean)
|
|
|
- page.studentContent.points = points
|
|
|
- page.bullets = points
|
|
|
-}
|
|
|
-
|
|
|
function buildExtraPrompt() {
|
|
|
return extraPrompt.value.trim()
|
|
|
}
|
|
|
@@ -1173,7 +1093,7 @@ function shortProgressLabel(message?: string) {
|
|
|
}
|
|
|
.workflow-steps {
|
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(3, 1fr);
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
margin-top: 12px;
|
|
|
padding: 4px;
|
|
|
border-radius: 8px;
|
|
|
@@ -1562,6 +1482,173 @@ function shortProgressLabel(message?: string) {
|
|
|
overflow: auto;
|
|
|
padding-right: 4px;
|
|
|
}
|
|
|
+.unified-review {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.compact-process-state {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 7px 10px;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: #f5f8fc;
|
|
|
+ color: #52616b;
|
|
|
+ font-size: 11px;
|
|
|
+ &.ready {
|
|
|
+ background: #eff9f5;
|
|
|
+ color: #16776f;
|
|
|
+ }
|
|
|
+ &.failed {
|
|
|
+ background: #fff7ed;
|
|
|
+ color: #9a5b08;
|
|
|
+ }
|
|
|
+ > div {
|
|
|
+ display: flex;
|
|
|
+ min-width: 0;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ strong {
|
|
|
+ color: #263442;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ span:last-child {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .loading-dot {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.process-state-icon {
|
|
|
+ display: grid;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ flex: 0 0 18px;
|
|
|
+ place-items: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: currentColor;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.unified-review-workspace {
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 154px minmax(0, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+.workspace-page-list {
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 2px 5px 2px 2px;
|
|
|
+ scrollbar-width: thin;
|
|
|
+}
|
|
|
+.workspace-page-item {
|
|
|
+ width: 100%;
|
|
|
+ padding: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: transparent;
|
|
|
+ color: #52616b;
|
|
|
+ text-align: left;
|
|
|
+ cursor: pointer;
|
|
|
+ + .workspace-page-item {
|
|
|
+ margin-top: 5px;
|
|
|
+ }
|
|
|
+ &:hover:not(:disabled) {
|
|
|
+ background: #f5f8fc;
|
|
|
+ }
|
|
|
+ &.active {
|
|
|
+ border-color: $themeColor;
|
|
|
+ background: rgba($themeColor, 0.06);
|
|
|
+ box-shadow: 0 0 0 2px rgba($themeColor, 0.08);
|
|
|
+ }
|
|
|
+ &:disabled {
|
|
|
+ cursor: default;
|
|
|
+ opacity: 0.72;
|
|
|
+ }
|
|
|
+ :deep(.thumbnail-slide) {
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 8px rgba(31, 41, 51, 0.1);
|
|
|
+ }
|
|
|
+ > span {
|
|
|
+ display: block;
|
|
|
+ margin-top: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+ font-size: 10px;
|
|
|
+ line-height: 1.35;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+}
|
|
|
+.workspace-preview-column {
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #dfe6ee;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.workspace-preview-heading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 7px;
|
|
|
+ > div {
|
|
|
+ display: flex;
|
|
|
+ min-width: 0;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ span,
|
|
|
+ small {
|
|
|
+ color: #7b8794;
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+ strong {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #1f2933;
|
|
|
+ font-size: 14px;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ small {
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+.workspace-canvas-shell {
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1 1 auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: auto;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: #eef2f7;
|
|
|
+ :deep(.thumbnail-slide) {
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 10px 28px rgba(31, 41, 51, 0.15);
|
|
|
+ }
|
|
|
+}
|
|
|
+.workspace-feedback {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ margin-top: 8px;
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
.sample-stage-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -2607,6 +2694,12 @@ function shortProgressLabel(message?: string) {
|
|
|
.draft-workspace {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
+ .unified-review-workspace {
|
|
|
+ grid-template-columns: 132px minmax(0, 1fr);
|
|
|
+ }
|
|
|
+ .workspace-page-item :deep(.thumbnail-slide) {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
.draft-page-list {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
@@ -2662,6 +2755,28 @@ function shortProgressLabel(message?: string) {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
+ .unified-review-workspace {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ .workspace-page-list {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ overflow-x: auto;
|
|
|
+ overflow-y: hidden;
|
|
|
+ padding-bottom: 6px;
|
|
|
+ }
|
|
|
+ .workspace-page-item {
|
|
|
+ width: 146px;
|
|
|
+ flex: 0 0 146px;
|
|
|
+ + .workspace-page-item {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .workspace-preview-heading {
|
|
|
+ align-items: flex-start;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 3px;
|
|
|
+ }
|
|
|
.footer {
|
|
|
flex-wrap: wrap;
|
|
|
}
|