|
|
@@ -14,6 +14,17 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <div class="workflow-steps">
|
|
|
+ <div
|
|
|
+ v-for="step in workflowSteps"
|
|
|
+ :key="step.value"
|
|
|
+ :class="['workflow-step', { active: workflowStep === step.value, done: workflowStep > step.value }]"
|
|
|
+ >
|
|
|
+ <span class="step-no">{{ workflowStep > step.value ? "✓" : step.value }}</span>
|
|
|
+ <span>{{ step.label }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div v-if="outlineLoading" class="loading-strip">
|
|
|
<span class="loading-dot"></span>
|
|
|
<span>正在生成大纲,请稍候</span>
|
|
|
@@ -27,75 +38,90 @@
|
|
|
<div class="progress-value">{{ progressValue }}%</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="extra">
|
|
|
- <div class="label">补充要求</div>
|
|
|
- <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
|
|
|
- </div>
|
|
|
+ <div v-if="workflowStep === 1" class="setup-stage">
|
|
|
+ <div class="stage-intro">
|
|
|
+ <div>
|
|
|
+ <div class="stage-title">设置这份课件的生成方式</div>
|
|
|
+ <div class="stage-desc">先确定课时、视觉和素材偏好,下一步再确认每页内容与视觉样张。</div>
|
|
|
+ </div>
|
|
|
+ <span v-if="outline" class="saved-state">已有大纲,可继续确认</span>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="style-selector">
|
|
|
- <div class="label">视觉方向</div>
|
|
|
- <div class="style-options">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- :class="['style-option', { active: selectedStyle === 'cartoon' }]"
|
|
|
- :disabled="working"
|
|
|
- @click="selectedStyle = 'cartoon'"
|
|
|
- >
|
|
|
- <strong>卡通课堂</strong>
|
|
|
- <span>适合小学和互动课堂</span>
|
|
|
- </button>
|
|
|
- <button type="button" :class="['style-option', { active: selectedStyle === 'clean' }]" :disabled="working" @click="selectedStyle = 'clean'">
|
|
|
- <strong>简洁教学</strong>
|
|
|
- <span>适合高年级和知识讲解</span>
|
|
|
- </button>
|
|
|
+ <div class="extra">
|
|
|
+ <div class="label">补充要求</div>
|
|
|
+ <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <div class="generation-preferences">
|
|
|
- <div class="label">生成偏好</div>
|
|
|
- <div class="preference-grid">
|
|
|
- <label
|
|
|
- ><span>课时</span
|
|
|
- ><select v-model.number="preferences.lessonMinutes" :disabled="working">
|
|
|
- <option :value="20">20分钟</option>
|
|
|
- <option :value="30">30分钟</option>
|
|
|
- <option :value="40">40分钟</option>
|
|
|
- <option :value="45">45分钟</option>
|
|
|
- </select></label
|
|
|
- >
|
|
|
- <label
|
|
|
- ><span>页数</span
|
|
|
- ><select v-model="pageRange" :disabled="working">
|
|
|
- <option value="9-12">9~12页</option>
|
|
|
- <option value="12-15">12~15页</option>
|
|
|
- <option value="9-15">9~15页</option>
|
|
|
- </select></label
|
|
|
- >
|
|
|
- <label
|
|
|
- ><span>素材</span
|
|
|
- ><select v-model="preferences.assetStyle" :disabled="working">
|
|
|
- <option value="illustration">统一插画</option>
|
|
|
- <option value="photo">真实照片</option>
|
|
|
- <option value="mixed">插画与照片混合</option>
|
|
|
- </select></label
|
|
|
- >
|
|
|
- <label
|
|
|
- ><span>教材原图</span
|
|
|
- ><select v-model="preferences.sourceImagePolicy" :disabled="working">
|
|
|
- <option value="strict">优先保留</option>
|
|
|
- <option value="balanced">适度使用</option>
|
|
|
- <option value="creative">自由创作</option>
|
|
|
- </select></label
|
|
|
- >
|
|
|
+ <div class="style-selector">
|
|
|
+ <div class="label">视觉方向</div>
|
|
|
+ <div class="style-options">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ :class="['style-option', { active: selectedStyle === 'cartoon' }]"
|
|
|
+ :disabled="working"
|
|
|
+ @click="selectedStyle = 'cartoon'"
|
|
|
+ >
|
|
|
+ <strong>卡通课堂</strong>
|
|
|
+ <span>适合小学和互动课堂</span>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ :class="['style-option', { active: selectedStyle === 'clean' }]"
|
|
|
+ :disabled="working"
|
|
|
+ @click="selectedStyle = 'clean'"
|
|
|
+ >
|
|
|
+ <strong>简洁教学</strong>
|
|
|
+ <span>适合高年级和知识讲解</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="media-options">
|
|
|
- <label><input v-model="preferences.requireAudio" type="checkbox" :disabled="working" />需要音频</label>
|
|
|
- <label><input v-model="preferences.requireVideo" type="checkbox" :disabled="working" />需要视频</label>
|
|
|
- <label><input v-model="preferences.enableAnimations" type="checkbox" :disabled="working" />启用克制动画</label>
|
|
|
+
|
|
|
+ <div class="generation-preferences">
|
|
|
+ <div class="label">生成偏好</div>
|
|
|
+ <div class="preference-grid">
|
|
|
+ <label
|
|
|
+ ><span>课时</span
|
|
|
+ ><select v-model.number="preferences.lessonMinutes" :disabled="working">
|
|
|
+ <option :value="20">20分钟</option>
|
|
|
+ <option :value="30">30分钟</option>
|
|
|
+ <option :value="40">40分钟</option>
|
|
|
+ <option :value="45">45分钟</option>
|
|
|
+ </select></label
|
|
|
+ >
|
|
|
+ <label
|
|
|
+ ><span>页数</span
|
|
|
+ ><select v-model="pageRange" :disabled="working">
|
|
|
+ <option value="9-12">9~12页</option>
|
|
|
+ <option value="12-15">12~15页</option>
|
|
|
+ <option value="9-15">9~15页</option>
|
|
|
+ </select></label
|
|
|
+ >
|
|
|
+ <label
|
|
|
+ ><span>素材</span
|
|
|
+ ><select v-model="preferences.assetStyle" :disabled="working">
|
|
|
+ <option value="illustration">统一插画</option>
|
|
|
+ <option value="photo">真实照片</option>
|
|
|
+ <option value="mixed">插画与照片混合</option>
|
|
|
+ </select></label
|
|
|
+ >
|
|
|
+ <label
|
|
|
+ ><span>教材原图</span
|
|
|
+ ><select v-model="preferences.sourceImagePolicy" :disabled="working">
|
|
|
+ <option value="strict">优先保留</option>
|
|
|
+ <option value="balanced">适度使用</option>
|
|
|
+ <option value="creative">自由创作</option>
|
|
|
+ </select></label
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <div class="media-options">
|
|
|
+ <label><input v-model="preferences.requireAudio" type="checkbox" :disabled="working" />需要音频</label>
|
|
|
+ <label><input v-model="preferences.requireVideo" type="checkbox" :disabled="working" />需要视频</label>
|
|
|
+ <label><input v-model="preferences.enableAnimations" type="checkbox" :disabled="working" />启用克制动画</label>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="draftConverted" class="result-preview">
|
|
|
+ <div v-if="workflowStep === 3 && draftConverted" class="result-preview">
|
|
|
<div class="preview-head">
|
|
|
<div>
|
|
|
<div class="preview-title">全稿预览</div>
|
|
|
@@ -139,18 +165,53 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-else-if="outline" class="outline">
|
|
|
+ <div v-else-if="workflowStep === 2 && outline" class="outline">
|
|
|
<div v-if="sampleSlide" class="sample-review">
|
|
|
<div class="sample-copy">
|
|
|
- <div class="field-title">视觉样张</div>
|
|
|
+ <div class="sample-eyebrow">视觉样张 · 当前同步左侧页面</div>
|
|
|
<div class="sample-title">第{{ samplePageNo }}页 · {{ samplePageTitle }}</div>
|
|
|
- <div class="sample-hint">样张使用最终模板和当前视觉方向渲染。修改大纲或视觉方向后需要重新确认。</div>
|
|
|
- <Button type="primary" @click="sampleApproved = !sampleApproved" :disabled="working">
|
|
|
- {{ sampleApproved ? "已确认样张" : "确认这个视觉方向" }}
|
|
|
- </Button>
|
|
|
+ <div class="sample-hint">{{ samplePageReason }}。你可以切换任意页面查看,也可以只重新生成当前页样张。</div>
|
|
|
+
|
|
|
+ <div class="sample-page-control">
|
|
|
+ <button type="button" :disabled="working || samplePageIndex <= 0" @click="previewPreviousPage">上一页</button>
|
|
|
+ <select v-model.number="selectedPageNo" :disabled="working">
|
|
|
+ <option v-for="page in outline.pages" :key="page.pageNo" :value="page.pageNo">第{{ page.pageNo }}页 · {{ page.title }}</option>
|
|
|
+ </select>
|
|
|
+ <button type="button" :disabled="working || samplePageIndex >= outline.pages.length - 1" @click="previewNextPage">下一页</button>
|
|
|
+ </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>
|
|
|
+ <textarea
|
|
|
+ v-model="sampleFeedbackText"
|
|
|
+ :disabled="working"
|
|
|
+ placeholder="还可以补充,例如:希望画面更童趣、减少卡片、突出山谷主视觉"
|
|
|
+ @input="sampleApproved = false"
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="sample-actions">
|
|
|
+ <Button @click="regenerateSamplePage" :disabled="working">重新生成当前样张</Button>
|
|
|
+ <Button type="primary" @click="approveSample" :disabled="working">
|
|
|
+ {{ sampleApproved ? "视觉方向已确认" : "确认这个视觉方向" }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div :class="['sample-canvas', { approved: sampleApproved }]">
|
|
|
<ThumbnailSlide :slide="sampleSlide" :size="420" />
|
|
|
+ <div v-if="sampleApproved" class="sample-approved-mark">已确认</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="page-nav">
|
|
|
@@ -262,19 +323,24 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-else class="empty">生成大纲后,可在这里确认课件结构、页面内容和设计要求。</div>
|
|
|
+ <div v-else-if="workflowStep === 1 && !outline" class="empty">完成生成设置后,系统会先生成大纲和可调整的视觉样张。</div>
|
|
|
|
|
|
<div class="footer">
|
|
|
<Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
|
|
|
- <template v-if="draftConverted">
|
|
|
+ <template v-if="workflowStep === 1">
|
|
|
+ <Button v-if="outline" @click="workflowStep = 2" :disabled="working">继续确认现有大纲</Button>
|
|
|
+ <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按新设置重新生成大纲" : "生成大纲并进入下一步" }}</Button>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="workflowStep === 2">
|
|
|
+ <Button @click="workflowStep = 1" :disabled="working">返回生成设置</Button>
|
|
|
+ <Button @click="regenerateSamplePage" :disabled="working">重新生成当前样张</Button>
|
|
|
+ <Button type="primary" @click="handleGenerate" :disabled="!sampleApproved || working">确认视觉并生成完整课件</Button>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
<Button @click="backToOutline" :disabled="working">返回修改</Button>
|
|
|
<Button type="primary" @click="handleGenerate" :disabled="working">重新生成</Button>
|
|
|
<Button type="primary" @click="applyDraft" :disabled="working || !draftQuality?.passed">应用到当前课件</Button>
|
|
|
</template>
|
|
|
- <template v-else>
|
|
|
- <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "重新生成大纲" : "生成大纲" }}</Button>
|
|
|
- <Button type="primary" @click="handleGenerate" :disabled="!outline || !sampleApproved || working">确认并生成PPT</Button>
|
|
|
- </template>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
@@ -322,7 +388,15 @@ const { addHistorySnapshot } = useHistorySnapshot()
|
|
|
const extraPrompt = ref("")
|
|
|
const outline = ref<AiCoursewareOutline>()
|
|
|
const taskInfo = ref<AiCoursewareTaskInfo>()
|
|
|
+const workflowStep = ref<1 | 2 | 3>(1)
|
|
|
+const workflowSteps = [
|
|
|
+ { value: 1, label: "生成设置" },
|
|
|
+ { value: 2, label: "大纲与样张" },
|
|
|
+ { value: 3, label: "全稿预览" }
|
|
|
+] as const
|
|
|
+const taskMode = ref<"full" | "sample">("full")
|
|
|
const selectedStyle = ref<"cartoon" | "clean">("cartoon")
|
|
|
+const paletteVariant = ref(0)
|
|
|
const pageRange = ref("9-12")
|
|
|
const preferences = reactive<AiCoursewarePreferences>({
|
|
|
lessonMinutes: 40,
|
|
|
@@ -335,6 +409,9 @@ const preferences = reactive<AiCoursewarePreferences>({
|
|
|
enableAnimations: true
|
|
|
})
|
|
|
const sampleApproved = ref(false)
|
|
|
+const sampleFeedbackOptions = ["配色不合适", "版式太单一", "图片不合适", "文字太多"]
|
|
|
+const sampleFeedbackTags = ref<string[]>([])
|
|
|
+const sampleFeedbackText = ref("")
|
|
|
const draftOutline = ref<AiCoursewareOutline>()
|
|
|
const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
const draftQuality = ref<AiPptQualityReport>()
|
|
|
@@ -371,17 +448,25 @@ const studentPoints = computed(() => {
|
|
|
const activityTask = computed(() => selectedPage.value?.classroomAction?.studentTask || selectedPage.value?.activity?.task || "")
|
|
|
const samplePageIndex = computed(() => {
|
|
|
const pages = outline.value?.pages || []
|
|
|
- const preferred = pages.findIndex(
|
|
|
- (page, index) => index > 0 && ["goal_cards", "activity_steps", "compare_listen", "summary_checklist"].includes(page.visualPlan?.layout || "")
|
|
|
- )
|
|
|
- return preferred >= 0 ? preferred : Math.min(1, Math.max(0, pages.length - 1))
|
|
|
+ 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 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 "这是你当前选中的页面,可用来判断版式和学生阅读体验"
|
|
|
+})
|
|
|
const sampleSlide = computed(() => {
|
|
|
if (!outline.value?.pages.length) return undefined
|
|
|
const previewOutline = { ...outline.value, style: selectedStyle.value }
|
|
|
- return aiCoursewareToSlides(previewOutline, { enableAnimations: preferences.enableAnimations }).slides[samplePageIndex.value]
|
|
|
+ return aiCoursewareToSlides(previewOutline, { enableAnimations: preferences.enableAnimations, paletteVariant: paletteVariant.value }).slides[
|
|
|
+ samplePageIndex.value
|
|
|
+ ]
|
|
|
})
|
|
|
|
|
|
const layoutOptions = [
|
|
|
@@ -411,6 +496,7 @@ const layoutOptions = [
|
|
|
]
|
|
|
|
|
|
watch(selectedStyle, style => {
|
|
|
+ paletteVariant.value = 0
|
|
|
if (outline.value) outline.value.style = style
|
|
|
invalidateVisualApproval()
|
|
|
})
|
|
|
@@ -457,6 +543,9 @@ async function handleOutline() {
|
|
|
outline.value = res.data as AiCoursewareOutline
|
|
|
outline.value.style = selectedStyle.value
|
|
|
selectedPageNo.value = outline.value.pages[0]?.pageNo || 1
|
|
|
+ sampleFeedbackTags.value = []
|
|
|
+ sampleFeedbackText.value = ""
|
|
|
+ workflowStep.value = 2
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -475,6 +564,7 @@ async function handleGenerate() {
|
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
|
sourceOutline.style = selectedStyle.value
|
|
|
+ taskMode.value = "full"
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
@@ -488,6 +578,25 @@ async function handleGenerate() {
|
|
|
await pollTask(taskInfo.value.taskId)
|
|
|
}
|
|
|
|
|
|
+async function regenerateSamplePage() {
|
|
|
+ if (!outline.value || !pptWork.id) return
|
|
|
+ sampleApproved.value = false
|
|
|
+ taskMode.value = "sample"
|
|
|
+ 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: outline.value,
|
|
|
+ visualPrompt: `${buildVisualPrompt()}当前只调整第${samplePageNo.value}页样张。${feedback ? `用户反馈:${feedback}。` : "请更换视觉组织方式,保持教学目标不变。"}`,
|
|
|
+ preferences: { ...preferences },
|
|
|
+ regeneratePageNo: samplePageNo.value
|
|
|
+ })
|
|
|
+ if (res.code !== 200) return
|
|
|
+ taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
+ await pollTask(taskInfo.value.taskId)
|
|
|
+}
|
|
|
+
|
|
|
async function pollTask(taskId: string) {
|
|
|
if (pollTimer.value) window.clearInterval(pollTimer.value)
|
|
|
await fetchTask(taskId)
|
|
|
@@ -505,7 +614,8 @@ async function fetchTask(taskId: string) {
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
if (taskInfo.value.status === "DONE") {
|
|
|
clearPollTimer()
|
|
|
- await loadDraft(taskId)
|
|
|
+ if (taskMode.value === "sample") await loadSampleResult(taskId)
|
|
|
+ else await loadDraft(taskId)
|
|
|
} else if (taskInfo.value.status === "FAILED") {
|
|
|
clearPollTimer()
|
|
|
ElMessage.error(taskInfo.value.message || "AI课件生成失败")
|
|
|
@@ -526,15 +636,36 @@ function clearPollTimer() {
|
|
|
|
|
|
async function loadDraft(taskId: string) {
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
|
- if (res.code !== 200) return
|
|
|
+ if (res.code !== 200) {
|
|
|
+ clearTask()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
const result = res.data as AiCoursewareGenerateResult
|
|
|
draftOutline.value = result.outline
|
|
|
rebuildDraftPreview()
|
|
|
+ workflowStep.value = 3
|
|
|
+ clearTask()
|
|
|
if (draftQuality.value?.passed) ElMessage.success("课件草稿已生成,请预览后确认应用")
|
|
|
else ElMessage.error(`课件草稿存在${draftQuality.value?.errors.length || 0}项阻止应用的问题`)
|
|
|
}
|
|
|
|
|
|
+async function loadSampleResult(taskId: string) {
|
|
|
+ const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
|
+ if (res.code !== 200) {
|
|
|
+ clearTask()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const result = res.data as AiCoursewareGenerateResult
|
|
|
+ outline.value = result.outline
|
|
|
+ outline.value.style = selectedStyle.value
|
|
|
+ sampleFeedbackTags.value = []
|
|
|
+ sampleFeedbackText.value = ""
|
|
|
+ workflowStep.value = 2
|
|
|
+ clearTask()
|
|
|
+ ElMessage.success(`第${samplePageNo.value}页样张已重新生成,请再次确认视觉方向`)
|
|
|
+}
|
|
|
+
|
|
|
function rebuildDraftPreview() {
|
|
|
if (!draftOutline.value) {
|
|
|
draftConverted.value = undefined
|
|
|
@@ -542,7 +673,10 @@ function rebuildDraftPreview() {
|
|
|
return
|
|
|
}
|
|
|
draftOutline.value.style = selectedStyle.value
|
|
|
- draftConverted.value = aiCoursewareToSlides(draftOutline.value, { enableAnimations: preferences.enableAnimations })
|
|
|
+ draftConverted.value = aiCoursewareToSlides(draftOutline.value, {
|
|
|
+ enableAnimations: preferences.enableAnimations,
|
|
|
+ paletteVariant: paletteVariant.value
|
|
|
+ })
|
|
|
draftQuality.value = inspectAiPptQuality(draftOutline.value, draftConverted.value.slides, preferences)
|
|
|
}
|
|
|
|
|
|
@@ -604,6 +738,7 @@ function removeDraftImage(index: number) {
|
|
|
|
|
|
async function regenerateDraftPage(index: number) {
|
|
|
if (!draftOutline.value || !pptWork.id) return
|
|
|
+ taskMode.value = "full"
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
@@ -634,14 +769,33 @@ function backToOutline() {
|
|
|
draftOutline.value = undefined
|
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
|
+ workflowStep.value = 2
|
|
|
}
|
|
|
|
|
|
function invalidateVisualApproval() {
|
|
|
sampleApproved.value = false
|
|
|
- if (!draftConverted.value) return
|
|
|
- draftOutline.value = undefined
|
|
|
- draftConverted.value = undefined
|
|
|
- draftQuality.value = undefined
|
|
|
+}
|
|
|
+
|
|
|
+function approveSample() {
|
|
|
+ sampleApproved.value = true
|
|
|
+ ElMessage.success("视觉方向已确认,可以生成完整课件")
|
|
|
+}
|
|
|
+
|
|
|
+function toggleSampleFeedback(item: string) {
|
|
|
+ sampleApproved.value = false
|
|
|
+ sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
|
|
|
+ ? sampleFeedbackTags.value.filter(value => value !== item)
|
|
|
+ : [...sampleFeedbackTags.value, item]
|
|
|
+}
|
|
|
+
|
|
|
+function previewPreviousPage() {
|
|
|
+ const page = outline.value?.pages[samplePageIndex.value - 1]
|
|
|
+ if (page) selectedPageNo.value = page.pageNo
|
|
|
+}
|
|
|
+
|
|
|
+function previewNextPage() {
|
|
|
+ const page = outline.value?.pages[samplePageIndex.value + 1]
|
|
|
+ if (page) selectedPageNo.value = page.pageNo
|
|
|
}
|
|
|
|
|
|
function updateStudentHeadline(page: AiCoursewarePage, event: Event) {
|
|
|
@@ -704,7 +858,13 @@ function buildVisualPrompt() {
|
|
|
: preferences.sourceImagePolicy === "creative"
|
|
|
? "允许根据教学目标重新创作主视觉。"
|
|
|
: "教材原图与新视觉保持平衡。"
|
|
|
- return `${base}${asset}${source}`
|
|
|
+ const palette =
|
|
|
+ paletteVariant.value === 1
|
|
|
+ ? "配色使用清新蓝绿体系。"
|
|
|
+ : paletteVariant.value === 2
|
|
|
+ ? "配色使用柔和紫色与珊瑚色体系。"
|
|
|
+ : "配色保持明快且有清晰对比。"
|
|
|
+ return `${base}${asset}${source}${palette}`
|
|
|
}
|
|
|
|
|
|
function formatPageNo(pageNo?: number) {
|
|
|
@@ -713,6 +873,7 @@ function formatPageNo(pageNo?: number) {
|
|
|
|
|
|
function shortProgressLabel(message?: string) {
|
|
|
if (!message) return ""
|
|
|
+ if (message.includes("重新生成第")) return "重做样张"
|
|
|
if (message.includes("读取") || message.includes("等待")) return "准备中"
|
|
|
if (message.includes("页面") || message.includes("内容")) return "生成内容"
|
|
|
if (message.includes("优化") || message.includes("教学设计")) return "优化设计"
|
|
|
@@ -762,6 +923,87 @@ function resourceTypeName(type: string) {
|
|
|
color: #7b8794;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
+.workflow-steps {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 4px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f4f7fa;
|
|
|
+}
|
|
|
+.workflow-step {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ height: 38px;
|
|
|
+ color: #7b8794;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ &.active {
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ color: $themeColor;
|
|
|
+ box-shadow: 0 2px 8px rgba(31, 41, 51, 0.08);
|
|
|
+ }
|
|
|
+ &.done {
|
|
|
+ color: #168f86;
|
|
|
+ }
|
|
|
+}
|
|
|
+.step-no {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #e7edf5;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.workflow-step.active .step-no {
|
|
|
+ background: $themeColor;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.workflow-step.done .step-no {
|
|
|
+ background: #dff3ed;
|
|
|
+ color: #168f86;
|
|
|
+}
|
|
|
+.setup-stage {
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+ overflow: auto;
|
|
|
+ padding-right: 4px;
|
|
|
+}
|
|
|
+.stage-intro {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 14px 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f7faff;
|
|
|
+}
|
|
|
+.stage-title {
|
|
|
+ color: #1f2933;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.stage-desc {
|
|
|
+ margin-top: 4px;
|
|
|
+ color: #7b8794;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.saved-state {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 5px 9px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #eaf7f4;
|
|
|
+ color: #168f86;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
.progress-strip,
|
|
|
.loading-strip {
|
|
|
margin-top: 12px;
|
|
|
@@ -903,26 +1145,107 @@ function resourceTypeName(type: string) {
|
|
|
.sample-review {
|
|
|
grid-column: 1 / -1;
|
|
|
display: grid;
|
|
|
- grid-template-columns: minmax(240px, 1fr) 420px;
|
|
|
+ grid-template-columns: minmax(360px, 1fr) 420px;
|
|
|
align-items: center;
|
|
|
gap: 20px;
|
|
|
- padding: 14px;
|
|
|
+ padding: 16px;
|
|
|
border: 1px solid #e5eaf0;
|
|
|
border-radius: 8px;
|
|
|
background: #fafcff;
|
|
|
}
|
|
|
+.sample-eyebrow {
|
|
|
+ color: $themeColor;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
.sample-title {
|
|
|
- margin: 8px 0;
|
|
|
- font-size: 16px;
|
|
|
+ margin: 7px 0 4px;
|
|
|
+ font-size: 18px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
.sample-hint {
|
|
|
- margin-bottom: 14px;
|
|
|
+ margin-bottom: 10px;
|
|
|
color: #7b8794;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
+.sample-page-control {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: auto minmax(0, 1fr) auto;
|
|
|
+ gap: 7px;
|
|
|
+ button,
|
|
|
+ select {
|
|
|
+ height: 32px;
|
|
|
+ border: 1px solid #dfe5ec;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ color: #52616b;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ button {
|
|
|
+ padding: 0 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ button:disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.45;
|
|
|
+ }
|
|
|
+ select {
|
|
|
+ min-width: 0;
|
|
|
+ padding: 0 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.sample-feedback {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+.feedback-label {
|
|
|
+ color: #52616b;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.feedback-chips {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 7px;
|
|
|
+ button {
|
|
|
+ height: 28px;
|
|
|
+ padding: 0 10px;
|
|
|
+ border: 1px solid #dfe5ec;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #fff;
|
|
|
+ color: #52616b;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 11px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ button.active {
|
|
|
+ border-color: $themeColor;
|
|
|
+ background: rgba($themeColor, 0.08);
|
|
|
+ color: $themeColor;
|
|
|
+ }
|
|
|
+}
|
|
|
+.sample-feedback textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 54px;
|
|
|
+ margin-top: 7px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ border: 1px solid #dfe5ec;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ resize: none;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+.sample-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
.sample-canvas {
|
|
|
+ position: relative;
|
|
|
overflow: hidden;
|
|
|
border: 2px solid transparent;
|
|
|
border-radius: 6px;
|
|
|
@@ -931,6 +1254,18 @@ function resourceTypeName(type: string) {
|
|
|
.sample-canvas.approved {
|
|
|
border-color: #168f86;
|
|
|
}
|
|
|
+.sample-approved-mark {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ padding: 5px 9px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #168f86;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ box-shadow: 0 4px 12px rgba(22, 143, 134, 0.25);
|
|
|
+}
|
|
|
.result-preview {
|
|
|
min-height: 0;
|
|
|
flex: 1;
|