|
|
@@ -1,2944 +1,183 @@
|
|
|
<template>
|
|
|
- <Modal
|
|
|
- :visible="visible"
|
|
|
- :width="1120"
|
|
|
- :content-style="{ maxWidth: 'calc(100vw - 32px)', boxSizing: 'border-box' }"
|
|
|
- :close-button="!working"
|
|
|
- :close-on-click-mask="!working"
|
|
|
- :close-on-esc="!working"
|
|
|
- @update:visible="value => emit('update:visible', value)"
|
|
|
- >
|
|
|
- <div :class="['ai-dialog', workflowStep === 1 ? 'setup-mode' : 'review-mode']">
|
|
|
- <div class="header">
|
|
|
- <div>
|
|
|
- <div class="title">{{ pageTitle }}</div>
|
|
|
- <div class="sub">{{ pageMeta }}</div>
|
|
|
- </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>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="workflowStep === 1" class="setup-stage simplified-setup">
|
|
|
- <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="setup-primary-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">由系统决定</option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="extra">
|
|
|
- <div class="label">还有什么特别要求?<span>选填</span></div>
|
|
|
- <TextArea v-model:value="extraPrompt" :rows="3" placeholder="例如:多安排节奏游戏,语言适合一年级学生" :disabled="working" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <button type="button" class="advanced-toggle" @click="advancedSettingsExpanded = !advancedSettingsExpanded">
|
|
|
- {{ advancedSettingsExpanded ? "收起高级设置" : "高级设置" }}
|
|
|
- </button>
|
|
|
- <div v-if="advancedSettingsExpanded" class="generation-preferences advanced-settings">
|
|
|
- <label class="label">本节课范围</label>
|
|
|
- <TextArea :value="preferences.lessonScope || ''" @update:value="preferences.lessonScope = $event" :rows="2" placeholder="例如:第一课时,重点学习第一段" :disabled="working" />
|
|
|
- <label class="label">学生基础与课堂条件</label>
|
|
|
- <TextArea :value="preferences.learningContext || ''" @update:value="preferences.learningContext = $event" :rows="2" placeholder="例如:尚不识谱,有打击乐器" :disabled="working" />
|
|
|
- <div class="preference-grid">
|
|
|
- <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-else-if="workflowStep === 2 && outline" class="outline unified-review">
|
|
|
- <details v-if="teachingDetailsExpanded || !outlineConfirmed" class="teaching-review" open>
|
|
|
- <summary>教材依据与教学主线</summary>
|
|
|
- <p>{{ reliableResearch ? "已检索到匹配教材的依据,可在这里查看教学安排。" : "教材尚未精确匹配,暂不能生成授课成品。请返回核对课程信息后重新检索。" }}</p>
|
|
|
- <p v-if="displayedConverted">必讲内容 {{ plannedMinutes }} 分钟 · 课时设置 {{ preferences.lessonMinutes }} 分钟</p>
|
|
|
- <p>{{ outline.textbookResearch?.contentSummary }}</p>
|
|
|
- <p v-for="item in outline.textbookResearch?.missingEvidence || []" :key="item">待核对:{{ item }}</p>
|
|
|
- <a v-for="source in researchSources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || "教材来源" }}</a>
|
|
|
- <ol><li v-for="objective in outline.textbookResearch?.teachingObjectives || []" :key="objective">{{ objective }}</li></ol>
|
|
|
- <div v-if="!outlineConfirmed" class="outline-plan">
|
|
|
- <label v-for="page in outline.pages" :key="page.pageNo">
|
|
|
- <span>第{{ page.pageNo }}页 · {{ page.teachingStage }}</span>
|
|
|
- <input v-model="page.title" aria-label="页面标题" />
|
|
|
- <input v-model="page.teachingPurpose" aria-label="教学目的" placeholder="本页教学目的" />
|
|
|
- </label>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
- <div v-if="displayedConverted && working" class="loading-strip" aria-live="polite"><span class="loading-dot"></span>正在完善课件,已有内容会保留</div>
|
|
|
- <section v-else-if="displayedConverted && (draftQuality?.errors.length || serverReviewMessage)" class="draft-notice" aria-live="polite">
|
|
|
- <span>初稿已保留,{{ draftQuality?.errors.length ? `有${draftQuality.errors.length}处待完善` : '还有内容待复核' }}</span>
|
|
|
- <button type="button" @click="qualityDetailsExpanded = !qualityDetailsExpanded">{{ qualityDetailsExpanded ? '收起' : '查看问题' }}</button>
|
|
|
- <Button :disabled="working" @click="repairDraft">自动完善</Button>
|
|
|
+ <Modal :visible="visible" :width="1040" :content-style="{ maxWidth: 'calc(100vw - 32px)' }" :close-button="!transferring" :close-on-click-mask="false" :close-on-esc="!transferring" @update:visible="close">
|
|
|
+ <div class="ai-dialog">
|
|
|
+ <header><h2>{{ outline?.chapterName || outline?.title || 'AI 生成课件' }}</h2><p>{{ metadata || '先确定教学安排,再生成课堂内容' }}</p></header>
|
|
|
+ <ol class="steps"><li v-for="(label, index) in steps" :key="label" :class="{ active: step === index + 1, done: step > index + 1 }"><span>{{ step > index + 1 ? '✓' : index + 1 }}</span>{{ label }}</li></ol>
|
|
|
+ <main>
|
|
|
+ <section v-if="step === 1" class="setup">
|
|
|
+ <h3>这节课准备怎么上?</h3>
|
|
|
+ <div class="fields"><label>课堂时长<select v-model.number="preferences.lessonMinutes" :disabled="outlineLoading"><option v-for="minutes in [20, 30, 40, 45]" :key="minutes" :value="minutes">{{ minutes }} 分钟</option></select></label><label>预计页数<select v-model="pageRange" :disabled="outlineLoading"><option value="9-12">9~12 页</option><option value="12-15">12~15 页</option><option value="9-15">9~15 页</option></select></label></div>
|
|
|
+ <label>教学范围<span class="hint">选填</span><input v-model="preferences.lessonScope" :disabled="outlineLoading" placeholder="例如:第一课时,重点认识声音的强弱" /></label>
|
|
|
+ <label>补充要求<span class="hint">选填</span><textarea v-model="extraPrompt" :disabled="outlineLoading" rows="3" placeholder="例如:多安排听辨和模仿活动,适合一年级学生" /></label>
|
|
|
+ <details><summary>更多设置</summary><label>学生情况<input v-model="preferences.learningContext" :disabled="outlineLoading" placeholder="例如:音乐基础较弱,喜欢游戏" /></label><div class="fields"><label>配图风格<select v-model="preferences.assetStyle"><option value="illustration">插画</option><option value="photo">实景照片</option><option value="mixed">综合搭配</option></select></label><label>教材配图<select v-model="preferences.sourceImagePolicy"><option value="balanced">优先使用教材配图</option><option value="strict">严格依据教材</option><option value="creative">允许创意配图</option></select></label></div><label class="check"><input v-model="preferences.enableAnimations" type="checkbox" />添加适量动画</label></details>
|
|
|
+ <p v-if="outlineLoading" role="status" class="notice">正在梳理教材和教学大纲,请稍候…</p><p v-if="setupError" role="alert" class="error">{{ setupError }}</p>
|
|
|
</section>
|
|
|
- <div v-if="qualityDetailsExpanded && displayedConverted" class="quality-panel">
|
|
|
- <p v-if="serverReviewMessage">{{ serverReviewMessage }}</p>
|
|
|
- <button v-for="(issue, index) in draftQuality?.errors || []" :key="index" type="button" @click="issue.pageNo && selectWorkspacePage(issue.pageNo - 1)">
|
|
|
- {{ issue.pageNo ? `第${issue.pageNo}页:` : '' }}{{ issue.message }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div class="sample-stage-head">
|
|
|
- <div class="sample-template-current">
|
|
|
- <span>当前样式</span>
|
|
|
- <strong>{{ selectedTemplate.name }}</strong>
|
|
|
- <small v-if="selectedTemplate.id === recommendedTemplate.id">系统推荐</small>
|
|
|
- </div>
|
|
|
- <button type="button" @click="teachingDetailsExpanded = !teachingDetailsExpanded">{{ teachingDetailsExpanded ? '收起说明' : '教学说明' }}</button>
|
|
|
- <button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
|
|
|
- {{ templatePickerExpanded ? "收起模板" : "更换模板" }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="templatePickerExpanded" class="compact-template-panel">
|
|
|
- <div class="sample-template-picker">
|
|
|
- <button
|
|
|
- v-for="template in visibleTemplates"
|
|
|
- :key="template.id"
|
|
|
- type="button"
|
|
|
- :class="['sample-template-option', { active: selectedTemplateId === template.id }]"
|
|
|
- :disabled="working"
|
|
|
- @click="selectTemplate(template.id, true)"
|
|
|
- >
|
|
|
- <span class="template-real-cover option" :style="templateCoverStyle(template.id)"></span>
|
|
|
- <span
|
|
|
- ><strong>{{ template.name }}</strong
|
|
|
- ><small>{{ template.id === recommendedTemplate.id ? "系统推荐 · " : "" }}{{ template.scene }}</small></span
|
|
|
- >
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <button v-if="!showAllTemplates" type="button" class="show-all-templates" @click="showAllTemplates = true">查看全部模板</button>
|
|
|
- <div v-if="selectedTemplateFit.level === 'low'" class="template-fit-warning">
|
|
|
- <span>当前模板不太适合这节课:{{ selectedTemplateFit.reason }}</span>
|
|
|
- <button type="button" :disabled="working" @click="useRecommendedTemplate">切换为系统推荐</button>
|
|
|
+ <section v-else-if="step === 2 && outline" class="outline-stage">
|
|
|
+ <div class="section-heading"><div><h3>确认教学大纲</h3><p>可直接修改内容,拖动调整顺序。确认后再生成课件。</p></div><div class="time">{{ pages.length }} 页 · 必讲 {{ totalMinutes }} / {{ preferences.lessonMinutes }} 分钟</div></div>
|
|
|
+ <ul v-if="validationErrors.length" class="error validation" role="alert"><li v-for="message in validationErrors" :key="message">{{ message }}</li></ul>
|
|
|
+ <div class="outline-grid"><aside><label>教学目标<span class="hint">每行一个</span><textarea v-model="goalsText" rows="7" @change="remember" /></label><label>课件模板<select v-model="templateId" @change="remember"><option v-for="template in AI_COURSEWARE_TEMPLATES" :key="template.id" :value="template.id">{{ template.name }}</option></select></label><details><summary>教材依据</summary><p>{{ outline.textbookResearch?.contentSummary }}</p><a v-for="source in sources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || '教材来源' }}</a></details><p v-if="!reliableResearch" class="error">尚未核实教材内容,请返回补充教学范围后重新生成大纲。</p></aside>
|
|
|
+ <div><Draggable v-model="pages" item-key="key" handle=".drag-handle" :animation="160" :move="canMove" @end="remember"><template #item="{ element, index }"><article class="page-card"><div class="page-heading"><span>第 {{ index + 1 }} 页{{ index === 0 ? ' · 封面' : '' }}</span><div v-if="index > 0" class="page-actions"><button class="drag-handle" type="button" aria-label="拖动排序">⠿</button><button type="button" :disabled="index === 1" :aria-label="`上移第${index + 1}页`" @click="movePage(index, -1)">↑</button><button type="button" :disabled="index === pages.length - 1" :aria-label="`下移第${index + 1}页`" @click="movePage(index, 1)">↓</button><button type="button" :disabled="pages.length <= MIN_OUTLINE_PAGES" :aria-label="`删除第${index + 1}页`" @click="removePage(index)">删除</button></div></div><label>页面标题<input v-model="element.page.title" @change="remember" /></label><div class="page-fields"><label>教学环节<input v-model="element.page.teachingStage" placeholder="例如:听辨练习" @change="remember" /></label><label>用时(分钟)<input v-model.number="element.page.durationMinutes" type="number" min="1" max="60" @change="remember" /></label><label class="check"><input v-model="element.page.optional" type="checkbox" @change="remember" />选讲</label></div><label>内容要点<span class="hint">每行一条</span><textarea v-model="element.pointsText" rows="3" @change="remember" /></label></article></template></Draggable><button class="add-page" type="button" :disabled="pages.length >= MAX_OUTLINE_PAGES" @click="addPage">+ 添加一页</button></div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="!workspaceSlide && outlineConfirmed" :class="['final-preview-placeholder', draftPreparationState]">
|
|
|
- <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
|
- <span v-else class="placeholder-icon">!</span>
|
|
|
- <strong>{{ draftPreparationState === "failed" ? "这次生成未完成" : generationStatusTitle }}</strong>
|
|
|
- <small>{{ draftPreparationState === "failed" ? draftFailureMessage : "完成后可直接预览、编辑和应用" }}</small>
|
|
|
- <Button v-if="draftPreparationState === 'failed'" type="primary" @click="regenerateFinalPreview">重试生成</Button>
|
|
|
- </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>
|
|
|
-
|
|
|
- <section class="workspace-preview-column" aria-label="页面预览及调整">
|
|
|
- <div class="workspace-preview-heading">
|
|
|
- <div>
|
|
|
- <span>第{{ selectedWorkspacePageIndex + 1 }} / {{ workspaceSlides.length }}页</span>
|
|
|
- <strong>{{ workspacePage?.title || "课件页面" }}</strong>
|
|
|
- </div>
|
|
|
- <small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
|
|
|
- </div>
|
|
|
- <div ref="workspaceCanvas" class="workspace-canvas-shell">
|
|
|
- <ThumbnailSlide
|
|
|
- :slide="workspaceSlide"
|
|
|
- :size="workspaceCanvasSize"
|
|
|
- :show-background-error="true"
|
|
|
- :editable-text="true"
|
|
|
- :edit-disabled="!canEditWorkspaceText"
|
|
|
- @background-load-state="handleSampleBackgroundLoadState"
|
|
|
- @text-change="handleWorkspaceTextChange"
|
|
|
- />
|
|
|
- <div v-if="working" class="workspace-processing-overlay">
|
|
|
- <span class="loading-dot"></span>
|
|
|
- <strong>{{ generationStatusTitle }}</strong>
|
|
|
- <small>完成后自动更新当前预览</small>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
|
- <details class="sample-feedback workspace-feedback">
|
|
|
- <summary>调整当前页</summary>
|
|
|
- <label><input v-model="editTeachingContent" type="checkbox" :disabled="working" />同时修改本页教学内容(保留其他页面)</label>
|
|
|
- <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="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
|
|
|
- <Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
- <details class="teaching-review" v-if="workspacePage">
|
|
|
- <summary>教师备注、用时与课堂资源</summary>
|
|
|
- <label>用时(分钟)<input type="number" min="1" max="60" v-model.number="workspacePage.durationMinutes" :disabled="working" @input="refreshEditedDraft" /></label>
|
|
|
- <label><input type="checkbox" v-model="workspacePage.optional" :disabled="working" @change="refreshEditedDraft" />选讲页,不计入必讲时长</label>
|
|
|
- <textarea v-model="workspacePage.speakerNotes" :disabled="working" @blur="refreshEditedDraft" aria-label="教师备注"></textarea>
|
|
|
- <p v-if="workspacePage.interaction?.answer">参考答案:{{ workspacePage.interaction.answer }} {{ workspacePage.interaction.explanation }}</p>
|
|
|
- <label v-for="resource in availableResources" :key="resource.resourceType + resource.resourceId">
|
|
|
- <input type="checkbox" :checked="pageHasResource(resource)" :disabled="working" @change="toggleResource(resource)" />{{ resource.resourceType }} · {{ resource.name }}
|
|
|
- </label>
|
|
|
- </details>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="footer">
|
|
|
- <Button @click="emit('update:visible', false)" :disabled="working">关闭</Button>
|
|
|
- <template v-if="isSetupStep">
|
|
|
- <Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
|
|
|
- <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
|
|
|
- </template>
|
|
|
- <template v-else-if="isStyleStep">
|
|
|
- <Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
|
- <Button v-if="!outlineConfirmed" type="primary" :disabled="working || !reliableResearch" @click="confirmOutline">确认教学安排并生成</Button>
|
|
|
- <span v-if="displayedConverted" class="draft-save-status">{{ draftSaved ? '草稿已保存' : '草稿未保存' }}</span>
|
|
|
- <details v-if="displayedConverted" class="more-actions"><summary>更多</summary><div class="more-actions-menu">
|
|
|
- <Button :disabled="working" @click="saveDraftExplicitly">保存初稿</Button>
|
|
|
- <Button v-if="previousCourseware" :disabled="working" @click="restorePreviousCourseware">恢复应用前课件</Button>
|
|
|
- </div></details>
|
|
|
- <Button
|
|
|
- v-if="displayedConverted && draftPreparationState === 'ready'"
|
|
|
- type="primary"
|
|
|
- :disabled="working || !draftQuality?.passed || Boolean(serverReviewMessage) || sampleBackgroundState.status === 'error'"
|
|
|
- @click="applyDraft"
|
|
|
- >应用到当前课件</Button
|
|
|
- >
|
|
|
- </template>
|
|
|
- </div>
|
|
|
+ </section>
|
|
|
+ <section v-else class="generation" aria-live="polite"><template v-if="generationError"><span class="error-icon">!</span><h3>{{ task ? '课件进度暂时无法继续' : '这次生成未完成' }}</h3><p>{{ generationError }}</p><p class="hint">已保留确认的大纲,当前课件没有被替换。</p></template><template v-else><span class="spinner"></span><h3>正在生成课件</h3><p>{{ task?.message || '正在准备课堂内容' }}</p><progress :value="task?.progress || 0" max="100"></progress><p class="hint">完成后进入编辑器。你可以先收起窗口。</p></template></section>
|
|
|
+ </main>
|
|
|
+ <footer><Button :disabled="transferring" @click="close">{{ task || outlineLoading ? '收起窗口' : '关闭' }}</Button><div v-if="step === 1"><Button v-if="outline" :disabled="outlineLoading" @click="step = 2">继续确认大纲</Button><Button type="primary" :disabled="outlineLoading" @click="generateOutline">{{ outlineLoading ? '正在生成大纲…' : outline ? '重新生成大纲' : '生成大纲' }}</Button></div><div v-else-if="step === 2"><Button @click="step = 1; remember()">返回修改需求</Button><Button type="primary" :disabled="!reliableResearch || transferring" @click="confirmAndGenerate">确认大纲并生成</Button></div><div v-else-if="generationError"><Button v-if="!task" @click="step = 2">返回修改大纲</Button><Button type="primary" :disabled="transferring" @click="retry">{{ task ? '继续查看课件' : '重试生成' }}</Button></div></footer>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
</template>
|
|
|
|
|
|
-<script lang="ts" setup>
|
|
|
-import { ref, reactive, computed, watch } from "vue"
|
|
|
-import { ElMessage } from "element-plus"
|
|
|
-import Modal from "@/components/Modal.vue"
|
|
|
-import Button from "@/components/Button.vue"
|
|
|
-import TextArea from "@/components/TextArea.vue"
|
|
|
-import ThumbnailSlide from "@/views/components/ThumbnailSlide/index.vue"
|
|
|
-import queryParams from "@/queryParams"
|
|
|
-import usePptWork from "@/store/pptWork"
|
|
|
-import { useSlidesStore } from "@/store"
|
|
|
-import useHistorySnapshot from "@/hooks/useHistorySnapshot"
|
|
|
-import { httpAjaxErrMsg } from "@/plugins/httpAjax"
|
|
|
-import {
|
|
|
- aiCoursewareGenerateApi,
|
|
|
- aiCoursewareOutlineApi,
|
|
|
- aiCoursewareResultApi,
|
|
|
- aiCoursewareTaskApi,
|
|
|
- type AiCoursewareOutline,
|
|
|
- type AiCoursewarePage,
|
|
|
- type AiCoursewarePreferences,
|
|
|
- type AiCoursewareTaskInfo,
|
|
|
- type AiCoursewareGenerateResult
|
|
|
-} from "@/api/aiCourseware"
|
|
|
-import { aiCoursewareToSlides } from "@/utils/aiCoursewareToSlides"
|
|
|
-import { inspectAiPptQuality, type AiPptQualityReport } from "@/utils/aiPptQuality"
|
|
|
-import { inspectAiPptResources } from "@/utils/aiPptResources"
|
|
|
-import {
|
|
|
- AI_COURSEWARE_TEMPLATES,
|
|
|
- getAiCoursewareTemplate,
|
|
|
- recommendAiCoursewareTemplate,
|
|
|
- scoreAiCoursewareTemplate,
|
|
|
- type AiCoursewareTemplateId
|
|
|
-} from "@/config/aiCoursewareTemplates"
|
|
|
-import { getAiCoursewareTemplateMaster } from "@/config/aiCoursewareTemplateMasters"
|
|
|
-import { resolveTemplateAssetUrl } from "@/utils/templateAssetUrl"
|
|
|
-import { draftKey, loadAiDraft, saveAiDraft, type AiDraftRecord, type ProtectedPageContent } from "@/utils/aiCoursewareDraft"
|
|
|
-import { usableResourceUrl } from "@/utils/aiPptContent"
|
|
|
-import type { AiCoursewareResourceBinding } from "@/api/aiCourseware"
|
|
|
-import type { Slide, SlideTheme } from "@/types/slides"
|
|
|
-
|
|
|
-const props = defineProps<{
|
|
|
- visible: boolean
|
|
|
-}>()
|
|
|
-
|
|
|
-const emit = defineEmits<{
|
|
|
- (event: "update:visible", payload: boolean): void
|
|
|
-}>()
|
|
|
-
|
|
|
-const pptWork = usePptWork()
|
|
|
-const slidesStore = useSlidesStore()
|
|
|
-const { addHistorySnapshot } = useHistorySnapshot()
|
|
|
-
|
|
|
-const extraPrompt = ref("")
|
|
|
-const outline = ref<AiCoursewareOutline>()
|
|
|
-const outlineConfirmed = ref(false)
|
|
|
-const teachingDetailsExpanded = ref(false)
|
|
|
-const qualityDetailsExpanded = ref(false)
|
|
|
-const serverReviewMessage = ref("")
|
|
|
-const draftSaved = ref(false)
|
|
|
-const workspaceCanvas = ref<HTMLElement>()
|
|
|
-const canvasWidth = ref(720)
|
|
|
-const canvasHeight = ref(405)
|
|
|
-watch(workspaceCanvas, (element, _, onCleanup) => {
|
|
|
- if (!element) return
|
|
|
- const observer = new ResizeObserver(([entry]) => {
|
|
|
- canvasWidth.value = entry.contentRect.width
|
|
|
- canvasHeight.value = entry.contentRect.height
|
|
|
- })
|
|
|
- observer.observe(element)
|
|
|
- onCleanup(() => observer.disconnect())
|
|
|
-})
|
|
|
-// 同时约束宽高,避免小窗口里只显示幻灯片中间的一截。
|
|
|
-const workspaceCanvasSize = computed(() => Math.max(1, Math.min(canvasWidth.value, canvasHeight.value / slidesStore.viewportRatio)))
|
|
|
-const editTeachingContent = ref(false)
|
|
|
-const availableResources = ref<AiCoursewareResourceBinding[]>([])
|
|
|
-const previousCourseware = ref<AiDraftRecord["backup"]>()
|
|
|
-const reliableResearch = computed(() => outline.value?.textbookResearch?.exactMatch === true && outline.value.textbookResearch.confidence === "HIGH" && researchSources.value.length > 0)
|
|
|
-const researchSources = computed(() => (outline.value?.textbookResearch?.sources || []).filter(source => /^https?:\/\//i.test(source.url)))
|
|
|
-const plannedMinutes = computed(() => (displayedOutline.value?.pages || []).filter(page => !page.optional).reduce((total, page) => total + (Number(page.durationMinutes) || 0), 0))
|
|
|
-const taskInfo = ref<AiCoursewareTaskInfo>()
|
|
|
-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: "预览、编辑并应用" }
|
|
|
-] as const
|
|
|
-const coursewareTemplates = AI_COURSEWARE_TEMPLATES
|
|
|
-const defaultTemplateId = AI_COURSEWARE_TEMPLATES[0].id
|
|
|
-const selectedTemplateId = ref<AiCoursewareTemplateId>(defaultTemplateId)
|
|
|
-const templateSelectionMode = ref<"auto" | "manual">("auto")
|
|
|
-const templatePickerExpanded = ref(false)
|
|
|
-const advancedSettingsExpanded = ref(false)
|
|
|
-const showAllTemplates = ref(false)
|
|
|
-const paletteVariant = ref(0)
|
|
|
-const pageRange = ref("9-12")
|
|
|
-const preferences = reactive<AiCoursewarePreferences>({
|
|
|
- lessonMinutes: 40,
|
|
|
- minPages: 9,
|
|
|
- maxPages: 12,
|
|
|
- assetStyle: "illustration",
|
|
|
- sourceImagePolicy: "balanced",
|
|
|
- requireAudio: false,
|
|
|
- requireVideo: false,
|
|
|
- enableAnimations: true
|
|
|
-})
|
|
|
-const sampleBackgroundState = ref<{ status: "none" | "loading" | "loaded" | "error"; url: string }>({ status: "none", url: "" })
|
|
|
-const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "层级不清晰"]
|
|
|
-const sampleFeedbackTags = ref<string[]>([])
|
|
|
-const sampleFeedbackText = ref("")
|
|
|
-const draftOutline = ref<AiCoursewareOutline>()
|
|
|
-const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
-const displayedOutline = ref<AiCoursewareOutline>()
|
|
|
-const displayedConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
-const displayedTemplateId = ref<AiCoursewareTemplateId>()
|
|
|
-const draftQuality = ref<AiPptQualityReport>()
|
|
|
-const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
|
|
|
-const draftFailureMessage = ref("请稍后重试")
|
|
|
-const protectedPageContent = new Map<number, ProtectedPageContent>()
|
|
|
-const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
|
|
|
-const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
|
|
|
-const pollTimer = ref<number>()
|
|
|
-const selectedDraftPageIndex = ref(0)
|
|
|
-const outlineLoading = ref(false)
|
|
|
-const working = computed(
|
|
|
- () =>
|
|
|
- outlineLoading.value ||
|
|
|
- draftPreparationState.value === "optimizing" ||
|
|
|
- taskInfo.value?.status === "PENDING" ||
|
|
|
- taskInfo.value?.status === "RUNNING"
|
|
|
-)
|
|
|
-const selectedTemplate = computed(() => getAiCoursewareTemplate(selectedTemplateId.value))
|
|
|
-const selectedStyle = computed(() => selectedTemplate.value.baseStyle)
|
|
|
-const templateRecommendation = computed(() =>
|
|
|
- outline.value ? recommendAiCoursewareTemplate(outline.value) : { id: defaultTemplateId, reason: "生成大纲后将根据课程内容自动推荐" }
|
|
|
-)
|
|
|
-const recommendedTemplate = computed(() => getAiCoursewareTemplate(templateRecommendation.value.id))
|
|
|
-const visibleTemplates = computed(() => {
|
|
|
- if (showAllTemplates.value) return coursewareTemplates
|
|
|
- const recommendedId = recommendedTemplate.value.id
|
|
|
- return [...coursewareTemplates]
|
|
|
- .sort((left, right) => {
|
|
|
- if (left.id === recommendedId) return -1
|
|
|
- if (right.id === recommendedId) return 1
|
|
|
- return outline.value ? templateFit(right.id).score - templateFit(left.id).score : 0
|
|
|
- })
|
|
|
- .slice(0, 4)
|
|
|
-})
|
|
|
-const selectedTemplateFit = computed(() =>
|
|
|
- outline.value
|
|
|
- ? scoreAiCoursewareTemplate(outline.value, selectedTemplateId.value)
|
|
|
- : { templateId: selectedTemplateId.value, score: 80, level: "usable" as const, label: "可以使用" as const, reason: "生成大纲后评估" }
|
|
|
-)
|
|
|
-const progressValue = computed(() => taskInfo.value?.progress || 0)
|
|
|
-const progressSteps = [
|
|
|
- { threshold: 0, label: "准备中" },
|
|
|
- { threshold: 25, label: "生成内容" },
|
|
|
- { threshold: 50, label: "优化设计" },
|
|
|
- { threshold: 65, label: "匹配图片" },
|
|
|
- { threshold: 100, label: "生成完成" }
|
|
|
-]
|
|
|
-const currentProgressLabel = computed(() => {
|
|
|
- const matched = [...progressSteps].reverse().find(step => progressValue.value >= step.threshold)
|
|
|
- return shortProgressLabel(taskInfo.value?.message) || matched?.label || "处理中"
|
|
|
-})
|
|
|
-const generationStatusTitle = computed(() => {
|
|
|
- if (!taskInfo.value) return "正在生成课件"
|
|
|
- return `${currentProgressLabel.value} ${progressValue.value}%`
|
|
|
-})
|
|
|
-const pageTitle = computed(() => outline.value?.chapterName || outline.value?.title || "AI生成课件")
|
|
|
-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 workspaceSlides = computed(() => displayedConverted.value?.slides || [])
|
|
|
-const workspacePages = computed(() => displayedOutline.value?.pages || [])
|
|
|
-const selectedWorkspacePageIndex = computed(() => selectedDraftPageIndex.value)
|
|
|
-const workspacePage = computed(() => workspacePages.value[selectedWorkspacePageIndex.value])
|
|
|
-const workspaceSlide = computed(() => workspaceSlides.value[selectedWorkspacePageIndex.value])
|
|
|
-const canEditWorkspaceText = computed(() => !working.value && Boolean(displayedConverted.value) && draftPreparationState.value === "ready")
|
|
|
-const previewSlides = computed(() => {
|
|
|
- if (!outline.value?.pages.length) return undefined
|
|
|
- const previewOutline = { ...outline.value, style: selectedStyle.value }
|
|
|
- return aiCoursewareToSlides(previewOutline, {
|
|
|
- enableAnimations: preferences.enableAnimations,
|
|
|
- paletteVariant: paletteVariant.value,
|
|
|
- templateId: selectedTemplateId.value
|
|
|
- }).slides
|
|
|
-})
|
|
|
-function selectWorkspacePage(index: number) {
|
|
|
- sampleFeedbackTags.value = []
|
|
|
- sampleFeedbackText.value = ""
|
|
|
- selectedDraftPageIndex.value = index
|
|
|
-}
|
|
|
-
|
|
|
-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
|
|
|
-
|
|
|
- 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
|
|
|
- }
|
|
|
-
|
|
|
- protectPageContent(page)
|
|
|
- restoreProtectedPageContent(outline.value)
|
|
|
- restoreProtectedPageContent(draftOutline.value)
|
|
|
- refreshEditedDraft()
|
|
|
- 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, () => {
|
|
|
- paletteVariant.value = 0
|
|
|
- if (outline.value) outline.value.style = selectedStyle.value
|
|
|
-})
|
|
|
-
|
|
|
-watch(pageRange, value => {
|
|
|
- const [minPages, maxPages] = value.split("-").map(Number)
|
|
|
- preferences.minPages = minPages
|
|
|
- preferences.maxPages = maxPages
|
|
|
-})
|
|
|
-
|
|
|
-watch(
|
|
|
- outline,
|
|
|
- () => {
|
|
|
- if (!outline.value) return
|
|
|
- if (templateSelectionMode.value === "auto") selectedTemplateId.value = templateRecommendation.value.id
|
|
|
- },
|
|
|
- { deep: true }
|
|
|
-)
|
|
|
-
|
|
|
-watch(
|
|
|
- () => props.visible,
|
|
|
- visible => {
|
|
|
- if (!visible) clearTask()
|
|
|
- else if (!outline.value) void restoreSavedDraft()
|
|
|
- }
|
|
|
-)
|
|
|
-
|
|
|
-async function handleOutline() {
|
|
|
- if (!pptWork.id) {
|
|
|
- ElMessage.error("当前课件ID为空")
|
|
|
- return
|
|
|
- }
|
|
|
- outlineLoading.value = true
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareOutlineApi, {
|
|
|
- id: pptWork.id,
|
|
|
- fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
- extraPrompt: buildExtraPrompt(),
|
|
|
- preferences: { ...preferences }
|
|
|
- }).finally(() => {
|
|
|
- outlineLoading.value = false
|
|
|
- })
|
|
|
- if (res.code === 200) {
|
|
|
- protectedPageContent.clear()
|
|
|
- draftOutline.value = undefined
|
|
|
- draftConverted.value = undefined
|
|
|
- displayedOutline.value = undefined
|
|
|
- displayedConverted.value = undefined
|
|
|
- displayedTemplateId.value = undefined
|
|
|
- draftQuality.value = undefined
|
|
|
- serverReviewMessage.value = ""
|
|
|
- qualityDetailsExpanded.value = false
|
|
|
- draftPreparationState.value = "idle"
|
|
|
- draftFailureMessage.value = "请稍后重试"
|
|
|
- outline.value = res.data as AiCoursewareOutline
|
|
|
- if (templateSelectionMode.value === "auto") selectedTemplateId.value = recommendAiCoursewareTemplate(outline.value).id
|
|
|
- outline.value.style = selectedStyle.value
|
|
|
- outline.value.templateId = selectedTemplateId.value
|
|
|
- selectedDraftPageIndex.value = 0
|
|
|
- sampleFeedbackTags.value = []
|
|
|
- sampleFeedbackText.value = ""
|
|
|
- outlineConfirmed.value = false
|
|
|
- draftPreparationState.value = "idle"
|
|
|
- workflowStep.value = 2
|
|
|
- await persistDraft()
|
|
|
- if (reliableResearch.value) await confirmOutline()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function continueToFinalPreview() {
|
|
|
- workflowStep.value = 2
|
|
|
- if (displayedConverted.value) {
|
|
|
- draftPreparationState.value = "ready"
|
|
|
- return
|
|
|
- }
|
|
|
- if (outlineConfirmed.value) await handleGenerate()
|
|
|
-}
|
|
|
-
|
|
|
-async function regenerateFinalPreview() {
|
|
|
- const sourceOutline = displayedOutline.value || draftOutline.value || outline.value
|
|
|
- if (!sourceOutline) return
|
|
|
- draftOutline.value = cloneCoursewareData(sourceOutline)
|
|
|
- restoreProtectedPageContent(draftOutline.value)
|
|
|
- workflowStep.value = 2
|
|
|
- await handleGenerate()
|
|
|
-}
|
|
|
-
|
|
|
-function publishFinalPreview() {
|
|
|
- if (!draftOutline.value || !draftConverted.value) return
|
|
|
- displayedOutline.value = cloneCoursewareData(draftOutline.value)
|
|
|
- displayedConverted.value = cloneCoursewareData(draftConverted.value)
|
|
|
- displayedTemplateId.value = selectedTemplateId.value
|
|
|
- selectedDraftPageIndex.value = Math.max(0, Math.min(selectedDraftPageIndex.value, displayedConverted.value.slides.length - 1))
|
|
|
- sampleFeedbackTags.value = []
|
|
|
- sampleFeedbackText.value = ""
|
|
|
-}
|
|
|
-
|
|
|
-function handleFinalPreparationFailure(message = "自动生成和优化未能完成") {
|
|
|
- if (displayedOutline.value && displayedConverted.value && displayedTemplateId.value) {
|
|
|
- selectedTemplateId.value = displayedTemplateId.value
|
|
|
- draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
- draftConverted.value = cloneCoursewareData(displayedConverted.value)
|
|
|
- draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
|
|
|
- draftPreparationState.value = "ready"
|
|
|
- ElMessage.warning(`${message},已保留上一次最终预览`)
|
|
|
- return
|
|
|
- }
|
|
|
- draftFailureMessage.value = message
|
|
|
- draftPreparationState.value = "failed"
|
|
|
-}
|
|
|
-
|
|
|
-async function repairDraft() {
|
|
|
- if (working.value || !displayedOutline.value) return
|
|
|
- draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
- await handleGenerate(true)
|
|
|
-}
|
|
|
-
|
|
|
-function cloneCoursewareData<T>(value: T): T {
|
|
|
- return JSON.parse(JSON.stringify(value)) as T
|
|
|
-}
|
|
|
-
|
|
|
-async function handleGenerate(repairOnly = false) {
|
|
|
- const sourceOutline = draftOutline.value || outline.value
|
|
|
- if (!sourceOutline || !pptWork.id) return
|
|
|
-
|
|
|
- draftConverted.value = undefined
|
|
|
- if (!displayedConverted.value) draftQuality.value = undefined
|
|
|
- draftPreparationState.value = "optimizing"
|
|
|
- draftFailureMessage.value = "请稍后重试"
|
|
|
- sourceOutline.style = selectedStyle.value
|
|
|
- sourceOutline.templateId = selectedTemplateId.value
|
|
|
- restoreProtectedPageContent(sourceOutline)
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
- id: pptWork.id,
|
|
|
- fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
- outline: sourceOutline,
|
|
|
- templateId: selectedTemplateId.value,
|
|
|
- visualPrompt: buildVisualPrompt() + (repairOnly ? `\n请在现有初稿上修复以下问题,保留无关内容及教师修改:${[serverReviewMessage.value, ...(draftQuality.value?.errors || []).map(issue => issue.message)].filter(Boolean).join(';')}` : ''),
|
|
|
- repairOnly,
|
|
|
- preferences: { ...preferences }
|
|
|
- })
|
|
|
- if (res.code !== 200) {
|
|
|
- handleFinalPreparationFailure(res.message || "课件生成请求失败,请重试")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
- await persistDraft()
|
|
|
- await pollTask(taskInfo.value.taskId)
|
|
|
-}
|
|
|
-
|
|
|
-async function pollTask(taskId: string) {
|
|
|
- if (pollTimer.value) window.clearInterval(pollTimer.value)
|
|
|
- await fetchTask(taskId)
|
|
|
-}
|
|
|
-
|
|
|
-async function fetchTask(taskId: string) {
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import Draggable from 'vuedraggable'
|
|
|
+import { nanoid } from 'nanoid'
|
|
|
+import Modal from '@/components/Modal.vue'
|
|
|
+import Button from '@/components/Button.vue'
|
|
|
+import queryParams from '@/queryParams'
|
|
|
+import usePptWork from '@/store/pptWork'
|
|
|
+import { useMainStore, useSlidesStore, useSnapshotStore } from '@/store'
|
|
|
+import { httpAjax } from '@/plugins/httpAjax'
|
|
|
+import { aiCoursewareGenerateApi, aiCoursewareOutlineApi, aiCoursewareResultApi, aiCoursewareTaskApi, type AiCoursewareOutline, type AiCoursewarePreferences, type AiCoursewareTaskInfo, type AiCoursewareGenerateResult } from '@/api/aiCourseware'
|
|
|
+import { AI_COURSEWARE_TEMPLATES, recommendAiCoursewareTemplate, getAiCoursewareTemplate } from '@/config/aiCoursewareTemplates'
|
|
|
+import { aiCoursewareToSlides } from '@/utils/aiCoursewareToSlides'
|
|
|
+import { inspectAiPptQuality, type AiPptQualityIssue } from '@/utils/aiPptQuality'
|
|
|
+import { inspectAiPptResources } from '@/utils/aiPptResources'
|
|
|
+import { draftKey, loadAiDraft, saveAiDraft } from '@/utils/aiCoursewareDraft'
|
|
|
+import { editableOutlinePages, confirmedCoursewareOutline, inspectConfirmedOutline, MIN_OUTLINE_PAGES, MAX_OUTLINE_PAGES, type EditableOutlinePage } from '@/utils/aiCoursewareOutline'
|
|
|
+
|
|
|
+const props = defineProps<{ visible: boolean }>()
|
|
|
+const emit = defineEmits<{ (e: 'update:visible', value: boolean): void; (e: 'generated', issues: AiPptQualityIssue[]): void }>()
|
|
|
+const slidesStore = useSlidesStore(), pptWork = usePptWork(), snapshots = useSnapshotStore()
|
|
|
+const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
|
|
|
+const defaults = (): AiCoursewarePreferences => ({ lessonMinutes: 40, minPages: 9, maxPages: 12, assetStyle: 'illustration', sourceImagePolicy: 'balanced', requireAudio: false, requireVideo: false, enableAnimations: true })
|
|
|
+const preferences = reactive(defaults())
|
|
|
+const steps = ['设置需求', '确认大纲', '生成课件', '编辑课件', '保存课件']
|
|
|
+const step = ref(1), outline = ref<AiCoursewareOutline>(), pages = ref<EditableOutlinePage[]>([]), goalsText = ref('')
|
|
|
+const extraPrompt = ref(''), pageRange = ref('9-12'), templateId = ref('')
|
|
|
+const outlineLoading = ref(false), transferring = ref(false), setupError = ref(''), generationError = ref(''), validationErrors = ref<string[]>([])
|
|
|
+const task = ref<AiCoursewareTaskInfo>(), confirmed = ref<AiCoursewareOutline>()
|
|
|
+let pollTimer: ReturnType<typeof setTimeout> | undefined, version = 0, replacementSnapshot = '', saveQueue = Promise.resolve()
|
|
|
+const source = () => queryParams.pptResourcesType || queryParams.fromType || 'TEACHER'
|
|
|
+const metadata = computed(() => [outline.value?.gradeName, outline.value?.unitName].filter(Boolean).join(' · '))
|
|
|
+const sources = computed(() => (outline.value?.textbookResearch?.sources || []).filter(item => /^https?:\/\//i.test(item.url)))
|
|
|
+const reliableResearch = computed(() => outline.value?.textbookResearch?.exactMatch && outline.value.textbookResearch.confidence === 'HIGH' && sources.value.length > 0)
|
|
|
+const totalMinutes = computed(() => pages.value.filter(item => !item.page.optional).reduce((sum, item) => sum + (Number(item.page.durationMinutes) || 0), 0))
|
|
|
+const editorSnapshot = () => JSON.stringify([slidesStore.title, slidesStore.slides, slidesStore.theme, slidesStore.viewportSize, slidesStore.viewportRatio])
|
|
|
+const currentPlan = () => outline.value ? confirmedCoursewareOutline(outline.value, goalsText.value, pages.value) : undefined
|
|
|
+const stopPolling = () => { if (pollTimer) clearTimeout(pollTimer); pollTimer = undefined }
|
|
|
+watch(pageRange, value => { const [min, max] = value.split('-').map(Number); preferences.minPages = min; preferences.maxPages = max })
|
|
|
+watch(() => props.visible, visible => { if (!visible) stopPolling(); else if (task.value) void pollTask(); else if (!outline.value) void restore() })
|
|
|
+watch(() => pptWork.id, () => { version++; stopPolling(); outlineLoading.value = false; task.value = undefined; confirmed.value = undefined; outline.value = undefined; pages.value = []; goalsText.value = ''; extraPrompt.value = ''; Object.assign(preferences, defaults()); pageRange.value = '9-12'; step.value = 1; if (props.visible) void restore() })
|
|
|
+onBeforeUnmount(() => { version++; stopPolling() })
|
|
|
+function close() { if (transferring.value) return; remember(); emit('update:visible', false) }
|
|
|
+function remember() {
|
|
|
+ const id = pptWork.id, from = source(), plan = currentPlan(), prefs = clone(preferences), prompt = extraPrompt.value, taskId = task.value?.taskId
|
|
|
+ if (!id) return
|
|
|
+ saveQueue = saveQueue.then(async () => { await saveAiDraft({ key: await draftKey(id, from), updatedAt: Date.now(), outline: plan, preferences: prefs, extraPrompt: prompt, taskId }) }).catch(error => { console.warn('AI courseware local state unavailable', error) })
|
|
|
+}
|
|
|
+async function restore() {
|
|
|
+ if (!pptWork.id) return
|
|
|
+ const requestVersion = version
|
|
|
try {
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareTaskApi, taskId)
|
|
|
- if (!props.visible || taskInfo.value?.taskId !== taskId) return
|
|
|
- if (res.code !== 200) {
|
|
|
- clearTask()
|
|
|
- handleFinalPreparationFailure(res.message || "未能取得课件生成进度,请重试")
|
|
|
- return
|
|
|
- }
|
|
|
- taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
- if (taskInfo.value.status === "DONE" || taskInfo.value.status === "DRAFT") {
|
|
|
- clearPollTimer()
|
|
|
- await loadDraft(taskId)
|
|
|
- } else if (taskInfo.value.status === "FAILED") {
|
|
|
- clearPollTimer()
|
|
|
- const message = taskInfo.value.message || "AI课件生成失败"
|
|
|
- clearTask()
|
|
|
- console.error("AI课件生成失败", message)
|
|
|
- handleFinalPreparationFailure(message)
|
|
|
- }
|
|
|
- else pollTimer.value = window.setTimeout(() => fetchTask(taskId), 2000)
|
|
|
- } catch {
|
|
|
- if (!props.visible || taskInfo.value?.taskId !== taskId) return
|
|
|
- clearPollTimer()
|
|
|
- handleFinalPreparationFailure("生成进度读取失败,可重新打开窗口恢复任务")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function clearTask() {
|
|
|
- clearPollTimer()
|
|
|
- taskInfo.value = undefined
|
|
|
-}
|
|
|
-
|
|
|
-function clearPollTimer() {
|
|
|
- if (pollTimer.value) {
|
|
|
- window.clearInterval(pollTimer.value)
|
|
|
- pollTimer.value = undefined
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function loadDraft(taskId: string) {
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
|
- if (res.code !== 200) {
|
|
|
- clearTask()
|
|
|
- handleFinalPreparationFailure(res.message || "未能取得最终课件")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const result = res.data as AiCoursewareGenerateResult
|
|
|
- if (!result?.outline?.pages?.length) {
|
|
|
- clearTask()
|
|
|
- handleFinalPreparationFailure("服务器未返回完整课件数据,请重新生成")
|
|
|
- return
|
|
|
- }
|
|
|
- draftOutline.value = result.outline
|
|
|
- serverReviewMessage.value = result.reviewMessage || ""
|
|
|
- availableResources.value = Array.isArray(result.context?.resources) ? result.context.resources : []
|
|
|
- restoreProtectedPageContent(draftOutline.value)
|
|
|
- draftOutline.value.templateId = selectedTemplateId.value
|
|
|
- selectedDraftPageIndex.value = 0
|
|
|
- workflowStep.value = 2
|
|
|
- clearTask()
|
|
|
- await prepareDraftForUse()
|
|
|
-}
|
|
|
-
|
|
|
-function rebuildDraftPreview() {
|
|
|
- if (!draftOutline.value) {
|
|
|
- draftConverted.value = undefined
|
|
|
- draftQuality.value = undefined
|
|
|
- return
|
|
|
- }
|
|
|
- draftOutline.value.style = selectedStyle.value
|
|
|
- draftOutline.value.templateId = selectedTemplateId.value
|
|
|
- draftConverted.value = aiCoursewareToSlides(draftOutline.value, {
|
|
|
- enableAnimations: preferences.enableAnimations,
|
|
|
- paletteVariant: paletteVariant.value,
|
|
|
- templateId: selectedTemplateId.value
|
|
|
- })
|
|
|
- draftQuality.value = inspectAiPptQuality(draftOutline.value, draftConverted.value.slides, preferences)
|
|
|
-}
|
|
|
-
|
|
|
-async function regenerateDraftPage(index: number) {
|
|
|
- if (!draftOutline.value || !pptWork.id) return
|
|
|
- if (editTeachingContent.value) protectedPageContent.delete(index + 1)
|
|
|
- else if (displayedOutline.value?.pages[index]) protectPageContent(displayedOutline.value.pages[index])
|
|
|
- draftPreparationState.value = "optimizing"
|
|
|
- draftFailureMessage.value = "请稍后重试"
|
|
|
- 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()}请只调整第${index + 1}页。${editTeachingContent.value ? "允许按反馈修改本页教学内容,保持整课目标不变。" : "只修改图片和版式,保持页面文字完全不变。"}${feedback ? `用户反馈:${feedback}。` : "请优化视觉组织和信息层级。"}`,
|
|
|
- preferences: { ...preferences },
|
|
|
- regeneratePageNo: index + 1
|
|
|
- })
|
|
|
- if (res.code !== 200) {
|
|
|
- handleFinalPreparationFailure(res.message || "当前页面更新失败,请重试")
|
|
|
- return
|
|
|
- }
|
|
|
- taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
- await persistDraft()
|
|
|
- await pollTask(taskInfo.value.taskId)
|
|
|
-}
|
|
|
-
|
|
|
-async function applyDraft() {
|
|
|
- if (!displayedConverted.value || !displayedOutline.value || working.value) return
|
|
|
- draftPreparationState.value = "optimizing"
|
|
|
- const resourceIssues = await inspectAiPptResources(displayedOutline.value, displayedConverted.value.slides)
|
|
|
- draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
|
|
|
- appendResourceIssues(resourceIssues)
|
|
|
- draftPreparationState.value = "ready"
|
|
|
- if (!draftQuality.value.passed || serverReviewMessage.value) return
|
|
|
- previousCourseware.value = cloneCoursewareData({ title: slidesStore.title, theme: slidesStore.theme, slides: slidesStore.slides, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio })
|
|
|
- if (!await persistDraft()) return
|
|
|
- slidesStore.setViewportSize(1920)
|
|
|
- slidesStore.setViewportRatio(0.5625)
|
|
|
- slidesStore.setTitle(displayedConverted.value.title)
|
|
|
- slidesStore.setTheme(displayedConverted.value.theme)
|
|
|
- slidesStore.setSlides(displayedConverted.value.slides)
|
|
|
- slidesStore.updateSlideIndex(0)
|
|
|
- addHistorySnapshot()
|
|
|
- ElMessage.success("PPT已应用,请检查后手动保存")
|
|
|
- emit("update:visible", false)
|
|
|
-}
|
|
|
-
|
|
|
-async function adjustCurrentPage() {
|
|
|
- if (!displayedConverted.value) return
|
|
|
- await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
|
-}
|
|
|
-
|
|
|
-function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
|
|
|
- sampleBackgroundState.value = payload
|
|
|
- if (payload.status === "error") {
|
|
|
- const page = workspacePage.value || draftOutline.value?.pages[selectedDraftPageIndex.value] || outline.value?.pages[selectedDraftPageIndex.value]
|
|
|
- console.error("AI课件模板背景加载失败", {
|
|
|
- templateId: selectedTemplateId.value,
|
|
|
- pageNo: page?.pageNo || selectedDraftPageIndex.value + 1,
|
|
|
- layout: page?.visualPlan?.layout,
|
|
|
- url: payload.url
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function toggleSampleFeedback(item: string) {
|
|
|
- sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
|
|
|
- ? sampleFeedbackTags.value.filter(value => value !== item)
|
|
|
- : [...sampleFeedbackTags.value, item]
|
|
|
-}
|
|
|
-
|
|
|
-async function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
|
|
|
- if (templateId === selectedTemplateId.value && displayedConverted.value) {
|
|
|
- templatePickerExpanded.value = false
|
|
|
- return
|
|
|
- }
|
|
|
- templateSelectionMode.value = "manual"
|
|
|
- selectedTemplateId.value = templateId
|
|
|
- if (fromSample) {
|
|
|
- templatePickerExpanded.value = false
|
|
|
- showAllTemplates.value = false
|
|
|
- }
|
|
|
- if (displayedOutline.value) {
|
|
|
- draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
- await prepareDraftForUse()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function useRecommendedTemplate() {
|
|
|
- templateSelectionMode.value = "auto"
|
|
|
- selectedTemplateId.value = templateRecommendation.value.id
|
|
|
- if (displayedOutline.value) {
|
|
|
- draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
- await prepareDraftForUse()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string, string> {
|
|
|
- const master = getAiCoursewareTemplateMaster(templateId)
|
|
|
- const cover = master.pages.find(page => page.layout === "cover") || master.pages[0]
|
|
|
- const backgroundImage = resolveTemplateAssetUrl(cover?.backgroundImage)
|
|
|
- if (backgroundImage) return { backgroundImage: `url("${backgroundImage}")` }
|
|
|
- return { background: cover?.background || selectedTemplate.value.palettes[0].background }
|
|
|
-}
|
|
|
-
|
|
|
-function templateFit(templateId: AiCoursewareTemplateId) {
|
|
|
- return outline.value
|
|
|
- ? scoreAiCoursewareTemplate(outline.value, templateId)
|
|
|
- : { templateId, score: 80, level: "usable" as const, label: "可以使用" as const, reason: "生成大纲后评估" }
|
|
|
-}
|
|
|
-
|
|
|
-async function prepareDraftForUse() {
|
|
|
- if (!draftOutline.value) return
|
|
|
- draftPreparationState.value = "optimizing"
|
|
|
+ const saved = await loadAiDraft(await draftKey(pptWork.id, source()))
|
|
|
+ if (!saved || requestVersion !== version || outline.value || !props.visible) return
|
|
|
+ Object.assign(preferences, saved.preferences); pageRange.value = `${preferences.minPages}-${preferences.maxPages}`; extraPrompt.value = saved.extraPrompt
|
|
|
+ if (saved.outline) { loadOutline(saved.outline); step.value = 2 }
|
|
|
+ if (saved.taskId && outline.value) { confirmed.value = currentPlan(); task.value = { taskId: saved.taskId, status: 'RUNNING', progress: 0, message: '正在获取生成进度' }; step.value = 3; await pollTask() }
|
|
|
+ } catch (error) { console.warn('AI courseware restore unavailable', error) }
|
|
|
+}
|
|
|
+function loadOutline(value: AiCoursewareOutline) {
|
|
|
+ outline.value = clone(value); pages.value = editableOutlinePages(value); goalsText.value = value.textbookResearch?.teachingObjectives?.join('\n') || ''
|
|
|
+ templateId.value = value.templateId || recommendAiCoursewareTemplate(value).id
|
|
|
+ const missing = pages.value.filter(item => !Number.isInteger(item.page.durationMinutes) || Number(item.page.durationMinutes) < 1)
|
|
|
+ let remaining = preferences.lessonMinutes - pages.value.filter(item => !missing.includes(item) && !item.page.optional).reduce((sum, item) => sum + Number(item.page.durationMinutes), 0)
|
|
|
+ let required = missing.filter(item => !item.page.optional).length
|
|
|
+ missing.forEach(item => { item.page.durationMinutes = item.page.optional ? 3 : Math.max(1, Math.floor(remaining / Math.max(1, required))); if (!item.page.optional) { remaining -= item.page.durationMinutes; required-- } })
|
|
|
+ validationErrors.value = []
|
|
|
+}
|
|
|
+async function ask(message: string, confirmButtonText: string) {
|
|
|
+ try { await ElMessageBox.confirm(message, '请确认', { confirmButtonText, cancelButtonText: '取消', closeOnClickModal: false }); return true } catch { return false }
|
|
|
+}
|
|
|
+async function generateOutline() {
|
|
|
+ if (!pptWork.id || outlineLoading.value) return
|
|
|
+ if (outline.value && !await ask('重新生成会替换当前大纲,包括你已修改的内容。是否继续?', '重新生成')) return
|
|
|
+ const requestVersion = ++version; outlineLoading.value = true; setupError.value = ''
|
|
|
try {
|
|
|
- normalizeDraftLocally()
|
|
|
- restoreProtectedPageContent(draftOutline.value)
|
|
|
- rebuildDraftPreview()
|
|
|
- if (!draftConverted.value?.slides.length || draftConverted.value.slides.length !== draftOutline.value.pages.length) {
|
|
|
- handleFinalPreparationFailure("课件页面数据不完整,请重新生成")
|
|
|
- return
|
|
|
- }
|
|
|
- publishFinalPreview()
|
|
|
- const resourceIssues = await inspectAiPptResources(displayedOutline.value!, displayedConverted.value!.slides)
|
|
|
- appendResourceIssues(resourceIssues)
|
|
|
- draftFailureMessage.value = "请稍后重试"
|
|
|
- draftPreparationState.value = "ready"
|
|
|
- await persistDraft()
|
|
|
- } catch (error) {
|
|
|
- console.error("AI课件预览转换失败", error)
|
|
|
- handleFinalPreparationFailure(error instanceof Error ? error.message : "课件预览转换失败,请重试")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function normalizeDraftLocally() {
|
|
|
- if (!draftOutline.value) return
|
|
|
- draftOutline.value.pages.forEach((page, index) => {
|
|
|
- if (!page.visualPlan) page.visualPlan = {}
|
|
|
- if (!page.studentContent) page.studentContent = { headline: "", points: [] }
|
|
|
-
|
|
|
- const movedTeacherNotes: string[] = []
|
|
|
- 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))
|
|
|
- .filter(item => {
|
|
|
- if (!item) return false
|
|
|
- if (isTeacherOnlyText(item)) {
|
|
|
- movedTeacherNotes.push(item)
|
|
|
- return false
|
|
|
- }
|
|
|
- const key = normalizeDraftText(item)
|
|
|
- if (!key || excluded.has(key) || pointKeys.has(key)) return false
|
|
|
- pointKeys.add(key)
|
|
|
- return true
|
|
|
- })
|
|
|
- page.studentContent.points = points
|
|
|
- page.bullets = [...points]
|
|
|
- page.visualPlan.visualDensity = points.length <= 2 ? "low" : points.length >= 4 ? "high" : "medium"
|
|
|
-
|
|
|
- if (movedTeacherNotes.length) {
|
|
|
- if (!page.activity) page.activity = { task: "", steps: [] }
|
|
|
- page.activity.steps = uniqueDraftTexts([...(page.activity.steps || []), ...movedTeacherNotes])
|
|
|
- }
|
|
|
-
|
|
|
- if (!page.classroomAction) {
|
|
|
- page.classroomAction = { teacherInstruction: "", studentTask: page.studentContent.headline || page.title, interactionType: "" }
|
|
|
- }
|
|
|
- const studentTask = cleanDraftText(page.classroomAction.studentTask)
|
|
|
- page.classroomAction.studentTask = !studentTask || isTeacherOnlyText(studentTask) ? page.studentContent.headline || page.title : studentTask
|
|
|
-
|
|
|
- const layout = page.visualPlan.layout || ""
|
|
|
- if (["hero_focus", "media_focus", "split_content", "diagram_focus"].includes(layout)) {
|
|
|
- page.visualPlan.focalContent ||= page.title
|
|
|
- page.visualPlan.mainVisual ||= page.imageKeywords?.[0] || `${page.title}的课堂主视觉`
|
|
|
- }
|
|
|
-
|
|
|
- // 同一观察对象可以跨页复用;重复图只给出建议,不擅自删除教学资源。
|
|
|
-
|
|
|
- if (index > 0) {
|
|
|
- const fallbackInstruction = `引导学生完成“${page.classroomAction.studentTask || page.title}”,根据学生的语言、动作或声音表现及时反馈。`
|
|
|
- const instruction = cleanDraftText(page.classroomAction.teacherInstruction || page.teacherTips || page.speakerNotes) || fallbackInstruction
|
|
|
- page.classroomAction.teacherInstruction = instruction
|
|
|
- page.teacherTips ||= instruction
|
|
|
- page.speakerNotes ||= instruction
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function cleanDraftText(value?: string) {
|
|
|
- return String(value || "")
|
|
|
- .replace(/\s+/g, " ")
|
|
|
- .trim()
|
|
|
-}
|
|
|
-
|
|
|
-function normalizeDraftText(value?: string) {
|
|
|
- return cleanDraftText(value)
|
|
|
- .replace(/[,。!?、;:,.!?;:\s]+/g, "")
|
|
|
- .toLowerCase()
|
|
|
-}
|
|
|
-
|
|
|
-function uniqueDraftTexts(values: string[]) {
|
|
|
- const keys = new Set<string>()
|
|
|
- return values.filter(value => {
|
|
|
- const key = normalizeDraftText(value)
|
|
|
- if (!key || keys.has(key)) return false
|
|
|
- keys.add(key)
|
|
|
- return true
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function isTeacherOnlyText(value?: string) {
|
|
|
- const text = cleanDraftText(value)
|
|
|
- return Boolean(text && (TEACHER_VISIBLE_PATTERN.test(text) || PLACEHOLDER_PATTERN.test(text)))
|
|
|
-}
|
|
|
-
|
|
|
-function buildExtraPrompt() {
|
|
|
- return extraPrompt.value.trim()
|
|
|
-}
|
|
|
-
|
|
|
-async function confirmOutline() {
|
|
|
- if (!reliableResearch.value || working.value) return
|
|
|
- outlineConfirmed.value = true
|
|
|
- await handleGenerate()
|
|
|
-}
|
|
|
-
|
|
|
-async function refreshEditedDraft() {
|
|
|
- if (!displayedOutline.value || working.value) return
|
|
|
- draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
- await prepareDraftForUse()
|
|
|
-}
|
|
|
-
|
|
|
-function appendResourceIssues(issues: Awaited<ReturnType<typeof inspectAiPptResources>>) {
|
|
|
- if (!draftQuality.value) return
|
|
|
- draftQuality.value.issues.push(...issues)
|
|
|
- draftQuality.value.errors.push(...issues)
|
|
|
- draftQuality.value.passed = draftQuality.value.errors.length === 0
|
|
|
-}
|
|
|
-
|
|
|
-function pageHasResource(resource: AiCoursewareResourceBinding) {
|
|
|
- return workspacePage.value?.resourceBindings?.some(item => item.resourceType === resource.resourceType && item.resourceId === resource.resourceId) || false
|
|
|
-}
|
|
|
-
|
|
|
-function toggleResource(resource: AiCoursewareResourceBinding) {
|
|
|
- const page = workspacePage.value
|
|
|
- if (!page || working.value) return
|
|
|
- const bindings = (page.resourceBindings || []).filter(item => item.resourceType !== resource.resourceType || item.resourceId !== resource.resourceId)
|
|
|
- if (!pageHasResource(resource)) bindings.push({ ...resource, verified: true, required: true, purpose: "教师为本页选择的教学资源" })
|
|
|
- page.resourceBindings = bindings
|
|
|
- if (resource.resourceType === "IMG") {
|
|
|
- const image = bindings.find(item => item.resourceType === "IMG")
|
|
|
- page.image = image ? { url: image.contentUrl || "", keyword: image.name || "", provider: "SYSTEM" } : undefined
|
|
|
- }
|
|
|
- refreshEditedDraft()
|
|
|
-}
|
|
|
-
|
|
|
-let saveQueue: Promise<unknown> = Promise.resolve()
|
|
|
-async function persistDraft() {
|
|
|
- if (!pptWork.id) return false
|
|
|
- draftSaved.value = false
|
|
|
- const snapshot = cloneCoursewareData({ outline: displayedOutline.value || draftOutline.value || outline.value,
|
|
|
- converted: displayedConverted.value, preferences: { ...preferences }, extraPrompt: extraPrompt.value,
|
|
|
- taskId: taskInfo.value && ['PENDING', 'RUNNING'].includes(taskInfo.value.status) ? taskInfo.value.taskId : undefined,
|
|
|
- reviewMessage: serverReviewMessage.value, resources: availableResources.value, backup: previousCourseware.value,
|
|
|
- protectedContent: Array.from(protectedPageContent.entries()) })
|
|
|
- const id = String(pptWork.id)
|
|
|
- const source = queryParams.pptResourcesType || queryParams.fromType || "TEACHER"
|
|
|
+ const response = await httpAjax(aiCoursewareOutlineApi, { id: pptWork.id, fromType: source(), extraPrompt: extraPrompt.value, preferences: clone(preferences) })
|
|
|
+ if (requestVersion !== version) return
|
|
|
+ if (response.code !== 200 || !response.data?.pages?.length) throw new Error(response.message || '大纲生成未完成,请重试')
|
|
|
+ loadOutline(response.data); confirmed.value = undefined; step.value = 2; remember()
|
|
|
+ } catch (error) { if (requestVersion === version) setupError.value = error instanceof Error ? error.message : '大纲生成未完成,请重试' }
|
|
|
+ finally { if (requestVersion === version) outlineLoading.value = false }
|
|
|
+}
|
|
|
+function canMove(event: { draggedContext: { index: number }; relatedContext: { index: number } }) { return event.draggedContext.index > 0 && event.relatedContext.index > 0 }
|
|
|
+function movePage(index: number, delta: number) { const next = index + delta; if (index < 1 || next < 1 || next >= pages.value.length) return; pages.value.splice(next, 0, pages.value.splice(index, 1)[0]); remember() }
|
|
|
+function addPage() { if (pages.value.length >= MAX_OUTLINE_PAGES) return; pages.value.push({ key: nanoid(), page: { pageNo: pages.value.length + 1, type: 'creative_activity', title: '', bullets: [], imageKeywords: [], teachingStage: '活动练习', durationMinutes: 3, optional: false }, pointsText: '' }) }
|
|
|
+function removePage(index: number) { if (index < 1 || pages.value.length <= MIN_OUTLINE_PAGES) return; pages.value.splice(index, 1); remember() }
|
|
|
+async function confirmAndGenerate() {
|
|
|
+ const plan = currentPlan(); if (!plan || !reliableResearch.value || transferring.value) return
|
|
|
+ validationErrors.value = inspectConfirmedOutline(plan, preferences.lessonMinutes); if (validationErrors.value.length) return
|
|
|
+ if (slidesStore.slides.some(slide => slide.elements.length) && !await ask('生成完成后,将用新课件替换编辑器中的当前内容。你仍可撤销恢复原页面,保存课件后才会更新服务器。', '确认并生成')) return
|
|
|
+ confirmed.value = clone(plan); replacementSnapshot = editorSnapshot(); await startGeneration()
|
|
|
+}
|
|
|
+async function startGeneration() {
|
|
|
+ if (!confirmed.value || !pptWork.id || transferring.value || task.value) return
|
|
|
+ const requestVersion = ++version; step.value = 3; generationError.value = ''; transferring.value = true
|
|
|
try {
|
|
|
- const key = await draftKey(id, source)
|
|
|
- saveQueue = saveQueue.catch(() => undefined).then(() => saveAiDraft({ ...snapshot, key, updatedAt: Date.now() }))
|
|
|
- await saveQueue
|
|
|
- draftSaved.value = true
|
|
|
- return true
|
|
|
- } catch {
|
|
|
- draftSaved.value = false
|
|
|
- ElMessage.warning("本机草稿保存失败,请勿刷新页面,并及时手动保存课件")
|
|
|
- return false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function saveDraftExplicitly() {
|
|
|
- if (await persistDraft()) ElMessage.success("初稿已保存在当前登录会话的本机草稿中,尚未标记为可授课")
|
|
|
-}
|
|
|
-
|
|
|
-async function restoreSavedDraft() {
|
|
|
- if (!pptWork.id) return
|
|
|
- const id = String(pptWork.id)
|
|
|
+ const plan = clone(confirmed.value); plan.templateId = templateId.value; plan.style = getAiCoursewareTemplate(templateId.value).baseStyle
|
|
|
+ const response = await httpAjax(aiCoursewareGenerateApi, { id: pptWork.id, fromType: source(), outline: plan, templateId: templateId.value, outlineConfirmed: true, preferences: { ...clone(preferences), minPages: plan.pages.length, maxPages: plan.pages.length } })
|
|
|
+ if (requestVersion !== version) return
|
|
|
+ if (response.code !== 200 || !response.data?.taskId) throw new Error(response.message || '未能开始生成,请重试')
|
|
|
+ task.value = response.data; remember()
|
|
|
+ } catch (error) { if (requestVersion === version) generationError.value = error instanceof Error ? error.message : '未能开始生成,请重试' }
|
|
|
+ finally { if (requestVersion === version) transferring.value = false }
|
|
|
+ if (props.visible && task.value) await pollTask()
|
|
|
+}
|
|
|
+async function retry() { generationError.value = ''; if (task.value) await pollTask(); else await startGeneration() }
|
|
|
+async function pollTask() {
|
|
|
+ stopPolling(); const taskId = task.value?.taskId, requestVersion = version
|
|
|
+ if (!taskId) return
|
|
|
+ const active = () => requestVersion === version && props.visible && task.value?.taskId === taskId
|
|
|
try {
|
|
|
- const saved = await loadAiDraft(await draftKey(id, queryParams.pptResourcesType || queryParams.fromType || "TEACHER"))
|
|
|
- if (!saved?.outline || outline.value || !props.visible || String(pptWork.id) !== id) return
|
|
|
- Object.assign(preferences, saved.preferences)
|
|
|
- pageRange.value = `${preferences.minPages}-${preferences.maxPages}`
|
|
|
- extraPrompt.value = saved.extraPrompt
|
|
|
- availableResources.value = saved.resources || []
|
|
|
- protectedPageContent.clear()
|
|
|
- for (const [pageNo, content] of saved.protectedContent || []) protectedPageContent.set(pageNo, content)
|
|
|
- previousCourseware.value = saved.backup
|
|
|
- serverReviewMessage.value = saved.reviewMessage || ""
|
|
|
- draftSaved.value = true
|
|
|
- outline.value = saved.outline
|
|
|
- selectedTemplateId.value = getAiCoursewareTemplate(saved.outline.templateId).id
|
|
|
- draftOutline.value = cloneCoursewareData(saved.outline)
|
|
|
- workflowStep.value = 2
|
|
|
- outlineConfirmed.value = Boolean(saved.converted || saved.taskId)
|
|
|
- if (saved.converted) {
|
|
|
- displayedOutline.value = cloneCoursewareData(saved.outline)
|
|
|
- displayedConverted.value = saved.converted
|
|
|
- displayedTemplateId.value = selectedTemplateId.value
|
|
|
- draftConverted.value = cloneCoursewareData(saved.converted)
|
|
|
- draftQuality.value = inspectAiPptQuality(saved.outline, saved.converted.slides, preferences)
|
|
|
- draftPreparationState.value = "ready"
|
|
|
- }
|
|
|
- if (saved.taskId) {
|
|
|
- taskInfo.value = { taskId: saved.taskId, status: "RUNNING", progress: 0, message: "恢复生成进度" }
|
|
|
- draftPreparationState.value = "optimizing"
|
|
|
- await pollTask(saved.taskId)
|
|
|
- }
|
|
|
- ElMessage.success("已恢复本机AI课件草稿")
|
|
|
- } catch { ElMessage.warning("无法读取本机草稿,可重新生成") }
|
|
|
-}
|
|
|
-
|
|
|
-function restorePreviousCourseware() {
|
|
|
- const backup = previousCourseware.value
|
|
|
- if (!backup || working.value) return
|
|
|
- slidesStore.setTitle(backup.title)
|
|
|
- slidesStore.setTheme(backup.theme)
|
|
|
- slidesStore.setViewportSize(backup.width)
|
|
|
- slidesStore.setViewportRatio(backup.ratio)
|
|
|
- slidesStore.setSlides(cloneCoursewareData(backup.slides))
|
|
|
- slidesStore.updateSlideIndex(0)
|
|
|
- addHistorySnapshot()
|
|
|
- ElMessage.success("已恢复应用前的课件,请手动保存")
|
|
|
-}
|
|
|
-
|
|
|
-function buildVisualPrompt() {
|
|
|
- const base = selectedTemplate.value.visualPrompt
|
|
|
- const system = `模板布局族:${selectedTemplate.value.layoutFamily}。字体体系:${selectedTemplate.value.typography}。背景体系:${selectedTemplate.value.backgroundStyle}。素材画风:${selectedTemplate.value.illustrationStyle}。`
|
|
|
- const layouts = [...new Set(selectedTemplate.value.layoutCapabilities || Object.values(selectedTemplate.value.pageLayouts))].join("、")
|
|
|
- const asset =
|
|
|
- preferences.assetStyle === "illustration"
|
|
|
- ? "主视觉使用统一画风插画。"
|
|
|
- : preferences.assetStyle === "photo"
|
|
|
- ? "主视觉优先真实照片。"
|
|
|
- : "插画和真实照片按教学内容合理混用。"
|
|
|
- const source =
|
|
|
- preferences.sourceImagePolicy === "strict"
|
|
|
- ? "优先保留教材原图,不随意替换。"
|
|
|
- : preferences.sourceImagePolicy === "creative"
|
|
|
- ? "允许根据教学目标重新创作主视觉。"
|
|
|
- : "教材原图与新视觉保持平衡。"
|
|
|
- const palette =
|
|
|
- paletteVariant.value === 1
|
|
|
- ? "配色使用清新蓝绿体系。"
|
|
|
- : paletteVariant.value === 2
|
|
|
- ? "配色使用柔和紫色与珊瑚色体系。"
|
|
|
- : "配色保持明快且有清晰对比。"
|
|
|
- return `${base}${system}可用视觉版式:${layouts}。版式只按每页真实内容需求选择,不得反向增加歌词、谱例、聆听或活动页面。模板场景:${selectedTemplate.value.scene}。${asset}${source}${palette}整套课件必须共享角色外观、环境、光线和重复道具;一页只保留一个视觉焦点。`
|
|
|
-}
|
|
|
-
|
|
|
-function formatPageNo(pageNo?: number) {
|
|
|
- return String(pageNo || 0).padStart(2, "0")
|
|
|
-}
|
|
|
-
|
|
|
-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 "优化设计"
|
|
|
- if (message.includes("图片") || message.includes("检索")) return "匹配图片"
|
|
|
- if (message.includes("完成")) return "生成完成"
|
|
|
- if (message.includes("失败")) return "生成失败"
|
|
|
- return message.length > 4 ? message.slice(0, 4) : message
|
|
|
+ const response = await httpAjax(aiCoursewareTaskApi, taskId); if (!active()) return
|
|
|
+ if (response.code !== 200) throw new Error(response.message || '暂时无法获取进度,请稍后继续查看')
|
|
|
+ task.value = response.data
|
|
|
+ if (task.value?.status === 'FAILED') { generationError.value = task.value.message || '课件生成未完成,请重试'; console.error('AI courseware failed', taskId, task.value.message); task.value = undefined; remember(); return }
|
|
|
+ if (task.value?.status === 'DONE' || task.value?.status === 'DRAFT') {
|
|
|
+ const result = await httpAjax(aiCoursewareResultApi, taskId); if (!active()) return
|
|
|
+ if (result.code !== 200) throw new Error(result.message || '课件读取失败,请继续查看')
|
|
|
+ await enterEditor(result.data, active)
|
|
|
+ } else pollTimer = setTimeout(pollTask, 2000)
|
|
|
+ } catch (error) { if (active()) generationError.value = error instanceof Error ? error.message : '暂时无法读取课件,请继续查看' }
|
|
|
+}
|
|
|
+async function enterEditor(result: AiCoursewareGenerateResult, active: () => boolean) {
|
|
|
+ if (!result?.outline?.pages?.length) throw new Error('尚未获得课件内容,请继续查看')
|
|
|
+ const converted = aiCoursewareToSlides(result.outline, { templateId: templateId.value, enableAnimations: preferences.enableAnimations })
|
|
|
+ const issues = [...inspectAiPptQuality(result.outline, converted.slides, { ...preferences, minPages: result.outline.pages.length, maxPages: result.outline.pages.length }).errors, ...await inspectAiPptResources(result.outline, converted.slides)]
|
|
|
+ if (result.reviewMessage) issues.unshift({ code: 'teaching-review', severity: 'warning', message: result.reviewMessage })
|
|
|
+ if (!active()) return
|
|
|
+ if (replacementSnapshot !== editorSnapshot() && slidesStore.slides.some(slide => slide.elements.length) && !await ask('当前课件已有新的修改。是否用生成的课件替换?替换前的页面可撤销恢复。', '进入新课件')) { generationError.value = '生成结果已就绪,可在准备好后继续进入编辑器。'; return }
|
|
|
+ if (!active()) return
|
|
|
+ transferring.value = true
|
|
|
+ const previous = clone({ title: slidesStore.title, slides: slidesStore.slides, theme: slidesStore.theme, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio, index: slidesStore.slideIndex })
|
|
|
+ let changed = false
|
|
|
+ try {
|
|
|
+ await snapshots.addSnapshot(); if (!active()) return
|
|
|
+ changed = true; slidesStore.setViewportSize(1920); slidesStore.setViewportRatio(0.5625); slidesStore.setTitle(converted.title); slidesStore.setTheme({ ...slidesStore.theme, ...converted.theme }); slidesStore.setSlides(converted.slides); slidesStore.setSlideIndex(0); useMainStore().setActiveElementIdList([])
|
|
|
+ await snapshots.addSnapshot(); pptWork.isSave = false; task.value = undefined; generationError.value = ''; step.value = 2; remember(); emit('generated', issues); emit('update:visible', false); ElMessage.success('课件已生成,可直接编辑,完成后点击“保存课件”')
|
|
|
+ } catch (error) { if (changed) { slidesStore.setTitle(previous.title); slidesStore.setTheme(previous.theme); slidesStore.setSlides(previous.slides); slidesStore.setViewportSize(previous.width); slidesStore.setViewportRatio(previous.ratio); slidesStore.setSlideIndex(previous.index) } throw error }
|
|
|
+ finally { transferring.value = false }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
-<style lang="scss" scoped>
|
|
|
-.draft-notice {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- padding: 8px 12px;
|
|
|
- margin: 8px 0;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff7e8;
|
|
|
- color: #76552b;
|
|
|
- font-size: 13px;
|
|
|
- flex-shrink: 0;
|
|
|
- > span { flex: 1; }
|
|
|
- > button:not(.button) { color: #1677d2; border: 0; background: none; cursor: pointer; }
|
|
|
-}
|
|
|
-.draft-save-status { font-size: 12px; color: #7b8794; margin-right: auto; }
|
|
|
-.more-actions {
|
|
|
- position: relative;
|
|
|
- font-size: 13px;
|
|
|
- summary { cursor: pointer; color: #64748b; }
|
|
|
-}
|
|
|
-.more-actions-menu {
|
|
|
- position: absolute;
|
|
|
- right: 0;
|
|
|
- bottom: 26px;
|
|
|
- z-index: 10;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 6px;
|
|
|
- width: 160px;
|
|
|
- padding: 8px;
|
|
|
- border: 1px solid #e1e8f0;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- box-shadow: 0 4px 18px rgba(31, 41, 51, 0.12);
|
|
|
-}
|
|
|
-.teaching-review, .quality-panel {
|
|
|
- margin: 10px 0;
|
|
|
- padding: 12px 16px;
|
|
|
- border: 1px solid #cbd5df;
|
|
|
- background: #f8fafc;
|
|
|
- border-radius: 6px;
|
|
|
- line-height: 1.6;
|
|
|
- max-height: 260px;
|
|
|
- overflow: auto;
|
|
|
- flex-shrink: 0;
|
|
|
- summary { cursor: pointer; font-weight: 600; }
|
|
|
- label, a { display: block; margin: 6px 0; }
|
|
|
- input:not([type="checkbox"]), textarea { border: 1px solid #aebdca; padding: 6px; max-width: 100%; }
|
|
|
- textarea { width: 95%; min-height: 70px; }
|
|
|
- button { display: block; padding: 4px 0; background: transparent; text-align: left; color: #8b3419; cursor: pointer; }
|
|
|
- span { margin-left: 12px; }
|
|
|
-}
|
|
|
-.outline-plan label { display: grid; grid-template-columns: 130px 1fr 2fr; gap: 8px; }
|
|
|
-.ai-dialog {
|
|
|
- max-height: calc(100vh - 64px);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- color: #1f2933;
|
|
|
-}
|
|
|
-.ai-dialog.review-mode {
|
|
|
- height: 82vh;
|
|
|
-}
|
|
|
-.ai-dialog.review-mode .header {
|
|
|
- .title {
|
|
|
- font-size: 22px;
|
|
|
- line-height: 1.25;
|
|
|
- }
|
|
|
- .sub {
|
|
|
- margin-top: 3px;
|
|
|
- font-size: 13px;
|
|
|
- }
|
|
|
-}
|
|
|
-.header,
|
|
|
-.footer {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
-}
|
|
|
-.header,
|
|
|
-.workflow-steps,
|
|
|
-.footer {
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-.title {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: 700;
|
|
|
- color: #1f2933;
|
|
|
-}
|
|
|
-.sub {
|
|
|
- margin-top: 6px;
|
|
|
- color: #7b8794;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.workflow-steps {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
- margin-top: 12px;
|
|
|
- padding: 4px;
|
|
|
- border-radius: 8px;
|
|
|
- background: #f4f7fa;
|
|
|
-}
|
|
|
-.workflow-step {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- gap: 8px;
|
|
|
- height: 34px;
|
|
|
- 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: 0 1 auto;
|
|
|
- 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;
|
|
|
-}
|
|
|
-.loading-strip {
|
|
|
- margin-top: 12px;
|
|
|
- height: 42px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-.loading-strip {
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
-}
|
|
|
-.loading-dot {
|
|
|
- width: 14px;
|
|
|
- height: 14px;
|
|
|
- border: 2px solid #d7e7ff;
|
|
|
- border-top-color: $themeColor;
|
|
|
- border-radius: 50%;
|
|
|
- animation: loading-rotate 0.8s linear infinite;
|
|
|
-}
|
|
|
-@keyframes loading-rotate {
|
|
|
- to {
|
|
|
- transform: rotate(360deg);
|
|
|
- }
|
|
|
-}
|
|
|
-.extra {
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.setup-primary-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
- gap: 12px;
|
|
|
- margin-top: 14px;
|
|
|
- label > span {
|
|
|
- display: block;
|
|
|
- margin-bottom: 6px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- }
|
|
|
- select {
|
|
|
- width: 100%;
|
|
|
- height: 38px;
|
|
|
- padding: 0 10px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 7px;
|
|
|
- background: #fff;
|
|
|
- color: #33404d;
|
|
|
- }
|
|
|
-}
|
|
|
-.template-real-cover {
|
|
|
- display: block;
|
|
|
- width: 116px;
|
|
|
- aspect-ratio: 16 / 9;
|
|
|
- border-radius: 6px;
|
|
|
- background-position: center;
|
|
|
- background-size: cover;
|
|
|
- box-shadow: inset 0 0 0 1px rgba(31, 41, 51, 0.08);
|
|
|
- &.option {
|
|
|
- width: 100%;
|
|
|
- margin-bottom: 8px;
|
|
|
- }
|
|
|
-}
|
|
|
-.compact-template-selector {
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #edf1f5;
|
|
|
- border-radius: 8px;
|
|
|
-}
|
|
|
-.show-all-templates,
|
|
|
-.advanced-toggle,
|
|
|
-.outline-toggle {
|
|
|
- margin-top: 10px;
|
|
|
- padding: 0;
|
|
|
- border: 0;
|
|
|
- background: transparent;
|
|
|
- color: $themeColor;
|
|
|
- font-size: 12px;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-.advanced-toggle {
|
|
|
- color: #657381;
|
|
|
-}
|
|
|
-.advanced-settings {
|
|
|
- margin-top: 8px;
|
|
|
-}
|
|
|
-.generation-preferences {
|
|
|
- margin-top: 12px;
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #e5eaf0;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fafcff;
|
|
|
-}
|
|
|
-.preference-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(4, minmax(140px, 1fr));
|
|
|
- gap: 10px;
|
|
|
- label,
|
|
|
- span {
|
|
|
- display: block;
|
|
|
- }
|
|
|
- span {
|
|
|
- margin-bottom: 5px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 12px;
|
|
|
- }
|
|
|
- input,
|
|
|
- select {
|
|
|
- width: 100%;
|
|
|
- height: 34px;
|
|
|
- padding: 0 8px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- }
|
|
|
-}
|
|
|
-.media-options {
|
|
|
- display: flex;
|
|
|
- gap: 18px;
|
|
|
- margin-top: 10px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- input {
|
|
|
- margin-right: 6px;
|
|
|
- }
|
|
|
-}
|
|
|
-.style-selector {
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.template-selector-head {
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 16px;
|
|
|
-}
|
|
|
-.template-help {
|
|
|
- margin-top: -3px;
|
|
|
- color: #7b8794;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.auto-template-button {
|
|
|
- flex-shrink: 0;
|
|
|
- padding: 5px 10px;
|
|
|
- border: 1px solid #cfe0f6;
|
|
|
- border-radius: 6px;
|
|
|
- background: #f5f9ff;
|
|
|
- color: $themeColor;
|
|
|
- font-size: 12px;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-.template-recommendation {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 8px;
|
|
|
- padding: 8px 10px;
|
|
|
- border-radius: 6px;
|
|
|
- background: #eef6ff;
|
|
|
- color: #52616b;
|
|
|
- font-size: 12px;
|
|
|
- span:first-child {
|
|
|
- padding: 2px 6px;
|
|
|
- border-radius: 999px;
|
|
|
- background: $themeColor;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
- strong {
|
|
|
- color: #1f2933;
|
|
|
- }
|
|
|
-}
|
|
|
-.style-options {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- gap: 10px;
|
|
|
- margin-top: 9px;
|
|
|
-}
|
|
|
-.style-option {
|
|
|
- min-width: 0;
|
|
|
- padding: 9px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fff;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-.style-option strong,
|
|
|
-.style-option span,
|
|
|
-.style-option small {
|
|
|
- display: block;
|
|
|
-}
|
|
|
-.style-option > span:not(.template-mini):not(.template-card-title) {
|
|
|
- margin-top: 4px;
|
|
|
- color: #7b8794;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.style-option small {
|
|
|
- margin-top: 6px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.style-option.active {
|
|
|
- border-color: $themeColor;
|
|
|
- box-shadow: 0 0 0 2px rgba($themeColor, 0.12);
|
|
|
-}
|
|
|
-.template-mini {
|
|
|
- position: relative;
|
|
|
- height: 66px;
|
|
|
- overflow: hidden;
|
|
|
- margin-bottom: 8px;
|
|
|
- border-radius: 6px;
|
|
|
- background: var(--template-background);
|
|
|
- box-shadow: inset 0 0 0 1px rgba(31, 41, 51, 0.06);
|
|
|
- i {
|
|
|
- position: absolute;
|
|
|
- display: block;
|
|
|
- }
|
|
|
- .mini-band {
|
|
|
- left: 0;
|
|
|
- top: 0;
|
|
|
- width: 32%;
|
|
|
- height: 100%;
|
|
|
- background: var(--template-primary);
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 38%;
|
|
|
- top: 13px;
|
|
|
- width: 42%;
|
|
|
- height: 7px;
|
|
|
- border-radius: 4px;
|
|
|
- background: var(--template-primary);
|
|
|
- }
|
|
|
- .mini-content {
|
|
|
- left: 38%;
|
|
|
- width: 52%;
|
|
|
- height: 14px;
|
|
|
- border-radius: 3px;
|
|
|
- background: var(--template-surface);
|
|
|
- box-shadow: 0 2px 5px rgba(31, 41, 51, 0.08);
|
|
|
- }
|
|
|
- .mini-content.first {
|
|
|
- top: 29px;
|
|
|
- }
|
|
|
- .mini-content.second {
|
|
|
- top: 47px;
|
|
|
- width: 37%;
|
|
|
- }
|
|
|
- .mini-accent {
|
|
|
- right: 7%;
|
|
|
- bottom: 6px;
|
|
|
- width: 11px;
|
|
|
- height: 11px;
|
|
|
- border-radius: 50%;
|
|
|
- background: var(--template-accent);
|
|
|
- }
|
|
|
-}
|
|
|
-.template-card-title {
|
|
|
- display: flex !important;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 8px;
|
|
|
- em {
|
|
|
- padding: 2px 5px;
|
|
|
- border-radius: 999px;
|
|
|
- background: #eaf7f4;
|
|
|
- color: #168f86;
|
|
|
- font-size: 10px;
|
|
|
- font-style: normal;
|
|
|
- font-weight: 600;
|
|
|
- }
|
|
|
-}
|
|
|
-.label {
|
|
|
- margin-bottom: 8px;
|
|
|
- font-size: 14px;
|
|
|
- color: #52616b;
|
|
|
-}
|
|
|
-.outline {
|
|
|
- margin-top: 10px;
|
|
|
- min-height: 0;
|
|
|
- flex: 1;
|
|
|
- display: block;
|
|
|
- overflow: auto;
|
|
|
- padding-right: 4px;
|
|
|
-}
|
|
|
-.unified-review {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.final-preview-placeholder {
|
|
|
- min-height: 320px;
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 8px;
|
|
|
- border: 1px solid #e1e8f0;
|
|
|
- border-radius: 8px;
|
|
|
- background: #f7f9fc;
|
|
|
- color: #52616b;
|
|
|
- strong {
|
|
|
- color: #263442;
|
|
|
- font-size: 15px;
|
|
|
- }
|
|
|
- small {
|
|
|
- color: #7b8794;
|
|
|
- font-size: 12px;
|
|
|
- }
|
|
|
- &.failed {
|
|
|
- background: #fffaf3;
|
|
|
- }
|
|
|
- .loading-dot {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
- }
|
|
|
- :deep(.button.primary) {
|
|
|
- min-width: 104px;
|
|
|
- margin-top: 6px;
|
|
|
- }
|
|
|
-}
|
|
|
-.placeholder-icon {
|
|
|
- display: grid;
|
|
|
- width: 28px;
|
|
|
- height: 28px;
|
|
|
- place-items: center;
|
|
|
- border: 1px solid #e5b66d;
|
|
|
- border-radius: 50%;
|
|
|
- color: #a46108;
|
|
|
- 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;
|
|
|
- overflow: auto;
|
|
|
- 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 {
|
|
|
- position: relative;
|
|
|
- min-height: 180px;
|
|
|
- flex: 1 0 180px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- overflow: hidden;
|
|
|
- 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-processing-overlay {
|
|
|
- position: absolute;
|
|
|
- inset: 10px;
|
|
|
- z-index: 5;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- flex-direction: column;
|
|
|
- gap: 7px;
|
|
|
- border-radius: 6px;
|
|
|
- background: rgba(245, 248, 252, 0.92);
|
|
|
- color: #52616b;
|
|
|
- backdrop-filter: blur(2px);
|
|
|
- strong {
|
|
|
- color: #263442;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- small {
|
|
|
- color: #7b8794;
|
|
|
- font-size: 11px;
|
|
|
- }
|
|
|
-}
|
|
|
-.workspace-feedback {
|
|
|
- flex: 0 0 auto;
|
|
|
- margin-top: 8px;
|
|
|
- padding-top: 8px;
|
|
|
- summary { color: #52616b; font-size: 13px; cursor: pointer; }
|
|
|
- &[open] summary { margin-bottom: 10px; }
|
|
|
-}
|
|
|
-.workspace-preview-column > .teaching-review {
|
|
|
- flex-shrink: 0;
|
|
|
- margin-bottom: 0;
|
|
|
- padding: 8px 12px;
|
|
|
- summary { font-size: 13px; }
|
|
|
-}
|
|
|
-.sample-stage-head {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 12px;
|
|
|
- button {
|
|
|
- height: 30px;
|
|
|
- padding: 0 10px;
|
|
|
- border: 1px solid #cbd9e8;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- color: $themeColor;
|
|
|
- font-size: 12px;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-template-current {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- min-width: 0;
|
|
|
- margin-right: auto;
|
|
|
- gap: 7px;
|
|
|
- color: #657381;
|
|
|
- font-size: 12px;
|
|
|
- strong {
|
|
|
- overflow: hidden;
|
|
|
- color: #1f2933;
|
|
|
- font-size: 14px;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
- small {
|
|
|
- flex-shrink: 0;
|
|
|
- padding: 2px 6px;
|
|
|
- border-radius: 999px;
|
|
|
- background: #eaf7f4;
|
|
|
- color: #168f86;
|
|
|
- font-size: 10px;
|
|
|
- font-weight: 600;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-carousel-shell {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 30px minmax(0, 1fr) 30px;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- margin-top: 8px;
|
|
|
-}
|
|
|
-.sample-carousel-arrow {
|
|
|
- width: 30px;
|
|
|
- height: 30px;
|
|
|
- padding: 0;
|
|
|
- border: 1px solid #d8e1eb;
|
|
|
- border-radius: 50%;
|
|
|
- background: #fff;
|
|
|
- color: #52616b;
|
|
|
- font-size: 21px;
|
|
|
- line-height: 26px;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0 3px 10px rgba(31, 41, 51, 0.08);
|
|
|
- &:hover:not(:disabled) {
|
|
|
- border-color: $themeColor;
|
|
|
- color: $themeColor;
|
|
|
- }
|
|
|
- &:disabled {
|
|
|
- opacity: 0.28;
|
|
|
- cursor: default;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-thumbnail-track {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- min-width: 0;
|
|
|
- overflow-x: auto;
|
|
|
- padding: 2px;
|
|
|
- scroll-behavior: smooth;
|
|
|
- scroll-snap-type: x proximity;
|
|
|
- scrollbar-width: none;
|
|
|
- &::-webkit-scrollbar {
|
|
|
- display: none;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-thumbnail-item {
|
|
|
- width: 120px;
|
|
|
- flex: 0 0 120px;
|
|
|
- padding: 0;
|
|
|
- overflow: hidden;
|
|
|
- border: 2px solid transparent;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- color: #52616b;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- scroll-snap-align: start;
|
|
|
- box-shadow: 0 2px 7px rgba(31, 41, 51, 0.07);
|
|
|
- transition:
|
|
|
- border-color 0.16s ease,
|
|
|
- box-shadow 0.16s ease;
|
|
|
- &.active {
|
|
|
- border-color: $themeColor;
|
|
|
- box-shadow: 0 0 0 2px rgba($themeColor, 0.1);
|
|
|
- }
|
|
|
- > span {
|
|
|
- display: block;
|
|
|
- padding: 5px 6px 6px;
|
|
|
- overflow: hidden;
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 600;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-}
|
|
|
-.compact-template-panel {
|
|
|
- margin-top: 8px;
|
|
|
- padding: 10px;
|
|
|
- border: 1px solid #e5eaf0;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fafcff;
|
|
|
- .sample-template-picker {
|
|
|
- grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
- }
|
|
|
- .template-real-cover.option {
|
|
|
- width: 86px;
|
|
|
- flex-shrink: 0;
|
|
|
- margin: 0;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-detail-section {
|
|
|
- margin-top: 10px;
|
|
|
-}
|
|
|
-.sample-detail-heading {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- min-height: 26px;
|
|
|
- gap: 16px;
|
|
|
- margin-bottom: 6px;
|
|
|
- > div {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- min-width: 0;
|
|
|
- gap: 8px;
|
|
|
- }
|
|
|
- span {
|
|
|
- flex-shrink: 0;
|
|
|
- color: $themeColor;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 700;
|
|
|
- }
|
|
|
- strong {
|
|
|
- overflow: hidden;
|
|
|
- color: #1f2933;
|
|
|
- font-size: 15px;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
- small {
|
|
|
- overflow: hidden;
|
|
|
- max-width: 48%;
|
|
|
- color: #8a96a3;
|
|
|
- font-size: 11px;
|
|
|
- text-align: right;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-detail-layout {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: minmax(260px, 0.68fr) minmax(0, 1.48fr);
|
|
|
- align-items: stretch;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-.sample-outline-panel,
|
|
|
-.sample-preview-column {
|
|
|
- min-width: 0;
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #dfe6ee;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fff;
|
|
|
-}
|
|
|
-.sample-outline-panel {
|
|
|
- display: flex;
|
|
|
-}
|
|
|
-.sample-preview-column {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-}
|
|
|
-.simplified-page-editor {
|
|
|
- display: flex;
|
|
|
- width: 100%;
|
|
|
- flex-direction: column;
|
|
|
- padding: 0;
|
|
|
- overflow: visible;
|
|
|
- border: 0;
|
|
|
- border-radius: 0;
|
|
|
- background: transparent;
|
|
|
- .title-editor {
|
|
|
- max-width: none;
|
|
|
- }
|
|
|
-}
|
|
|
-.simplified-page-editor .field-card {
|
|
|
- margin-top: 11px;
|
|
|
- padding: 11px 0 0;
|
|
|
- border: 0;
|
|
|
- border-top: 1px solid #edf1f5;
|
|
|
- border-radius: 0;
|
|
|
- background: transparent;
|
|
|
-}
|
|
|
-.simplified-page-editor .page-bullets {
|
|
|
- min-height: 124px;
|
|
|
- height: 124px;
|
|
|
-}
|
|
|
-.content-readability-hint {
|
|
|
- margin-top: 6px;
|
|
|
- color: #b76e00;
|
|
|
- font-size: 11px;
|
|
|
- line-height: 1.45;
|
|
|
-}
|
|
|
-.representative-samples {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- gap: 6px;
|
|
|
- margin-bottom: 10px;
|
|
|
- button {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 24px 1fr;
|
|
|
- grid-template-rows: auto auto;
|
|
|
- column-gap: 6px;
|
|
|
- align-items: center;
|
|
|
- min-width: 0;
|
|
|
- padding: 7px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- color: #52616b;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
- button.active {
|
|
|
- border-color: $themeColor;
|
|
|
- background: #f5f9ff;
|
|
|
- }
|
|
|
- button.approved span {
|
|
|
- background: #1f9d69;
|
|
|
- }
|
|
|
- span {
|
|
|
- grid-row: 1 / 3;
|
|
|
- display: grid;
|
|
|
- width: 24px;
|
|
|
- height: 24px;
|
|
|
- place-items: center;
|
|
|
- border-radius: 50%;
|
|
|
- background: $themeColor;
|
|
|
- color: #fff;
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 700;
|
|
|
- }
|
|
|
- strong,
|
|
|
- small {
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
- strong {
|
|
|
- font-size: 11px;
|
|
|
- }
|
|
|
- small {
|
|
|
- color: #8a96a3;
|
|
|
- font-size: 9px;
|
|
|
- }
|
|
|
- &.story {
|
|
|
- .mini-band {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- opacity: 0.22;
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 10%;
|
|
|
- top: 12px;
|
|
|
- width: 46%;
|
|
|
- }
|
|
|
- .mini-content {
|
|
|
- left: 10%;
|
|
|
- background: rgba(255, 255, 255, 0.9);
|
|
|
- }
|
|
|
- .mini-accent {
|
|
|
- right: 12%;
|
|
|
- bottom: 13px;
|
|
|
- width: 30px;
|
|
|
- height: 30px;
|
|
|
- opacity: 0.85;
|
|
|
- }
|
|
|
- }
|
|
|
- &.activity {
|
|
|
- .mini-band {
|
|
|
- left: 7%;
|
|
|
- top: 12px;
|
|
|
- width: 18%;
|
|
|
- height: 42px;
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 32%;
|
|
|
- width: 48%;
|
|
|
- }
|
|
|
- .mini-content.first {
|
|
|
- left: 32%;
|
|
|
- top: 29px;
|
|
|
- width: 58%;
|
|
|
- }
|
|
|
- .mini-content.second {
|
|
|
- left: 32%;
|
|
|
- top: 47px;
|
|
|
- width: 45%;
|
|
|
- }
|
|
|
- }
|
|
|
- &.technology {
|
|
|
- background: var(--template-dark, #102744);
|
|
|
- .mini-band {
|
|
|
- width: 100%;
|
|
|
- height: 3px;
|
|
|
- opacity: 0.8;
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 10%;
|
|
|
- background: var(--template-secondary);
|
|
|
- }
|
|
|
- .mini-content {
|
|
|
- left: 10%;
|
|
|
- width: 76%;
|
|
|
- background: rgba(255, 255, 255, 0.15);
|
|
|
- }
|
|
|
- .mini-accent {
|
|
|
- border-radius: 3px;
|
|
|
- }
|
|
|
- }
|
|
|
- &.culture {
|
|
|
- .mini-band {
|
|
|
- left: 78%;
|
|
|
- width: 14%;
|
|
|
- height: 100%;
|
|
|
- opacity: 0.18;
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 10%;
|
|
|
- width: 44%;
|
|
|
- }
|
|
|
- .mini-content {
|
|
|
- left: 10%;
|
|
|
- width: 58%;
|
|
|
- background: var(--template-soft);
|
|
|
- box-shadow: none;
|
|
|
- }
|
|
|
- }
|
|
|
- &.academic {
|
|
|
- .mini-band {
|
|
|
- width: 4%;
|
|
|
- }
|
|
|
- .mini-title {
|
|
|
- left: 12%;
|
|
|
- width: 54%;
|
|
|
- }
|
|
|
- .mini-content {
|
|
|
- left: 12%;
|
|
|
- width: 76%;
|
|
|
- border-radius: 0;
|
|
|
- box-shadow: none;
|
|
|
- }
|
|
|
- .mini-accent {
|
|
|
- border-radius: 1px;
|
|
|
- }
|
|
|
- }
|
|
|
- em.fit-usable {
|
|
|
- background: #fff7e6;
|
|
|
- color: #b76e00;
|
|
|
- }
|
|
|
- em.fit-low {
|
|
|
- background: #fff1f0;
|
|
|
- color: #cf1322;
|
|
|
- }
|
|
|
-}
|
|
|
-.template-fit-warning {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 12px;
|
|
|
- margin-top: 10px;
|
|
|
- padding: 9px 12px;
|
|
|
- border: 1px solid #ffd591;
|
|
|
- border-radius: 7px;
|
|
|
- background: #fffbe6;
|
|
|
- color: #874d00;
|
|
|
- font-size: 12px;
|
|
|
- button {
|
|
|
- flex: 0 0 auto;
|
|
|
- border: 0;
|
|
|
- background: transparent;
|
|
|
- color: $themeColor;
|
|
|
- font-weight: 700;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
-}
|
|
|
-.template-fit-warning.compact {
|
|
|
- margin: 0 0 10px;
|
|
|
-}
|
|
|
-.template-load-error {
|
|
|
- margin-bottom: 10px;
|
|
|
- color: #b42318;
|
|
|
- font-size: 12px;
|
|
|
- overflow-wrap: anywhere;
|
|
|
-}
|
|
|
-.sample-template-picker {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
- gap: 6px;
|
|
|
- margin: -2px 0 10px;
|
|
|
-}
|
|
|
-.sample-template-option {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- min-width: 0;
|
|
|
- padding: 7px 8px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- &.active {
|
|
|
- border-color: $themeColor;
|
|
|
- background: #f5f9ff;
|
|
|
- }
|
|
|
- > span:last-child {
|
|
|
- min-width: 0;
|
|
|
- }
|
|
|
- strong,
|
|
|
- small {
|
|
|
- display: block;
|
|
|
- }
|
|
|
- strong {
|
|
|
- overflow: hidden;
|
|
|
- color: #33404d;
|
|
|
- font-size: 11px;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
- small {
|
|
|
- margin-top: 2px;
|
|
|
- color: #8a96a3;
|
|
|
- font-size: 9px;
|
|
|
- }
|
|
|
-}
|
|
|
-.template-swatches {
|
|
|
- display: flex;
|
|
|
- flex-shrink: 0;
|
|
|
- overflow: hidden;
|
|
|
- width: 34px;
|
|
|
- height: 28px;
|
|
|
- border-radius: 4px;
|
|
|
- i {
|
|
|
- flex: 1;
|
|
|
- }
|
|
|
-}
|
|
|
-.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: auto;
|
|
|
- padding-top: 10px;
|
|
|
- border-top: 1px solid #e5eaf0;
|
|
|
-}
|
|
|
-.feedback-label {
|
|
|
- color: #52616b;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.feedback-chips {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 6px;
|
|
|
- margin-top: 6px;
|
|
|
- button {
|
|
|
- height: 26px;
|
|
|
- padding: 0 9px;
|
|
|
- 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: 48px;
|
|
|
- margin: 0;
|
|
|
- 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-feedback-action {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: minmax(0, 1fr) auto;
|
|
|
- align-items: end;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 6px;
|
|
|
- :deep(.button) {
|
|
|
- height: 34px;
|
|
|
- line-height: 34px;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-}
|
|
|
-.sample-canvas {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- overflow: auto;
|
|
|
- min-height: 0;
|
|
|
- flex: 1 1 auto;
|
|
|
- padding: 10px;
|
|
|
- border: 0;
|
|
|
- border-radius: 6px;
|
|
|
- background: #f1f4f8;
|
|
|
- :deep(.thumbnail-slide) {
|
|
|
- flex-shrink: 0;
|
|
|
- border-radius: 6px;
|
|
|
- box-shadow: 0 8px 24px rgba(31, 41, 51, 0.14);
|
|
|
- }
|
|
|
-}
|
|
|
-.result-preview {
|
|
|
- min-height: 0;
|
|
|
- flex: 1;
|
|
|
- margin-top: 14px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.draft-status {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- padding: 12px 14px;
|
|
|
- border: 1px solid #dce8f7;
|
|
|
- border-radius: 10px;
|
|
|
- background: #f6f9fe;
|
|
|
- color: #35516f;
|
|
|
- &.ready {
|
|
|
- border-color: #cce8df;
|
|
|
- background: #f0faf7;
|
|
|
- color: #16776f;
|
|
|
- }
|
|
|
- &.failed {
|
|
|
- border-color: #f3d7b2;
|
|
|
- background: #fff8ee;
|
|
|
- color: #9a5b08;
|
|
|
- }
|
|
|
- > div {
|
|
|
- min-width: 0;
|
|
|
- flex: 1;
|
|
|
- }
|
|
|
- strong,
|
|
|
- span {
|
|
|
- display: block;
|
|
|
- }
|
|
|
- strong {
|
|
|
- color: #1f2933;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- > div > span {
|
|
|
- margin-top: 3px;
|
|
|
- font-size: 12px;
|
|
|
- }
|
|
|
-}
|
|
|
-.draft-status-icon {
|
|
|
- width: 28px;
|
|
|
- height: 28px;
|
|
|
- flex-shrink: 0;
|
|
|
- display: inline-flex !important;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- border: 2px solid currentColor;
|
|
|
- border-radius: 50%;
|
|
|
- font-size: 15px !important;
|
|
|
- font-weight: 800;
|
|
|
- .optimizing & {
|
|
|
- border-color: #c6d7ee;
|
|
|
- border-top-color: $themeColor;
|
|
|
- animation: loading-rotate 0.8s linear infinite;
|
|
|
- }
|
|
|
-}
|
|
|
-.draft-workspace {
|
|
|
- min-height: 0;
|
|
|
- flex: 1;
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 170px minmax(0, 1fr);
|
|
|
- gap: 16px;
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.draft-page-list {
|
|
|
- min-height: 0;
|
|
|
- overflow-y: auto;
|
|
|
- padding: 3px 7px 3px 3px;
|
|
|
-}
|
|
|
-.draft-page-item {
|
|
|
- width: 100%;
|
|
|
- padding: 6px;
|
|
|
- border: 1px solid transparent;
|
|
|
- border-radius: 8px;
|
|
|
- background: transparent;
|
|
|
- color: #52616b;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- transition:
|
|
|
- border-color 0.16s ease,
|
|
|
- background 0.16s ease;
|
|
|
- + .draft-page-item {
|
|
|
- margin-top: 7px;
|
|
|
- }
|
|
|
- &:hover {
|
|
|
- background: #f5f8fc;
|
|
|
- }
|
|
|
- &.active {
|
|
|
- border-color: $themeColor;
|
|
|
- background: rgba($themeColor, 0.06);
|
|
|
- box-shadow: 0 0 0 2px rgba($themeColor, 0.08);
|
|
|
- }
|
|
|
- :deep(.thumbnail-slide) {
|
|
|
- border-radius: 5px;
|
|
|
- box-shadow: 0 3px 10px rgba(31, 41, 51, 0.1);
|
|
|
- }
|
|
|
- > span {
|
|
|
- display: block;
|
|
|
- margin-top: 6px;
|
|
|
- overflow: hidden;
|
|
|
- font-size: 11px;
|
|
|
- line-height: 1.4;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-}
|
|
|
-.draft-preview-pane {
|
|
|
- min-width: 0;
|
|
|
- min-height: 0;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-}
|
|
|
-.draft-preview-toolbar {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 14px;
|
|
|
- margin-bottom: 8px;
|
|
|
- span,
|
|
|
- strong {
|
|
|
- display: block;
|
|
|
- }
|
|
|
- span {
|
|
|
- color: #7b8794;
|
|
|
- font-size: 11px;
|
|
|
- }
|
|
|
- strong {
|
|
|
- margin-top: 2px;
|
|
|
- color: #1f2933;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- button {
|
|
|
- padding: 7px 11px;
|
|
|
- border: 1px solid #d9e2ec;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- color: #52616b;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
-}
|
|
|
-.draft-canvas-shell {
|
|
|
- min-width: 0;
|
|
|
- min-height: 0;
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- overflow: auto;
|
|
|
- padding: 18px;
|
|
|
- border: 1px solid #e2e8f0;
|
|
|
- border-radius: 10px;
|
|
|
- background: #eef2f7;
|
|
|
- :deep(.thumbnail-slide) {
|
|
|
- flex-shrink: 0;
|
|
|
- border-radius: 7px;
|
|
|
- box-shadow: 0 14px 34px rgba(31, 41, 51, 0.16);
|
|
|
- }
|
|
|
-}
|
|
|
-.page-actions {
|
|
|
- display: grid;
|
|
|
- gap: 7px;
|
|
|
- padding: 8px;
|
|
|
- select,
|
|
|
- input {
|
|
|
- min-width: 0;
|
|
|
- height: 30px;
|
|
|
- padding: 0 7px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 5px;
|
|
|
- font-size: 12px;
|
|
|
- }
|
|
|
- > div {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 5px;
|
|
|
- }
|
|
|
- button {
|
|
|
- padding: 4px 7px;
|
|
|
- border: 1px solid #dfe5ec;
|
|
|
- border-radius: 5px;
|
|
|
- background: #fff;
|
|
|
- color: #52616b;
|
|
|
- font-size: 11px;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
- button:disabled {
|
|
|
- cursor: not-allowed;
|
|
|
- opacity: 0.5;
|
|
|
- }
|
|
|
-}
|
|
|
-.page-nav {
|
|
|
- overflow: auto;
|
|
|
- border: 1px solid #edf1f5;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fafcff;
|
|
|
- padding: 8px;
|
|
|
-}
|
|
|
-.nav-item {
|
|
|
- width: 100%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- min-height: 42px;
|
|
|
- padding: 8px 10px;
|
|
|
- border: 0;
|
|
|
- border-radius: 6px;
|
|
|
- background: transparent;
|
|
|
- color: #52616b;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- &.active {
|
|
|
- background: #eaf4ff;
|
|
|
- color: #1f2933;
|
|
|
- .nav-no {
|
|
|
- color: #fff;
|
|
|
- background: $themeColor;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-.nav-no {
|
|
|
- width: 34px;
|
|
|
- height: 28px;
|
|
|
- line-height: 28px;
|
|
|
- text-align: center;
|
|
|
- border-radius: 5px;
|
|
|
- color: #7b8794;
|
|
|
- background: #edf1f5;
|
|
|
- flex-shrink: 0;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.nav-title {
|
|
|
- min-width: 0;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.page-editor {
|
|
|
- min-width: 0;
|
|
|
- overflow: auto;
|
|
|
- border: 1px solid #edf1f5;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fff;
|
|
|
- padding: 16px;
|
|
|
-}
|
|
|
-.editor-head {
|
|
|
- display: block;
|
|
|
-}
|
|
|
-.title-editor {
|
|
|
- max-width: 460px;
|
|
|
-}
|
|
|
-.outline-field-label {
|
|
|
- display: block;
|
|
|
- margin-bottom: 8px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.purpose-card {
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.field-grid {
|
|
|
- margin-top: 12px;
|
|
|
- display: grid;
|
|
|
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-.field-card {
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #edf1f5;
|
|
|
- border-radius: 8px;
|
|
|
- background: #fbfcfe;
|
|
|
- &.visual {
|
|
|
- background: #fffaf3;
|
|
|
- }
|
|
|
-}
|
|
|
-.field-title {
|
|
|
- margin-bottom: 8px;
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.page-title,
|
|
|
-.purpose-input,
|
|
|
-.headline-input,
|
|
|
-.page-bullets,
|
|
|
-.page-design,
|
|
|
-.keyword-input,
|
|
|
-.remark-input {
|
|
|
- width: 100%;
|
|
|
- border: 1px solid #d9d9d9;
|
|
|
- border-radius: 6px;
|
|
|
- box-sizing: border-box;
|
|
|
- font-family: inherit;
|
|
|
-}
|
|
|
-.page-title {
|
|
|
- height: 36px;
|
|
|
- padding: 0 10px;
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.purpose-input {
|
|
|
- height: 36px;
|
|
|
- padding: 0 10px;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.headline-input {
|
|
|
- height: 36px;
|
|
|
- padding: 0 10px;
|
|
|
- margin-bottom: 10px;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.page-bullets,
|
|
|
-.page-design {
|
|
|
- height: 150px;
|
|
|
- padding: 10px;
|
|
|
- resize: vertical;
|
|
|
- line-height: 1.5;
|
|
|
-}
|
|
|
-.full-field {
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.page-design {
|
|
|
- height: 90px;
|
|
|
-}
|
|
|
-.resource-card {
|
|
|
- margin-top: 12px;
|
|
|
- padding: 10px 12px;
|
|
|
- border: 1px solid #dcefe9;
|
|
|
- border-radius: 6px;
|
|
|
- background: #f6fbf9;
|
|
|
-}
|
|
|
-.resource-head {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-.resource-hint {
|
|
|
- color: #7b8794;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.resource-list {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-.resource-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- min-width: 0;
|
|
|
- height: 30px;
|
|
|
- padding: 0 10px;
|
|
|
- border: 1px solid #cfe6de;
|
|
|
- border-radius: 4px;
|
|
|
- background: #fff;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.resource-type {
|
|
|
- margin-right: 8px;
|
|
|
- color: #168f86;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.resource-name {
|
|
|
- max-width: 240px;
|
|
|
- overflow: hidden;
|
|
|
- color: #374151;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
-}
|
|
|
-.keyword-card {
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.keyword-head {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
-.keyword-label {
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.keyword-hint {
|
|
|
- color: #9aa5b1;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.keyword-input {
|
|
|
- height: 34px;
|
|
|
- padding: 0 10px;
|
|
|
-}
|
|
|
-.remark-card {
|
|
|
- margin-top: 12px;
|
|
|
- padding: 10px 12px;
|
|
|
- border-radius: 6px;
|
|
|
- background: #f7f9fc;
|
|
|
-}
|
|
|
-.remark-head {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
-.remark-title {
|
|
|
- color: #52616b;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.remark-hint {
|
|
|
- color: #9aa5b1;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.remark-input {
|
|
|
- height: 72px;
|
|
|
- padding: 10px;
|
|
|
- resize: vertical;
|
|
|
- line-height: 1.5;
|
|
|
- background: #fff;
|
|
|
-}
|
|
|
-.task {
|
|
|
- min-height: 20px;
|
|
|
-}
|
|
|
-.footer {
|
|
|
- margin-top: 14px;
|
|
|
- padding-top: 12px;
|
|
|
- border-top: 1px solid #edf1f5;
|
|
|
- background: #fff;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 10px;
|
|
|
- :deep(.button.primary) {
|
|
|
- min-width: 154px;
|
|
|
- height: 40px;
|
|
|
- line-height: 40px;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 700;
|
|
|
- }
|
|
|
-}
|
|
|
-.ai-dialog.review-mode .footer {
|
|
|
- margin-top: 10px;
|
|
|
- padding-top: 10px;
|
|
|
- :deep(.button.primary) {
|
|
|
- height: 36px;
|
|
|
- line-height: 36px;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@media (max-width: 900px) {
|
|
|
- .ai-dialog.review-mode {
|
|
|
- height: calc(100vh - 64px);
|
|
|
- }
|
|
|
- .setup-primary-grid,
|
|
|
- .sample-detail-layout {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
- .sample-canvas {
|
|
|
- justify-self: center;
|
|
|
- max-width: 100%;
|
|
|
- }
|
|
|
- .sample-preview-column {
|
|
|
- order: -1;
|
|
|
- }
|
|
|
- .compact-template-panel .sample-template-picker {
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
- }
|
|
|
- .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;
|
|
|
- overflow-x: auto;
|
|
|
- overflow-y: hidden;
|
|
|
- padding: 3px 3px 8px;
|
|
|
- }
|
|
|
- .draft-page-item {
|
|
|
- width: 160px;
|
|
|
- flex-shrink: 0;
|
|
|
- + .draft-page-item {
|
|
|
- margin-top: 0;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@media (max-width: 640px) {
|
|
|
- .workflow-step {
|
|
|
- gap: 4px;
|
|
|
- font-size: 12px;
|
|
|
- }
|
|
|
- .step-no {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
- }
|
|
|
- .sample-carousel-shell {
|
|
|
- grid-template-columns: 30px minmax(0, 1fr) 30px;
|
|
|
- gap: 5px;
|
|
|
- }
|
|
|
- .sample-carousel-arrow {
|
|
|
- width: 30px;
|
|
|
- height: 30px;
|
|
|
- }
|
|
|
- .sample-feedback-action {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
- .sample-detail-heading {
|
|
|
- align-items: flex-start;
|
|
|
- flex-direction: column;
|
|
|
- gap: 3px;
|
|
|
- small {
|
|
|
- max-width: none;
|
|
|
- text-align: left;
|
|
|
- white-space: normal;
|
|
|
- }
|
|
|
- }
|
|
|
- .compact-template-panel .sample-template-picker {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
- .draft-status,
|
|
|
- .draft-preview-toolbar,
|
|
|
- .sample-stage-head {
|
|
|
- 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;
|
|
|
- }
|
|
|
-}
|
|
|
+<style scoped lang="scss">
|
|
|
+.ai-dialog { display: flex; flex-direction: column; max-height: calc(100vh - 88px); color: #283544; font-size: 14px; }
|
|
|
+header { padding: 2px 0 18px; } h2 { font-size: 25px; margin: 0 0 6px; } h3 { font-size: 18px; margin: 0 0 8px; } p { margin: 6px 0; color: #77869a; line-height: 1.6; }
|
|
|
+.steps { display: flex; padding: 12px; margin: 0 0 20px; list-style: none; background: #f4f7fa; border-radius: 8px; gap: 6px; }
|
|
|
+.steps li { flex: 1; display: flex; justify-content: center; align-items: center; gap: 8px; color: #8491a2; padding: 8px 0; white-space: nowrap; } .steps span { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: #e6ebf1; } .steps .active { color: #1989fa; background: white; border-radius: 6px; font-weight: 600; box-shadow: 0 2px 10px #23395708; } .active span { background: #1989fa; color: white; } .steps .done { color: #169e91; } .done span { background: #ddf3ee; }
|
|
|
+main { overflow: auto; min-height: 0; flex: 1; padding: 0 3px 3px; } .setup { max-width: 720px; margin: 8px auto 24px; } label { display: block; font-weight: 500; margin: 0 0 16px; line-height: 1.5; } input:not([type='checkbox']), select, textarea { display: block; width: 100%; box-sizing: border-box; border: 1px solid #d7dfe8; border-radius: 5px; padding: 9px 11px; margin-top: 7px; background: white; color: #283544; font: inherit; font-weight: 400; } textarea { resize: vertical; min-height: 76px; } input:focus, textarea:focus, select:focus { outline: 2px solid #1989fa35; border-color: #1989fa; } .fields { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .hint { color: #8a96a6; font-size: 12px; font-weight: 400; margin-left: 8px; } details { margin-top: 12px; } summary { cursor: pointer; color: #60748b; margin-bottom: 14px; } .check { display: flex; align-items: center; gap: 6px; font-weight: 400; } .check input { margin: 0; } .notice { color: #1989fa; } .error { color: #b96218; } .validation { padding: 12px 28px; background: #fff7ed; border-radius: 6px; line-height: 1.7; }
|
|
|
+.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 20px; } .time { color: #46718f; background: #f0f6fc; padding: 9px 12px; border-radius: 6px; white-space: nowrap; } .outline-grid { display: grid; grid-template-columns: 225px minmax(0, 1fr); gap: 24px; } aside { padding: 16px; background: #f7f9fc; border-radius: 8px; align-self: start; } aside a { display: block; color: #1989fa; margin: 10px 0; overflow-wrap: anywhere; } .page-card { padding: 16px 18px 2px; margin-bottom: 14px; border: 1px solid #dfe6ee; border-radius: 8px; background: white; } .page-heading { display: flex; align-items: center; justify-content: space-between; color: #708299; margin-bottom: 12px; height: 28px; } .page-actions { display: flex; gap: 6px; } .page-actions button { border: 0; background: #f4f7fa; color: #597088; padding: 4px 8px; border-radius: 4px; cursor: pointer; min-width: 28px; } button:disabled { opacity: .4; cursor: default; } button:focus-visible { outline: 2px solid #1989fa; outline-offset: 2px; } .drag-handle { cursor: grab !important; } .page-fields { display: grid; grid-template-columns: 1fr 110px 65px; gap: 12px; align-items: end; } .page-fields .check { padding-bottom: 9px; } .add-page { width: 100%; padding: 13px; border: 1px dashed #9bbbe0; border-radius: 6px; background: #f8fbff; color: #1989fa; cursor: pointer; }
|
|
|
+.generation { min-height: 360px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 30px; background: #f8fafc; border: 1px solid #e2e9f1; border-radius: 8px; } .generation p { max-width: 650px; } progress { width: 320px; max-width: 100%; height: 8px; margin: 20px 0; accent-color: #1989fa; } .error-icon { color: #d58a2d; border: 1px solid; border-radius: 50%; width: 36px; height: 36px; line-height: 36px; margin-bottom: 18px; font-size: 24px; } .spinner { width: 30px; height: 30px; border: 3px solid #dceafa; border-top-color: #1989fa; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 22px; } @keyframes spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
|
|
|
+footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-top: 1px solid #e9eef4; padding-top: 16px; margin-top: 18px; } footer > div { display: flex; gap: 12px; }
|
|
|
+@media (max-width: 740px) { .steps { padding: 6px; } .steps li { flex-direction: column; font-size: 12px; gap: 5px; } .outline-grid { grid-template-columns: 1fr; } .section-heading { align-items: start; flex-direction: column; } .page-fields { grid-template-columns: 1fr 90px 60px; } .fields { gap: 12px; } footer { flex-wrap: wrap; } .generation { padding: 15px; min-height: 260px; } }
|
|
|
</style>
|