liuhairui пре 2 дана
родитељ
комит
bceed06b4c

+ 25 - 1
src/api/mes/job.js

@@ -1184,7 +1184,14 @@ export const txttoimg_update = (data) => {
     data
     data
   })
   })
 }
 }
-
+//多图生成
+export const generate = (data) => {
+  return service({
+    url: '/mes_server/image_generation/generate',
+    method: 'post',
+    data
+  })
+}
 
 
 export const getPreviewFolders = (params) => {
 export const getPreviewFolders = (params) => {
   return service({
   return service({
@@ -1201,6 +1208,23 @@ export const Getvideolist = (params) => {
     params
     params
   })
   })
 }
 }
+export const Create_ImgToVideo = (data, config = {}) => {
+  return service({
+    url: '/mes_server/work_order/Create_ImgToVideo',
+    method: 'post',
+    data,
+    ...config
+  })
+}
+export const Get_ImgToVideo = (params, config = {}) => {
+  return service({
+    url: '/mes_server/work_order/Get_ImgToVideo',
+    method: 'get',
+    params,
+    ...config
+  })
+}
+
 export const video = (params) => {
 export const video = (params) => {
   return service({
   return service({
     url: '/mes_server/work_order/video',
     url: '/mes_server/work_order/video',

+ 53 - 10
src/view/ModelConfiguration/aitoken.vue

@@ -6,7 +6,7 @@
         <el-button icon="Refresh" @click="getTableData">刷新</el-button>
         <el-button icon="Refresh" @click="getTableData">刷新</el-button>
         <el-input
         <el-input
           v-model="searchKeyword"
           v-model="searchKeyword"
-          placeholder="搜索任意字段"
+          placeholder="搜索"
           style="width: 320px; margin-left: auto;"
           style="width: 320px; margin-left: auto;"
         >
         >
           <template #prefix>
           <template #prefix>
@@ -23,7 +23,7 @@
         </el-table-column>
         </el-table-column>
         <el-table-column align="left" label="API地址名称" prop="supplier" min-width="80" show-overflow-tooltip />
         <el-table-column align="left" label="API地址名称" prop="supplier" min-width="80" show-overflow-tooltip />
         <el-table-column align="left" label="密钥" prop="api_key" min-width="55" show-overflow-tooltip>
         <el-table-column align="left" label="密钥" prop="api_key" min-width="55" show-overflow-tooltip>
-          <template #default="scope">{{ scope.row.api_key ? '***' + String(scope.row.api_key).slice(-4) : '-' }}</template>
+          <template #default="scope">{{ scope.row.api_key ? '***' + String(scope.row.api_key).slice(-4) : '******' }}</template>
         </el-table-column>
         </el-table-column>
         <el-table-column align="left" label="接口地址" prop="api_url" min-width="200" show-overflow-tooltip />
         <el-table-column align="left" label="接口地址" prop="api_url" min-width="200" show-overflow-tooltip />
         <el-table-column align="left" label="分组" prop="model_group" width="100" show-overflow-tooltip />
         <el-table-column align="left" label="分组" prop="model_group" width="100" show-overflow-tooltip />
@@ -97,6 +97,7 @@
             placeholder="选择或输入"
             placeholder="选择或输入"
             style="width: 100%"
             style="width: 100%"
           >
           >
+            <el-option label="无" value="" />
             <el-option v-for="item in modelGroupOptions" :key="item" :label="item" :value="item" />
             <el-option v-for="item in modelGroupOptions" :key="item" :label="item" :value="item" />
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
@@ -118,11 +119,21 @@
           />
           />
         </el-form-item>
         </el-form-item>
         <el-form-item label="模型类型" prop="model_type">
         <el-form-item label="模型类型" prop="model_type">
-          <el-input
-            v-model="form.model_type"
-            placeholder="必填,多能力逗号分隔如:文生图,图生图"
-            @input="(v) => { if (v && v.includes(',')) form.model_type = v.replace(/,/g, ',') }"
-          />
+          <el-select
+            v-model="modelTypeSelected"
+            multiple
+            collapse-tags
+            collapse-tags-tooltip
+            placeholder="请选择模型类型"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in MODEL_TYPE_OPTIONS"
+              :key="item"
+              :label="item"
+              :value="item"
+            />
+          </el-select>
         </el-form-item>
         </el-form-item>
         <el-form-item label="优先级" prop="sort">
         <el-form-item label="优先级" prop="sort">
           <el-tooltip content="同一模型内的优先级,数值越小越优先调用" placement="top">
           <el-tooltip content="同一模型内的优先级,数值越小越优先调用" placement="top">
@@ -156,7 +167,28 @@ defineOptions({
   name: 'ApiToken'
   name: 'ApiToken'
 })
 })
 
 
+/** 模型能力类型(多选后逗号拼接提交) */
+const MODEL_TYPE_OPTIONS = [
+  '图生文',
+  '文生文',
+  '文生图',
+  '图生图',
+  '文本生视频',
+  '文本图片生视频',
+  '文本视频生视频',
+]
+
+const parseModelTypeList = (value) => {
+  if (value == null || value === '') return []
+  return String(value)
+    .split(/[,,]/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .filter((s) => MODEL_TYPE_OPTIONS.includes(s))
+}
+
 const formRef = ref(null)
 const formRef = ref(null)
+const modelTypeSelected = ref([])
 const tableData = ref([])
 const tableData = ref([])
 const dialogVisible = ref(false)
 const dialogVisible = ref(false)
 const type = ref('create')
 const type = ref('create')
@@ -179,7 +211,13 @@ const formRules = {
   api_key: [{ required: true, message: 'KEY不能为空', trigger: 'blur' }],
   api_key: [{ required: true, message: 'KEY不能为空', trigger: 'blur' }],
   api_url: [{ required: true, message: '接口地址不能为空', trigger: 'blur' }],
   api_url: [{ required: true, message: '接口地址不能为空', trigger: 'blur' }],
   model_name: [{ required: true, message: '模型不能为空', trigger: 'blur' }],
   model_name: [{ required: true, message: '模型不能为空', trigger: 'blur' }],
-  model_type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }]
+  model_type: [{
+    validator: (_rule, _value, callback) => {
+      if (!modelTypeSelected.value.length) callback(new Error('请选择模型类型'))
+      else callback()
+    },
+    trigger: 'change',
+  }],
 }
 }
 
 
 const loading = ref(false)
 const loading = ref(false)
@@ -262,6 +300,7 @@ const resetForm = () => {
   form.model_name = ''
   form.model_name = ''
   form.model_alias = ''
   form.model_alias = ''
   form.model_type = ''
   form.model_type = ''
+  modelTypeSelected.value = []
   form.sort = 1
   form.sort = 1
 }
 }
 
 
@@ -282,6 +321,7 @@ const updateRow = (row) => {
   form.model_name = row.model_name ?? ''
   form.model_name = row.model_name ?? ''
   form.model_alias = row.model_alias ?? row.model_name ?? ''
   form.model_alias = row.model_alias ?? row.model_name ?? ''
   form.model_type = row.model_type ?? ''
   form.model_type = row.model_type ?? ''
+  modelTypeSelected.value = parseModelTypeList(row.model_type)
   form.sort = Math.max(1, Number(row.sort) || 1)
   form.sort = Math.max(1, Number(row.sort) || 1)
   dialogVisible.value = true
   dialogVisible.value = true
 }
 }
@@ -299,6 +339,9 @@ const enterDialog = async () => {
     return
     return
   }
   }
 
 
+  const model_type = modelTypeSelected.value.join(',')
+  form.model_type = model_type
+
   submitLoading.value = true
   submitLoading.value = true
   try {
   try {
     let res
     let res
@@ -311,7 +354,7 @@ const enterDialog = async () => {
         model_group: form.model_group,
         model_group: form.model_group,
         model_name: form.model_name,
         model_name: form.model_name,
         model_alias: form.model_alias || form.model_name,
         model_alias: form.model_alias || form.model_name,
-        model_type: form.model_type,
+        model_type,
         sort: form.sort
         sort: form.sort
       })
       })
     } else {
     } else {
@@ -324,7 +367,7 @@ const enterDialog = async () => {
         model_group: form.model_group,
         model_group: form.model_group,
         model_name: form.model_name,
         model_name: form.model_name,
         model_alias: form.model_alias || form.model_name,
         model_alias: form.model_alias || form.model_name,
-        model_type: form.model_type,
+        model_type,
         sort: form.sort
         sort: form.sort
       })
       })
     }
     }

+ 11 - 13
src/view/Product/ProductImageGeneration.vue

@@ -51,7 +51,7 @@
                     type="textarea"
                     type="textarea"
                     :rows="5"
                     :rows="5"
                     placeholder="请输入对效果图的描述"
                     placeholder="请输入对效果图的描述"
-                    maxlength="500"
+                    maxlength="1000"
                     show-word-limit
                     show-word-limit
                     resize="none"
                     resize="none"
                     class="prompt-textarea"
                     class="prompt-textarea"
@@ -187,7 +187,7 @@
             <div v-else v-show="!promptHistoryVisible" class="result-empty">
             <div v-else v-show="!promptHistoryVisible" class="result-empty">
               <el-icon :size="60" class="empty-icon"><Picture /></el-icon>
               <el-icon :size="60" class="empty-icon"><Picture /></el-icon>
               <p>效果图将在此处显示</p>
               <p>效果图将在此处显示</p>
-              <span>上传产品图、填写提示词后点击下方按钮生成</span>
+              <span>填写提示词或上传产品图后点击下方按钮生成</span>
               <el-button type="primary" :loading="isGenerating" size="large" class="generate-btn-center" @click="generateImage">
               <el-button type="primary" :loading="isGenerating" size="large" class="generate-btn-center" @click="generateImage">
                 {{ isGenerating ? '生成中...' : '立即生成' }}
                 {{ isGenerating ? '生成中...' : '立即生成' }}
               </el-button>
               </el-button>
@@ -280,7 +280,7 @@ defineOptions({ name: 'ProductImageGeneration' })
 const userStore = useUserStore()
 const userStore = useUserStore()
 const uploadPath = ref(import.meta.env.VITE_BASE_API)
 const uploadPath = ref(import.meta.env.VITE_BASE_API)
 const uploadConfig = [
 const uploadConfig = [
-  { key: 'product_img', label: '产品图', placeholder: '上传产品图', required: true },
+  { key: 'product_img', label: '产品图(可选)', placeholder: '上传产品图', required: false },
   { key: 'template_img', label: '参考图(可选)', placeholder: '上传参考图', required: false },
   { key: 'template_img', label: '参考图(可选)', placeholder: '上传参考图', required: false },
 ]
 ]
 
 
@@ -589,17 +589,19 @@ const fileToBase64Compressed = (file, maxSize = 1920, quality = 0.82) =>
   })
   })
 
 
 const generateImage = async () => {
 const generateImage = async () => {
-  if (!formData.product_img) { ElMessage.warning('请先上传产品图,或联系管理人员开通上传权限'); return }
-  if (!productFileRef.value) { ElMessage.warning('请重新上传产品图,或联系管理人员开通上传权限'); return }
-  if (!formData.prompt) { ElMessage.warning('请填写提示词'); return }
+  if (!formData.prompt?.trim() && !productFileRef.value) {
+    ElMessage.warning('请填写提示词或上传产品图')
+    return
+  }
   const sizeStr = getSizeStr()
   const sizeStr = getSizeStr()
   isGenerating.value = true
   isGenerating.value = true
   generatedImageUrl.value = ''
   generatedImageUrl.value = ''
   try {
   try {
-    const product_img = await fileToBase64Compressed(productFileRef.value)
+    const hasProductImg = !!productFileRef.value
+    const product_img = hasProductImg ? await fileToBase64Compressed(productFileRef.value) : ''
     const template_img = templateFileRef.value ? await fileToBase64Compressed(templateFileRef.value) : ''
     const template_img = templateFileRef.value ? await fileToBase64Compressed(templateFileRef.value) : ''
     const params = {
     const params = {
-      status_val: '图生图',
+      status_val: hasProductImg ? '图生图' : '文生图',
       status_type: 'ProductImageGeneration',
       status_type: 'ProductImageGeneration',
       prompt: formData.prompt,
       prompt: formData.prompt,
       model: formData.model,
       model: formData.model,
@@ -611,11 +613,7 @@ const generateImage = async () => {
       height: formData.customHeight,
       height: formData.customHeight,
       sys_id: userStore.userInfo.nickName
       sys_id: userStore.userInfo.nickName
     }
     }
-    console.log('立即生成 传参:', {
-      ...params,
-      product_img: product_img ? `base64(${product_img.length}字符)` : '',
-      template_img: template_img ? `base64(${template_img.length}字符)` : '',
-    })
+    // console.log(params);return;
     const res = await CallAIModelApi(params)
     const res = await CallAIModelApi(params)
     if (res?.code === 0 && res?.data?.task_id) {
     if (res?.code === 0 && res?.data?.task_id) {
       taskIdRef.value = res.data.task_id
       taskIdRef.value = res.data.task_id

+ 1344 - 444
src/view/performance/QualityAssessment/texttoimage.vue

@@ -1,569 +1,1469 @@
 <template>
 <template>
-  <div class="image-generation-container">
-    <el-card class="mb-4">
-      <template #header>
-          <span>文生图生成</span>
-      </template>
-      <br>
-      <el-form ref="formRef" :model="form" label-width="100px" class="mb-4"
-          style="margin: 0px;padding: 0px !important;">
-        <el-form-item label="提示词" prop="chinese_description">
-          <el-input 
-            v-model="form.chinese_description" 
-            type="textarea" 
-            :rows="3" 
-            placeholder="请输入文生图描述"
-          />
-        </el-form-item>
-        
-        <el-form-item label="模型" prop="model">
-          <div style="display: flex; align-items: center; gap: 20px;">
-            <el-radio-group v-model="txtimgselectedModel" @change="handleModelChange">
-                <el-radio 
-                v-for="item in txttoimg_modelList" 
-                :key="item.id"
-                :label="item.txttoimg">
-                {{ item.txttoimg }} 
-                <span v-if="item.id === usedIds.wenshengtu" style="color: #67C23A; margin-left: 5px;">(默认使用)</span>
-                </el-radio>
-            </el-radio-group>
-            <el-button type="success"  @click="setActive('wenshengtu')"  :disabled="!selectedIds.wenshengtu || selectedIds.wenshengtu === usedIds.wenshengtu">
-            设为默认使用模型
-            </el-button>
+  <div class="texttoimage-page">
+    <div class="two-column-layout">
+      <div class="left-column">
+        <el-card class="config-card" shadow="hover">
+          <div class="config-body">
+            <el-form ref="formRef" :model="form" label-position="top" size="small" class="config-form" hide-required-asterisk>
+              <el-form-item class="upload-row">
+                <div class="upload-row-inner">
+                  <div v-for="item in uploadConfig" :key="item.key" class="upload-cell">
+                    <span class="upload-label">{{ item.label }}<span class="optional">(可选)</span></span>
+                    <div class="upload-cell-inner">
+                      <el-upload
+                        class="image-uploader square"
+                        :show-file-list="false"
+                        :auto-upload="false"
+                        :on-change="(uploadFile) => onUploadFileChange(item.key, uploadFile)"
+                        accept="image/jpeg,image/png,image/webp,image/jpg"
+                      >
+                        <div v-if="hasUploadImage(item.key)" class="uploaded-preview">
+                          <el-image :src="getUploadDisplayUrl(item.key)" fit="contain" class="preview-image">
+                            <template #error>
+                              <div class="image-error"><el-icon><Picture /></el-icon></div>
+                            </template>
+                          </el-image>
+                          <div class="upload-mask">
+                            <svg class="swap-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                              <path d="M4 8h12l-3-3 3-3" />
+                              <path d="M20 16H8l3 3-3 3" />
+                            </svg>
+                            <span>更换</span>
+                          </div>
+                        </div>
+                        <div v-else class="upload-placeholder">
+                          <el-icon :size="20"><Plus /></el-icon>
+                          <span>{{ item.placeholder }}</span>
+                        </div>
+                      </el-upload>
+                      <button
+                        v-if="hasUploadImage(item.key)"
+                        type="button"
+                        class="upload-remove-btn"
+                        title="移除"
+                        @click.stop="clearUploadImage(item.key)"
+                      >
+                        <el-icon><Close /></el-icon>
+                      </button>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="prompt-form-item">
+                <template #label><span>提示词</span></template>
+                <div class="prompt-block">
+                  <div v-loading="isOptimizing" element-loading-text="优化中..." class="prompt-wrapper">
+                    <el-input
+                      v-model="form.chinese_description"
+                      type="textarea"
+                      :rows="10"
+                      placeholder="请输入文生图描述"
+                      maxlength="1000"
+                      show-word-limit
+                      resize="none"
+                      class="prompt-textarea"
+                    />
+                  </div>
+                  <div class="prompt-actions-row">
+                    <el-button type="success" link size="small" :loading="isOptimizing" @click="optimizePrompt">
+                      <el-icon><MagicStick /></el-icon>{{ isOptimizing ? '优化中...' : '优化提示词' }}
+                    </el-button>
+                  </div>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item model-item">
+                  <span class="config-label">模型</span>
+                  <el-select
+                    v-model="form.model"
+                    placeholder="请选择模型"
+                    size="small"
+                    class="model-select"
+                    filterable
+                  >
+                    <el-option
+                      v-for="(item, idx) in modelList"
+                      :key="item?.id ?? item?.ID ?? idx"
+                      :label="item.model_alias"
+                      :value="item.model_name || item.model_alias || ''"
+                    />
+                  </el-select>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item max-images-item">
+                  <span class="config-label">生成数量</span>
+                  <el-select
+                    v-model="form.max_images"
+                    size="small"
+                    class="max-images-select"
+                  >
+                    <el-option
+                      v-for="n in maxImagesOptions"
+                      :key="n"
+                      :label="String(n)"
+                      :value="n"
+                    />
+                  </el-select>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item size-item">
+                  <span class="config-label">出图尺寸</span>
+                  <div class="size-groups">
+                    <div class="size-group size-group-no-label">
+                      <div class="size-group-options">
+                        <span
+                          v-for="item in sizePresets"
+                          :key="item.value"
+                          class="size-chip"
+                          :class="{ active: form.size === item.value }"
+                          @click="selectSize(item)"
+                        >{{ item.label }}</span>
+                        <span class="size-chip" :class="{ active: form.size === 'custom' }" @click="form.size = 'custom'">自定义</span>
+                        <span class="size-input-label">宽</span>
+                        <el-input
+                          v-model.number="form.width"
+                          size="small"
+                          placeholder="宽度"
+                          type="number"
+                          min="1"
+                          class="custom-size-input"
+                          @focus="form.size = 'custom'"
+                        />
+                        <span class="size-sep">×</span>
+                        <span class="size-input-label">高</span>
+                        <el-input
+                          v-model.number="form.height"
+                          size="small"
+                          placeholder="高度"
+                          type="number"
+                          min="1"
+                          class="custom-size-input"
+                          @focus="form.size = 'custom'"
+                        />
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+            </el-form>
           </div>
           </div>
-        </el-form-item>
-        
-        <el-form-item label="出图尺寸">
-          <div style="display: flex; gap: 10px;">
-            <div style="width: calc(50% - 5px);">
-              <el-input 
-                v-model.number="form.width" 
-                placeholder="宽度" 
-                type="number" 
-                min="1" 
-                style="width: 100%"
-              />
+        </el-card>
+      </div>
+
+      <div class="right-column">
+        <el-card class="result-card" shadow="hover">
+          <template #header>
+            <el-button size="small" class="record-btn-header" @click="openRecordDialog">
+              图片生成记录
+            </el-button>
+          </template>
+          <div class="result-area">
+            <div v-if="generatedImageUrl" class="result-preview">
+              <el-image
+                :src="formatImageUrl(generatedImageUrl)"
+                fit="contain"
+                class="result-image"
+                :preview-src-list="[formatImageUrl(generatedImageUrl)]"
+                preview-teleported
+              >
+                <template #error>
+                  <div class="image-error-large">
+                    <el-icon :size="48"><Picture /></el-icon>
+                    <span>图片加载失败</span>
+                  </div>
+                </template>
+              </el-image>
+              <div v-if="currentImageItem" class="result-meta">
+                <p class="result-meta-prompt" :title="currentImageItem.prompt">{{ currentImageItem.prompt || '-' }}</p>
+                <span class="result-meta-info">
+                  {{ currentImageItem.model || '-' }} · {{ currentImageItem.width }}×{{ currentImageItem.height }}
+                </span>
+              </div>
+              <div class="result-actions">
+                <el-button type="primary" size="small" :loading="isGenerating" @click="texttoimg">
+                  {{ isGenerating ? '生成中...' : '再次生成' }}
+                </el-button>
+                <el-button type="primary" size="small" @click="downloadImage">
+                  <el-icon><Download /></el-icon>下载
+                </el-button>
+              </div>
             </div>
             </div>
-            <div style="width: calc(50% - 5px);">
-              <el-input 
-                v-model.number="form.height" 
-                placeholder="高度" 
-                type="number" 
-                min="1" 
-                style="width: 100%"
-              />
+            <div v-else-if="isGenerating" class="result-loading">
+              <el-icon :size="48" class="loading-icon is-loading"><Loading /></el-icon>
+              <p>正在生成图片,请稍候...</p>
+              <el-button type="default" size="small" @click="cancelGenerate">取消生成</el-button>
+            </div>
+            <div v-else class="result-empty">
+              <el-icon :size="60" class="empty-icon"><Picture /></el-icon>
+              <p>图片将在此处显示</p>
+              <span>填写提示词或上传原图/参考图后点击下方按钮生成</span>
+              <el-button type="primary" :loading="isGenerating" size="large" class="generate-btn-center" @click="texttoimg">
+                {{ isGenerating ? '生成中...' : '立即生成' }}
+              </el-button>
             </div>
             </div>
           </div>
           </div>
-        </el-form-item>
-        
-        <el-form-item>
-          <el-button 
-            type="primary" 
-            @click="texttoimg" 
-            :loading="isGenerating"
-          >
-            {{ isGenerating ? '生成中...' : '生成文生图' }}
-          </el-button>
-          <el-button @click="resetForm">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-    
-    <!-- 历史记录 -->
-    <el-card class="mt-1">
-      <el-table :data="historyList"
-                :row-style="{ height: '20px' }"
-                :cell-style="{ padding: '0px' }" :header-row-style="{ height: '20px' }"
-                :header-cell-style="{ padding: '0px' }"
-                style="width: 100%;height: 40vh">
-        <el-table-column align="center" prop="id" label="ID" width="80" />
-        <el-table-column prop="prompt" label="提示词" width="500">
+        </el-card>
+      </div>
+    </div>
+
+    <el-dialog
+      v-model="recordDialogVisible"
+      title="文生图生成记录"
+      align-center
+      width="min(95vw, 1200px)"
+      class="image-record-dialog"
+      destroy-on-close
+      @open="onRecordDialogOpen"
+    >
+      <el-table :data="historyList" stripe size="small" max-height="min(60vh, 520px)" style="width: 100%">
+        <el-table-column align="center" prop="id" label="ID" width="70" />
+        <el-table-column label="生成图" width="80" align="center">
           <template #default="scope">
           <template #default="scope">
-            <span :title="scope.row.prompt" class="text-ellipsis" style="display: inline-block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">{{ scope.row.prompt }}</span>
+            <el-image
+              v-if="getHistoryImageUrl(scope.row)"
+              :src="formatImageUrl(getHistoryImageUrl(scope.row))"
+              fit="cover"
+              class="history-ref-thumb history-thumb-preview"
+              :preview-src-list="[formatImageUrl(getHistoryImageUrl(scope.row))]"
+              preview-teleported
+              hide-on-click-modal
+              :z-index="3200"
+            >
+              <template #error>
+                <div class="history-thumb-fallback"><el-icon><Picture /></el-icon></div>
+              </template>
+            </el-image>
+            <span v-else class="history-ref-empty">-</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column prop="model" label="模型" width="100" />
-        <el-table-column label="尺寸" width="100">
+        <el-table-column prop="prompt" label="提示词" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="model" label="模型" width="140" show-overflow-tooltip />
+        <el-table-column label="尺寸" width="100" align="center">
           <template #default="scope">
           <template #default="scope">
-            {{ scope.row.width }}x{{ scope.row.height }}
+            {{ formatRowSize(scope.row) }}
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column prop="sys_rq" label="创建时间" width="180" />
-        <el-table-column label="状态" width="100">
+        <el-table-column prop="sys_rq" label="创建时间" width="168" />
+        <el-table-column label="状态" width="88" align="center">
           <template #default="scope">
           <template #default="scope">
-            <el-tag :type="scope.row.web_url ? 'success' : 'warning'">
-              {{ scope.row.web_url ? '已完成' : '生成中' }}
+            <el-tag size="small" :type="getHistoryImageUrl(scope.row) ? 'success' : 'warning'">
+              {{ getHistoryImageUrl(scope.row) ? '已完成' : '生成中' }}
             </el-tag>
             </el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="操作" width="300">
-            <template #default="scope">
-              <el-button 
-                size="small" 
-                @click="viewHistoryImage(scope.row)"
-                :disabled="!scope.row.web_url"
-              >
-                查看
-              </el-button>
-              <el-button 
-                size="small" 
-                type="primary" 
-                @click="refreshImage(scope.row)"
-                v-if="!scope.row.web_url && scope.row.image_id"
-              >
-                重新获取图片
-              </el-button>
-            </template>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="scope">
+            <el-button size="small" type="primary" link :disabled="!getHistoryImageUrl(scope.row)" @click="viewHistoryImage(scope.row)">
+              查看
+            </el-button>
+            <el-button
+              v-if="!getHistoryImageUrl(scope.row) && (scope.row.image_id || scope.row.task_id)"
+              size="small"
+              type="primary"
+              link
+              @click="refreshImage(scope.row)"
+            >
+              重新获取
+            </el-button>
+          </template>
         </el-table-column>
         </el-table-column>
       </el-table>
       </el-table>
-      <!-- 分页组件 -->
-      <div class="gva-pagination">
-          <el-pagination
-              v-model:current-page="page"
-              v-model:page-size="pageSize"
-              :page-sizes="[10, 30, 50, 100, 500, 1000]"
-              layout="total, sizes, prev, pager, next, jumper"
-              :total="total"
-              @size-change="handleSizeChange"
-              @current-change="handleCurrentChange">
-          </el-pagination>
+      <div class="gva-pagination record-dialog-pagination">
+        <el-pagination
+          v-model:current-page="page"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      v-model="recordPreviewVisible"
+      title="图片预览"
+      align-center
+      width="min(90vw, 900px)"
+      destroy-on-close
+    >
+      <div class="record-preview-body">
+        <el-image
+          v-if="recordPreviewUrl"
+          :src="recordPreviewUrl"
+          fit="contain"
+          class="record-preview-image"
+          :preview-src-list="[recordPreviewUrl]"
+          preview-teleported
+        />
+        <div v-if="recordPreviewItem" class="record-preview-meta">
+          <p class="record-preview-prompt">{{ recordPreviewItem.prompt || '-' }}</p>
+          <span class="record-preview-info">
+            {{ recordPreviewItem.model || '-' }} · {{ formatRowSize(recordPreviewItem) }} · {{ recordPreviewItem.sys_rq || '-' }}
+          </span>
         </div>
         </div>
-    </el-card>
+      </div>
+    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, shallowReactive, onMounted, onBeforeUnmount, toRaw } from 'vue'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
-import { txttoimg_moxing, txttoimg_update,imageToText } from '@/api/mes/job'
+import { Picture, Download, Loading, Plus, Close, MagicStick } from '@element-plus/icons-vue'
+import { GetAIModel, GetImageStatus, GetProductList, CallAIModelApi } from '@/api/mes/job'
 import { useUserStore } from '@/pinia/modules/user'
 import { useUserStore } from '@/pinia/modules/user'
 import { displayImageUrl } from '@/utils/displayImageUrl.js'
 import { displayImageUrl } from '@/utils/displayImageUrl.js'
 
 
-//获取登录用户信息
+defineOptions({ name: 'TextToImage' })
+
 const userStore = useUserStore()
 const userStore = useUserStore()
-const _username = ref('')
-_username.value = userStore.userInfo.userName + '/' + userStore.userInfo.nickName
-console.log('获取用户信息',_username.value)
-console.log('获取用户名称',userStore.userInfo.nickName)
+const formatImageUrl = displayImageUrl
+
+const uploadConfig = [
+  { key: 'product_img', label: '原图', placeholder: '上传原图' },
+  { key: 'template_img', label: '参考图', placeholder: '上传参考图' },
+]
+const UPLOAD_KEYS = uploadConfig.map((item) => item.key)
+const uploadPreviewUrls = reactive({ product_img: '', template_img: '' })
+const uploadFiles = shallowReactive({ product_img: null, template_img: null })
+const MAX_UPLOAD_MB = 10
+
+const maxImagesOptions = Array.from({ length: 15 }, (_, i) => i + 1)
+
+const sizePresets = [
+  { label: '1:1', value: '1024x1024', width: 1024, height: 1024 },
+  { label: '2:3', value: '768x1152', width: 768, height: 1152 },
+  { label: '3:2', value: '1152x768', width: 1152, height: 768 },
+  { label: '3:4', value: '768x1024', width: 768, height: 1024 },
+  { label: '4:3', value: '1024x768', width: 1024, height: 768 },
+  { label: '4:5', value: '1024x1280', width: 1024, height: 1280 },
+  { label: '5:4', value: '1280x1024', width: 1280, height: 1024 },
+  { label: '9:16', value: '1080x1920', width: 1080, height: 1920 },
+  { label: '16:9', value: '1920x1080', width: 1920, height: 1080 },
+  { label: '21:9', value: '2560x1080', width: 2560, height: 1080 },
+]
 
 
-// 表单数据
 const form = reactive({
 const form = reactive({
   chinese_description: '',
   chinese_description: '',
   model: '',
   model: '',
-  width: 1024, // 默认宽度
-  height: 1024 // 默认高度
+  max_images: 1,
+  width: 1024,
+  height: 1024,
+  size: '1024x1024',
 })
 })
 
 
-// 表单引用
 const formRef = ref()
 const formRef = ref()
+const isGenerating = ref(false)
+const isOptimizing = ref(false)
+const generatedImageUrl = ref('')
+const currentImageItem = ref(null)
+const recordDialogVisible = ref(false)
+const recordPreviewVisible = ref(false)
+const recordPreviewUrl = ref('')
+const recordPreviewItem = ref(null)
 
 
-// 页面状态
-const isGenerating = ref(false);
-const dialogVisible = ref(false);
-const dialogImageUrl = ref('');
-const isPreviewLoading = ref(false);
+const historyList = ref([])
+const page = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
 
 
-// 历史记录相关
-const historyList = ref([]);
-const page = ref(1);
-const pageSize = ref(10);
-const total = ref(0);
+const modelList = ref([])
 
 
-// 模型选择相关变量
-const txtimgselectedModel = ref('');
-const txttoimg_modelList = ref([]);
-const usedIds = ref({});
-const selectedIds = ref({});
+const taskIdRef = ref('')
+const pollIntervalRef = ref(null)
+const pollCountRef = ref(0)
+let generateSessionId = 0
 
 
-const formatImageUrl = displayImageUrl
+const IMAGE_GEN_TYPES = ['文生图', '图生图']
 
 
-const loadImageHistory = async () => {
-  // 获取模型列表和历史记录
-  const response = await txttoimg_moxing();
-  txttoimg_modelList.value = response.data.models.wenshengtu;
-  usedIds.value = response.data.used_ids;
-  // 设置默认选中的模型
-  if (txttoimg_modelList.value.length > 0) {
-    const defaultModel = txttoimg_modelList.value.find(item => item.id === usedIds.value.wenshengtu);
-    if (defaultModel) {
-      txtimgselectedModel.value = defaultModel.txttoimg;
-      selectedIds.value.wenshengtu = defaultModel.id;
-      form.model = defaultModel.txttoimg;
+function parseModelTypes (modelType) {
+  if (modelType == null || modelType === '') return []
+  return String(modelType).split(/[,,]/).map((t) => t.trim()).filter(Boolean)
+}
+
+function modelSupportsImageGen (item) {
+  const types = parseModelTypes(item?.model_type)
+  return IMAGE_GEN_TYPES.some((t) => types.includes(t))
+}
+
+const getUploadRawFile = (key) => {
+  const f = uploadFiles[key]
+  if (!f) return null
+  const raw = toRaw(f)
+  return raw instanceof File ? raw : (f instanceof File ? f : null)
+}
+
+const hasUploadImage = (key) => !!(uploadFiles[key] || uploadPreviewUrls[key])
+
+const getUploadDisplayUrl = (key) => uploadPreviewUrls[key] || ''
+
+const onUploadFileChange = (key, uploadFile) => {
+  const file = uploadFile?.raw
+  if (!file) return
+  const mimeOk = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg'].includes(file.type)
+  const extOk = /\.(jpe?g|png|webp)$/i.test(file.name || '')
+  if (!mimeOk && !extOk) {
+    ElMessage.error('仅支持 jpg、png、webp 格式')
+    return
+  }
+  if (file.size / 1024 / 1024 > MAX_UPLOAD_MB) {
+    ElMessage.error(`图片大小不能超过 ${MAX_UPLOAD_MB}MB`)
+    return
+  }
+  if (uploadPreviewUrls[key]) URL.revokeObjectURL(uploadPreviewUrls[key])
+  uploadFiles[key] = file
+  uploadPreviewUrls[key] = URL.createObjectURL(file)
+}
+
+const clearUploadImage = (key, showMsg = true) => {
+  if (uploadPreviewUrls[key]) {
+    URL.revokeObjectURL(uploadPreviewUrls[key])
+    uploadPreviewUrls[key] = ''
+  }
+  uploadFiles[key] = null
+  if (showMsg) {
+    const label = uploadConfig.find((item) => item.key === key)?.label || '图片'
+    ElMessage.info(`已移除${label}`)
+  }
+}
+
+const hasAnyUploadImage = () => UPLOAD_KEYS.some((key) => hasUploadImage(key))
+
+const fileToBase64Compressed = (file, maxSize = 1920, quality = 0.82) =>
+  new Promise((resolve, reject) => {
+    const url = URL.createObjectURL(file)
+    const img = new Image()
+    img.onload = () => {
+      URL.revokeObjectURL(url)
+      let w = img.naturalWidth
+      let h = img.naturalHeight
+      if (w > maxSize || h > maxSize) {
+        if (w > h) {
+          h = Math.round((h * maxSize) / w)
+          w = maxSize
+        } else {
+          w = Math.round((w * maxSize) / h)
+          h = maxSize
+        }
+      }
+      const canvas = document.createElement('canvas')
+      canvas.width = w
+      canvas.height = h
+      const ctx = canvas.getContext('2d')
+      ctx.drawImage(img, 0, 0, w, h)
+      resolve(canvas.toDataURL('image/jpeg', quality))
+    }
+    img.onerror = () => {
+      URL.revokeObjectURL(url)
+      reject(new Error('图片加载失败'))
     }
     }
+    img.src = url
+  })
+
+function dedupeModelList (list) {
+  const seenName = new Set()
+  const out = []
+  for (const item of list) {
+    const name = (item.model_name || item.model_alias || '').trim().toLowerCase()
+    if (!name || seenName.has(name)) continue
+    seenName.add(name)
+    out.push(item)
   }
   }
+  return out
 }
 }
-loadImageHistory()
 
 
-// 生成图片
-const texttoimg = async () => {
-  if (!form.chinese_description) {
-    ElMessage.warning('请输入提示词')
-    return
+const selectSize = (item) => {
+  form.size = item.value
+  form.width = item.width
+  form.height = item.height
+}
+
+const getHistoryImageUrl = (row) => {
+  if (!row) return ''
+  return row.web_url || row.generated_image || row.new_image_url || row.image_url || ''
+}
+
+const normalizeHistoryRow = (item) => {
+  const sizeStr = item.size || ''
+  const [sw, sh] = sizeStr.includes('x') ? sizeStr.split('x') : []
+  return {
+    ...item,
+    prompt: item.prompt || item.chinese_description || '',
+    model: item.model || item.selectedOption || '',
+    web_url: getHistoryImageUrl(item),
+    sys_rq: item.sys_rq || item.createTime || '',
+    width: item.width || Number(sw) || '',
+    height: item.height || Number(sh) || '',
+    image_id: item.image_id || item.task_id || '',
   }
   }
-  
-  // 加载开启
-  isGenerating.value = true
-  
+}
+
+const formatRowSize = (row) => {
+  if (!row) return '-'
+  if (row.width && row.height) return `${row.width}×${row.height}`
+  if (row.size) return row.size.replace('x', '×')
+  return '-'
+}
+
+const loadModelList = async () => {
   try {
   try {
-    const payload = {
-        status_val : '文生图',
-        type: '文生图',
-        selectedOption: txtimgselectedModel.value,
-        width: form.width,
-        height: form.height,
-        chinese_description: form.chinese_description,
-        sys_id:userStore.userInfo.nickName
+    const res = await GetAIModel({})
+    const list = res?.data?.list ?? res?.data ?? []
+    const raw = (Array.isArray(list) ? list : [])
+      .filter((item) => String(item?.status ?? '1') === '1')
+      .filter(modelSupportsImageGen)
+      .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
+    modelList.value = dedupeModelList(raw)
+    if (modelList.value.length > 0) {
+      const cur = (form.model || '').trim()
+      const exists = modelList.value.some(
+        (m) => (m.model_name || m.model_alias || '') === cur
+      )
+      if (!cur || !exists) {
+        const first = modelList.value[0]
+        form.model = first.model_name || first.model_alias || ''
+      }
+    } else {
+      form.model = ''
     }
     }
+  } catch (error) {
+    console.error('加载文生图模型失败', error)
+    modelList.value = []
+    form.model = ''
+    ElMessage.error('加载模型列表失败')
+  }
+}
 
 
-    // 打印调试信息
-    console.log('生成图片请求参数:', payload)
-    // await new Promise(resolve => setTimeout(resolve, 2000));
-	// isLoading.value = false;
-	// return;
-
-    // 调用图片生成API
-    const res = await imageToText(payload)
-    
-    if (res.code === 200) {
-      ElMessage.success('图片生成任务已提交')
-      // 生成成功后刷新历史记录
-      await loadImageHistory()
-      // 重置表单
-      resetForm()
-    } else {
-      ElMessage.error(res.msg || '图片生成失败')
+const loadImageHistory = async () => {
+  try {
+    const res = await GetProductList({
+      sys_id: userStore.userInfo.nickName,
+      page: page.value,
+      limit: pageSize.value,
+      status_val: '文生图',
+    })
+    const raw = res?.data
+    let list = []
+    if (Array.isArray(raw)) {
+      list = raw
+    } else if (raw && Array.isArray(raw.list)) {
+      list = raw.list
     }
     }
+    historyList.value = list
+      .filter((item) => (item.status_val === '文生图' || item.type === '文生图') && item.status_type !== 'ProductImageGeneration')
+      .map(normalizeHistoryRow)
+    total.value = Number(res?.total ?? res?.count ?? raw?.total ?? historyList.value.length)
   } catch (error) {
   } catch (error) {
-    console.error('图片生成过程中发生错误:', error)
-    ElMessage.error('图片生成失败,请检查网络连接后重试')
-  } finally {
-    // 关闭加载状态
-    isGenerating.value = false
+    console.error('加载文生图历史失败', error)
+    historyList.value = []
+    total.value = 0
+  }
+}
+
+const stopPolling = () => {
+  if (pollIntervalRef.value) {
+    clearInterval(pollIntervalRef.value)
+    pollIntervalRef.value = null
   }
   }
 }
 }
 
 
-// 轮询检查视频生成进度
-let checkInterval = null
-const startCheckingProgress = (videoId) => {
-  if (!videoId) return
-  
-  // 清除之前的轮询
-  if (checkInterval) {
-    clearInterval(checkInterval)
-  }
-  
-  // 最大检查次数,避免无限轮询
-  let checkCount = 0
-  const maxCheckCount = 20 // 最多检查20次(60秒)
-  
-  // 每3秒检查一次
-  checkInterval = setInterval(async () => {
-    checkCount++
-    
-    if (checkCount > maxCheckCount) {
-      ElMessage.info('视频生成可能需要更长时间,请手动刷新查看')
-      clearInterval(checkInterval)
+const pollImageStatus = async (sessionId) => {
+  if (!taskIdRef.value || sessionId !== generateSessionId) {
+    stopPolling()
+    return
+  }
+  try {
+    pollCountRef.value++
+    if (pollCountRef.value > 30) {
+      stopPolling()
+      isGenerating.value = false
+      ElMessage.warning('生成超时,请在「图片生成记录」中查看')
       return
       return
     }
     }
-    
-    try {
-      // 使用videoContent接口检查视频状态
-      const statusRes = await videoContent({ video_id: videoId, action: 'status' })
-      
-      if (statusRes.code === 0 && statusRes.data?.status === 'completed' && statusRes.data?.web_url) {
-        // 视频生成完成
-        ElMessage.success('视频生成完成')
-        
-        // 重新加载历史记录
-        await loadVideoHistory()
-        
-        // 查找对应的视频记录
-        const videoItem = historyList.value.find(item => item.video_id === videoId)
-        if (videoItem) {
-          // 自动打开视频播放对话框
-          playHistoryVideo(videoItem)
+    const statusRes = await GetImageStatus({ task_id: taskIdRef.value })
+    if (sessionId !== generateSessionId) return
+    if (statusRes?.code === 0 && statusRes?.data) {
+      const imageUrl = statusRes.data.image_url || statusRes.data.web_url || statusRes.data.new_image_url
+      if (imageUrl) {
+        stopPolling()
+        generatedImageUrl.value = imageUrl
+        currentImageItem.value = {
+          prompt: form.chinese_description,
+          model: form.model,
+          width: form.width,
+          height: form.height,
         }
         }
-        
-        clearInterval(checkInterval)
-      } else if (statusRes.code !== 0) {
-        console.error('检查视频状态失败', statusRes.msg)
+        isGenerating.value = false
+        ElMessage.success('图片生成成功')
+        return
+      }
+      if (statusRes.data.error) {
+        stopPolling()
+        isGenerating.value = false
+        ElMessage.error('图片生成失败: ' + statusRes.data.error)
       }
       }
-    } catch (error) {
-      console.error('检查视频进度失败', error)
     }
     }
-  }, 3000)
-}
-
-// 重置表单
-const resetForm = () => {
-  form.prompt = ''
-  form.model = 'sora-2'
-  form.seconds = 4
-  form.size = '1280x720'
-  videoUrl.value = ''
-}
-
-// 下载视频
-const downloadVideo = () => {
-  if (!videoUrl.value) return
-  
-  // 创建下载链接
-  const link = document.createElement('a')
-  link.href = videoUrl.value
-  link.download = `video_${Date.now()}.mp4`
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-  
-  ElMessage.success('视频下载中')
-}
-
-// 下载历史视频
-const downloadHistoryVideo = (row) => {
-  if (!row.web_url) {
-    ElMessage.warning('视频尚未生成完成')
-    return
+  } catch (error) {
+    console.error('轮询图片状态失败', error)
   }
   }
-  
-  const videoUrl = formatVideoUrl(row.web_url)
-  const link = document.createElement('a')
-  link.href = videoUrl
-  link.download = `video_${row.video_id || Date.now()}.mp4`
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-  
-  ElMessage.success('视频下载中')
 }
 }
 
 
-// 下载对话框中的视频
-const downloadDialogVideo = () => {
-  if (!currentVideoItem.value || !currentVideoItem.value.web_url) return
-  
-  downloadHistoryVideo(currentVideoItem.value)
+const startPolling = (taskId, sessionId) => {
+  taskIdRef.value = taskId
+  pollCountRef.value = 0
+  stopPolling()
+  pollIntervalRef.value = setInterval(() => pollImageStatus(sessionId), 5000)
+  pollImageStatus(sessionId)
 }
 }
 
 
-// 重新生成
-const regenerateVideo = () => {
-  generateVideo()
+const cancelGenerate = () => {
+  generateSessionId++
+  stopPolling()
+  taskIdRef.value = ''
+  isGenerating.value = false
+  ElMessage.info('已取消生成')
 }
 }
 
 
-// 播放历史视频
-const playHistoryVideo = (row) => {
-  if (!row.web_url) {
-    ElMessage.warning('视频尚未生成完成')
+const optimizePrompt = async () => {
+  if (!form.chinese_description?.trim()) {
+    ElMessage.warning('请先输入提示词')
     return
     return
   }
   }
-  
-  // 设置当前视频项和URL
-  currentVideoItem.value = row
-  dialogVideoUrl.value = formatVideoUrl(row.web_url)
-  
-  console.log('正在播放视频:', dialogVideoUrl.value)
-  
-  // 显示对话框
-  dialogVisible.value = true
-  
-  // 填充表单
-  // form.prompt = row.prompt
-  // form.model = row.model
-  // form.seconds = row.seconds
-  // form.size = row.size
-}
-
-// 重新获取视频
-const refreshVideo = async (row) => {
-  if (!row.video_id) {
-    ElMessage.warning('视频ID不存在,无法重新获取')
+  isOptimizing.value = true
+  try {
+    const res = await CallAIModelApi(
+      {
+        status_val: '文生文',
+        status_type: 'TextToImage',
+        prompt: form.chinese_description,
+        model: 'gpt-4',
+      },
+      { donNotShowLoading: true }
+    )
+    const content = res?.data?.content ?? res?.content
+    if (res?.code === 0 && content) {
+      form.chinese_description = content
+      ElMessage.success('提示词优化完成')
+    } else {
+      ElMessage.error(res?.msg || '优化失败')
+    }
+  } catch (e) {
+    console.error('[优化提示词] 错误:', e)
+    ElMessage.error('优化失败: ' + (e?.message || '未知错误'))
+  } finally {
+    isOptimizing.value = false
+  }
+}
+
+const texttoimg = async () => {
+  if (!form.chinese_description?.trim() && !hasAnyUploadImage()) {
+    ElMessage.warning('请填写提示词或上传原图/参考图')
     return
     return
   }
   }
-    ElMessage.info('正在重新获取视频...')
-    // 调用videoContent接口重新获取视频
-    const res = await videoContent({ video_id: row.video_id })
-    // 无论获取结果如何,都刷新表格数据
-    await loadVideoHistory()
-    if (res.code === 0) {
-      // 如果获取到了视频URL
-      if (res.data && res.data.web_url) {
-        ElMessage.success('视频获取成功')
-        
-        // 查找更新后的视频项
-        const updatedVideo = historyList.value.find(item => item.video_id === row.video_id)
-        if (updatedVideo && updatedVideo.web_url) {
-          // 自动播放获取到的视频
-          playHistoryVideo(updatedVideo)
+
+  const sessionId = ++generateSessionId
+  isGenerating.value = true
+  generatedImageUrl.value = ''
+  currentImageItem.value = null
+  stopPolling()
+  taskIdRef.value = ''
+
+  try {
+    const productFile = getUploadRawFile('product_img')
+    const templateFile = getUploadRawFile('template_img')
+    const hasProductImg = !!productFile
+    const hasTemplateImg = !!templateFile
+    const isImgToImg = hasProductImg || hasTemplateImg
+    const product_img = productFile ? await fileToBase64Compressed(productFile) : ''
+    const template_img = templateFile ? await fileToBase64Compressed(templateFile) : ''
+    if (sessionId !== generateSessionId) return
+
+    const payload = {
+      status_val: isImgToImg ? '图生图' : '文生图',
+      type: isImgToImg ? '图生图' : '文生图',
+      model: form.model,
+      max_images: form.max_images,
+      width: form.width,
+      height: form.height,
+      prompt: form.chinese_description,
+      prompt: form.chinese_description,
+      selectedOption: form.model,
+      product_img,
+      template_img,
+      status_type: 'texttoimage',
+      sys_id: userStore.userInfo.nickName,
+    }
+    // console.log(payload);return;
+    const res = await CallAIModelApi(payload)
+    if (sessionId !== generateSessionId) return
+
+    if (res.code === 200 || res.code === 0) {
+      const data = res.data || {}
+      const imageUrl = data.image_url || data.web_url || data.new_image_url || data.url
+      const taskId = data.task_id || data.image_id
+
+      if (imageUrl) {
+        generatedImageUrl.value = imageUrl
+        currentImageItem.value = {
+          prompt: form.chinese_description,
+          model: form.model,
+          width: form.width,
+          height: form.height,
         }
         }
+        isGenerating.value = false
+        ElMessage.success('图片生成成功')
+      } else if (taskId) {
+        ElMessage.success('图片生成任务已提交')
+        startPolling(taskId, sessionId)
       } else {
       } else {
-        ElMessage.info('视频仍在生成中,请稍后再试')
+        isGenerating.value = false
+        ElMessage.success(res.msg || '图片生成任务已提交')
       }
       }
+    } else {
+      isGenerating.value = false
+      ElMessage.error(res.msg || '图片生成失败')
     }
     }
+  } catch (error) {
+    if (sessionId !== generateSessionId) return
+    console.error('图片生成过程中发生错误:', error)
+    isGenerating.value = false
+    ElMessage.error('图片生成失败,请检查网络连接后重试')
+  }
 }
 }
 
 
-// 处理模型选择变化
-const handleModelChange = (modelName) => {
-  // 找到当前选中模型的ID
-  const selectedModel = txttoimg_modelList.value.find(item => item.txttoimg === modelName)
-  if (selectedModel) {
-    selectedIds.value.wenshengtu = selectedModel.id
-    form.model = modelName
+const downloadImage = async () => {
+  if (!generatedImageUrl.value) return
+  const fullUrl = formatImageUrl(generatedImageUrl.value)
+  const url = fullUrl.startsWith('http') ? fullUrl : (fullUrl.startsWith('/') ? fullUrl : '/' + fullUrl)
+  try {
+    const res = await fetch(url, {
+      headers: { 'x-token': userStore.token },
+      credentials: 'include',
+    })
+    if (!res.ok) throw new Error(res.statusText)
+    const blob = await res.blob()
+    const blobUrl = URL.createObjectURL(blob)
+    const link = document.createElement('a')
+    link.href = blobUrl
+    link.download = `文生图_${Date.now()}.png`
+    document.body.appendChild(link)
+    link.click()
+    setTimeout(() => {
+      document.body.removeChild(link)
+      URL.revokeObjectURL(blobUrl)
+    }, 100)
+    ElMessage.success('下载完成')
+  } catch (error) {
+    console.error('[下载] 失败:', error)
+    ElMessage.error('下载失败,请重试')
   }
   }
 }
 }
 
 
-// 设置默认模型
-const setActive = async (type) => {
-  const id = selectedIds.value[type]
-  if (id) {
-    try {
-      // 调用API设置默认模型
-      const res = await txttoimg_update({ id: id,type:"wenshengtu"})
-      if (res.code === 0) {
-        usedIds.value[type] = id
-        ElMessage.success('默认模型设置成功')
+const openRecordDialog = () => {
+  recordDialogVisible.value = true
+}
+
+const onRecordDialogOpen = () => {
+  loadImageHistory()
+}
+
+const viewHistoryImage = (row) => {
+  const url = getHistoryImageUrl(row)
+  if (!url) {
+    ElMessage.warning('图片尚未生成完成')
+    return
+  }
+  recordPreviewItem.value = row
+  recordPreviewUrl.value = formatImageUrl(url)
+  recordPreviewVisible.value = true
+}
+
+const refreshImage = async (row) => {
+  const taskId = row.image_id || row.task_id
+  if (!taskId) {
+    ElMessage.warning('任务ID不存在,无法重新获取')
+    return
+  }
+  try {
+    ElMessage.info('正在重新获取图片...')
+    const res = await GetImageStatus({ task_id: taskId })
+    await loadImageHistory()
+    if (res.code === 0 && res.data) {
+      const imageUrl = res.data.image_url || res.data.web_url || res.data.new_image_url
+      if (imageUrl) {
+        ElMessage.success('图片获取成功')
+        const updated = historyList.value.find((item) => (item.image_id || item.task_id) === taskId)
+        if (updated) viewHistoryImage(updated)
       } else {
       } else {
-        ElMessage.error(res.msg || '设置默认模型失败')
+        ElMessage.info('图片仍在生成中,请稍后再试')
       }
       }
-    } catch (error) {
-      console.error('设置默认模型失败', error)
-      ElMessage.error('设置默认模型失败')
     }
     }
+  } catch (error) {
+    console.error('重新获取图片失败', error)
+    ElMessage.error('重新获取失败')
   }
   }
 }
 }
 
 
-// 搜索处理
-const handleSearch = () => {
-  // 重置页码为1
-  page.value = 1
-  // 重新加载数据
-  loadVideoHistory()
-}
-
-// 分页大小变化处理
 const handleSizeChange = (size) => {
 const handleSizeChange = (size) => {
   pageSize.value = size
   pageSize.value = size
-  // 重新加载数据
-  loadVideoHistory()
+  page.value = 1
+  loadImageHistory()
 }
 }
 
 
-// 当前页码变化处理
 const handleCurrentChange = (current) => {
 const handleCurrentChange = (current) => {
   page.value = current
   page.value = current
-  // 重新加载数据
-  loadVideoHistory()
+  loadImageHistory()
 }
 }
 
 
-// 删除历史视频
-const deleteHistoryVideo = async (id) => {
-  try {
-    // 这里应该调用实际的删除API
-    // const res = await deleteVideoApi({ id })
-    // if (res.code === 0) {
-      const index = historyList.value.findIndex(item => item.id === id)
-      if (index !== -1) {
-        historyList.value.splice(index, 1)
-        ElMessage.success('删除成功')
-      }
-    // }
-  } catch (error) {
-    console.error('删除视频失败', error)
-    ElMessage.error('删除失败,请重试')
-  }
-}
+onMounted(() => {
+  loadModelList()
+})
+
+onBeforeUnmount(() => {
+  stopPolling()
+  UPLOAD_KEYS.forEach((key) => {
+    if (uploadPreviewUrls[key]) URL.revokeObjectURL(uploadPreviewUrls[key])
+  })
+})
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
+.texttoimage-page {
+  padding: 6px 10px;
+  height: calc(100vh - 200px);
+  max-height: calc(100vh - 200px);
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.two-column-layout {
+  display: flex;
+  gap: 10px;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
 
 
-::v-deep(.el-card__body){
-	padding: 0px;
+.left-column {
+  flex: 0 0 380px;
+  min-width: 280px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 }
-.video-generation-container {
-  padding: 0px;
-  
-  .card-header {
+
+.right-column {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.config-card,
+.result-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+
+  :deep(.el-card__body) {
+    padding: 0;
+    flex: 1;
+    min-height: 0;
     display: flex;
     display: flex;
-    justify-content: space-between;
-    align-items: center;
+    flex-direction: column;
+    overflow: hidden;
+  }
+}
+
+.config-card :deep(.el-card__header) {
+  display: none;
+}
+
+.result-card :deep(.el-card__header) {
+  padding: 10px 14px;
+  flex-shrink: 0;
+  background: linear-gradient(180deg, #fafbfc 0%, #f5f7fa 100%);
+  border-bottom: 1px solid #ebeef5;
+}
+
+.result-card :deep(.el-card__body) {
+  padding: 8px;
+}
+
+.result-card :deep(.record-btn-header.el-button) {
+  display: inline-flex;
+  align-items: center;
+  height: auto;
+  padding: 6px 14px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 8px;
+  color: #409eff !important;
+  background: #ecf5ff !important;
+  border: 1px solid #d9ecff !important;
+  box-shadow: none;
+
+  &:hover,
+  &:focus {
+    background: #409eff !important;
+    color: #fff !important;
+    border-color: #409eff !important;
   }
   }
-  
-  .refresh-button {
-    margin-left: auto;
+}
+
+.config-body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.config-form {
+  flex: 1;
+  min-height: 0;
+  padding: 6px 10px;
+  overflow-y: auto;
+  overflow-x: hidden;
+
+  :deep(.el-form-item) {
+    margin-bottom: 4px;
   }
   }
 }
 }
 
 
-.el-form {
-  .el-form-item {
-    margin-bottom: 10px;
-    padding: 0px;
+.upload-row-inner {
+  display: flex;
+  gap: 10px;
+  width: 100%;
+}
+
+.upload-cell {
+  flex: 1;
+  min-width: 0;
+  max-width: 140px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.upload-cell-inner {
+  position: relative;
+}
+
+.upload-label {
+  font-size: 12px;
+  color: #606266;
+
+  .optional {
+    color: #909399;
+    font-weight: normal;
   }
   }
 }
 }
 
 
-.el-table {
-  .el-button {
-    margin-right: 5px;
+.image-uploader.square {
+  width: 100%;
+
+  :deep(.el-upload) {
+    width: 100%;
+    display: block;
+  }
+
+  .upload-placeholder,
+  .uploaded-preview {
+    width: 100%;
+    aspect-ratio: 1;
+    height: auto !important;
+    min-height: 80px;
+    max-height: 130px;
+    max-width: 130px;
+    margin: 0 auto;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s;
+    background: #fafafa;
+    overflow: hidden;
+    box-sizing: border-box;
+
+    &:hover {
+      border-color: #409eff;
+      background: #f0f9ff;
+    }
+  }
+
+  .upload-placeholder {
+    color: #8c939d;
+
+    span {
+      font-size: 11px;
+      margin-top: 2px;
+    }
+  }
+
+  .uploaded-preview {
+    position: relative;
+
+    .preview-image {
+      width: 100%;
+      height: 100%;
+    }
+
+    .upload-mask {
+      position: absolute;
+      inset: 0;
+      background: rgba(0, 0, 0, 0.4);
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      color: #fff;
+      opacity: 0;
+      transition: opacity 0.2s;
+
+      span {
+        font-size: 11px;
+        margin-top: 2px;
+      }
+
+      .swap-icon {
+        width: 18px;
+        height: 18px;
+      }
+    }
+
+    &:hover .upload-mask {
+      opacity: 1;
+    }
   }
   }
 }
 }
 
 
-// 对话框视频样式
-.dialog-video-preview {
+.upload-remove-btn {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  z-index: 3;
+  width: 18px;
+  height: 18px;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.5);
+  color: #fff;
+  cursor: pointer;
   display: flex;
   display: flex;
+  align-items: center;
   justify-content: center;
   justify-content: center;
+  line-height: 1;
+
+  &:hover {
+    background: #f56c6c;
+  }
+}
+
+.image-error {
+  width: 100%;
+  height: 100%;
+  display: flex;
   align-items: center;
   align-items: center;
-  min-height: 400px;
-  padding: 20px 0;
-  
-  video {
-    max-height: 600px;
-    max-width: 100%;
-    border-radius: 4px;
-  }
-  
-  .loading-text {
-    font-size: 16px;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #909399;
+}
+
+.prompt-form-item {
+  margin-bottom: 4px;
+
+  :deep(.el-form-item__label) {
+    width: 100%;
+    display: flex;
+    justify-content: flex-start;
+    text-align: left;
+    padding-left: 0;
+    line-height: 1.4;
+  }
+}
+
+.prompt-block {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  width: 100%;
+}
+
+.prompt-wrapper {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  overflow: hidden;
+  background: #fff;
+
+  :deep(.el-textarea__inner) {
+    border: none;
+    border-radius: 0;
+    box-shadow: none;
+    padding-bottom: 28px;
+  }
+
+  :deep(.el-input__count) {
+    background: #fff;
+    padding-left: 6px;
+    font-size: 12px;
     color: #909399;
     color: #909399;
   }
   }
 }
 }
 
 
-// 最新生成结果样式
-.video-result-preview {
+.prompt-textarea {
+  width: 100%;
+
+  :deep(textarea) {
+    resize: none;
+    min-height: 100px;
+  }
+}
+
+.prompt-actions-row {
   display: flex;
   display: flex;
-  justify-content: space-between;
   align-items: center;
   align-items: center;
-  padding: 10px;
-  background-color: #fafafa;
-  border-radius: 4px;
-  
-  .video-result-info {
-    flex: 1;
-    p {
-      margin: 5px 0;
-      font-size: 14px;
+  justify-content: flex-end;
+  gap: 10px;
+  width: 100%;
+  padding: 4px 0 0;
+}
+
+.config-row-compact {
+  margin-bottom: 0 !important;
+}
+
+.config-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  width: 100%;
+}
+
+.config-label {
+  font-size: 12px;
+  color: #606266;
+}
+
+.model-item {
+  min-width: 0;
+}
+
+.max-images-item {
+  min-width: 0;
+}
+
+.max-images-select {
+  width: 100%;
+}
+
+.model-select {
+  width: 100%;
+
+  :deep(.el-select__wrapper) {
+    border-radius: 8px;
+    border: 1px solid #e4e7ed;
+    background: #fafbfc;
+    box-shadow: none;
+    min-height: 36px;
+    padding: 6px 14px;
+
+    &:hover {
+      border-color: #c0c4cc;
+      background: #fff;
     }
     }
   }
   }
-  
-  .video-result-actions {
-    display: flex;
-    gap: 10px;
+
+  :deep(.el-select__wrapper.is-focused) {
+    border-color: #409eff;
+    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+    background: #fff;
   }
   }
 }
 }
 
 
-// 响应式调整
-@media screen and (max-width: 768px) {
-  .dialog-video-preview {
-    min-height: 300px;
-    
-    video {
-      max-height: 400px;
-    }
+.size-item {
+  width: 100%;
+}
+
+.size-groups {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.size-group {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.size-group-no-label .size-group-options {
+  flex: 1;
+}
+
+.size-group-options {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 4px;
+}
+
+.size-chip {
+  padding: 2px 8px;
+  font-size: 12px;
+  border-radius: 4px;
+  cursor: pointer;
+  user-select: none;
+  background: #f5f7fa;
+  color: #606266;
+  border: 1px solid #e4e7ed;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: #409eff;
+    color: #409eff;
   }
   }
-  
-  .video-result-preview {
+
+  &.active {
+    background: #ecf5ff;
+    border-color: #409eff;
+    color: #409eff;
+    font-weight: 500;
+  }
+}
+
+.size-input-label {
+  font-size: 12px;
+  color: #606266;
+}
+
+.custom-size-input {
+  width: 88px;
+}
+
+.size-sep {
+  color: #909399;
+  font-size: 12px;
+}
+
+.result-area {
+  flex: 1;
+  min-height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f9fafb;
+  border-radius: 6px;
+  padding: 12px;
+  overflow: hidden;
+}
+
+.result-preview {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+}
+
+.result-image {
+  width: 100%;
+  max-height: min(52vh, 480px);
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
+}
+
+.result-meta {
+  width: 100%;
+  max-width: 720px;
+  text-align: center;
+  padding: 0 8px;
+}
+
+.result-meta-prompt {
+  margin: 0 0 4px;
+  font-size: 13px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.result-meta-info {
+  font-size: 12px;
+  color: #909399;
+}
+
+.result-actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+.result-empty {
+  text-align: center;
+  color: #909399;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+
+  .empty-icon {
+    color: #dcdfe6;
+  }
+
+  p {
+    margin: 0;
+    font-size: 14px;
+  }
+
+  span {
+    font-size: 12px;
+    color: #c0c4cc;
+  }
+}
+
+.generate-btn-center {
+  margin-top: 8px;
+}
+
+.result-loading {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  color: #606266;
+
+  .loading-icon {
+    color: #409eff;
+  }
+}
+
+.image-error-large {
+  width: 100%;
+  min-height: 120px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #909399;
+  gap: 8px;
+}
+
+.history-ref-thumb {
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  cursor: zoom-in;
+}
+
+.history-thumb-preview {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.history-ref-empty {
+  color: #c0c4cc;
+  font-size: 12px;
+}
+
+.history-thumb-fallback {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f0f2f5;
+  color: #909399;
+  border-radius: 4px;
+}
+
+.record-dialog-pagination {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.record-preview-body {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.record-preview-image {
+  width: 100%;
+  max-height: min(70vh, 520px);
+  border-radius: 6px;
+}
+
+.record-preview-meta {
+  .record-preview-prompt {
+    margin: 0 0 6px;
+    font-size: 13px;
+    color: #303133;
+    line-height: 1.5;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+  }
+
+  .record-preview-info {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+
+@media screen and (max-width: 992px) {
+  .two-column-layout {
     flex-direction: column;
     flex-direction: column;
-    align-items: flex-start;
-    gap: 15px;
-    
-    .video-result-actions {
-      width: 100%;
-      justify-content: flex-start;
-    }
   }
   }
+
+  .left-column {
+    flex: 0 0 auto;
+    max-height: 50vh;
+    min-width: 100%;
+  }
+
+  .right-column {
+    flex: 1;
+    min-height: 280px;
+  }
+}
+</style>
+
+<style lang="scss">
+.el-image-viewer__wrapper {
+  z-index: 3200 !important;
+}
+
+.el-image-viewer__mask {
+  cursor: zoom-out;
 }
 }
-</style>
+</style>

+ 1392 - 496
src/view/performance/QualityAssessment/video.vue

@@ -1,622 +1,1518 @@
 <template>
 <template>
-  <div class="video-generation-container">
-    <el-card class="mb-4">
-      <template #header>
-          <span>文生视频生成</span>
-      </template>
-      <br>
-      <el-form ref="formRef" :model="form" label-width="100px" class="mb-4"
-          style="margin: 0px;padding: 0px !important;">
-        <el-form-item label="提示词" prop="prompt" required>
-          <el-input 
-            v-model="form.prompt" 
-            type="textarea" 
-            :rows="3" 
-            placeholder="请输入视频描述,例如:一个穿着宇航服的宇航员在月球上行走, 高品质, 电影级"
-          />
-        </el-form-item>
-        
-        <el-form-item label="模型" prop="model">
-          <el-select v-model="form.model" placeholder="请选择模型">
-            <el-option label="sora-2" value="sora-2" />
-          </el-select>
-        </el-form-item>
-        
-        <el-form-item label="时长(秒)" prop="seconds">
-          <el-select 
-            v-model.number="form.seconds" 
-            placeholder="请选择视频时长" 
-            style="width: 100%"
-          >
-            <el-option label="4秒" :value="4" />
-            <el-option label="8秒" :value="8" />
-            <el-option label="12秒" :value="12" />
-          </el-select>
-        </el-form-item>
-        
-        <el-form-item label="分辨率" prop="size">
-          <el-select v-model="form.size" placeholder="请选择分辨率" style="width: 100%">
-            <el-option label="720x1280" value="720x1280" />
-            <el-option label="1280x720" value="1280x720" />
-          </el-select>
-        </el-form-item>
-        
-        <el-form-item>
-           <!-- <el-input 
-              v-model="searchInfo" 
-              placeholder="搜索提示词" 
-              @keyup.enter="handleSearch"
-              clearable
-              @clear="handleSearch"
-              style="width: 230px; margin-right: 10px;margin: 0px;" 
-            />
-            <el-button type="primary" icon="Search" class="search" @click="handleSearch">查询</el-button> -->
-          <el-button 
-            type="primary" 
-            @click="generateVideo" 
-            :loading="isGenerating"
-          >
-            {{ isGenerating ? '生成中...' : '生成视频' }}
-          </el-button>
-          <el-button @click="resetForm">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-    
-    <!-- 视频播放对话框 -->
+  <div class="video-generation-page">
+    <div class="two-column-layout">
+      <div class="left-column">
+        <el-card class="config-card" shadow="hover">
+          <div class="config-body">
+            <el-form ref="formRef" :model="form" label-position="top" size="small" class="config-form" hide-required-asterisk>
+              <el-form-item class="upload-row">
+                <div class="upload-row-inner">
+                  <div
+                    v-for="frame in frameUploadList"
+                    :key="frame.key"
+                    class="upload-cell"
+                  >
+                    <span class="upload-label">{{ frame.label }}<span class="optional">(可选)</span></span>
+                    <div class="upload-cell-inner">
+                      <el-upload
+                        class="image-uploader square"
+                        :show-file-list="false"
+                        :auto-upload="false"
+                        :on-change="(uploadFile) => onFrameFileChange(frame.key, uploadFile)"
+                        accept="image/jpeg,image/png,image/webp,image/jpg"
+                      >
+                        <div v-if="hasFrameImage(frame.key)" class="uploaded-preview">
+                          <el-image :src="getFrameDisplayUrl(frame.key)" fit="contain" class="preview-image">
+                            <template #error>
+                              <div class="image-error"><el-icon><Picture /></el-icon></div>
+                            </template>
+                          </el-image>
+                          <div class="upload-mask">
+                            <svg class="swap-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                              <path d="M4 8h12l-3-3 3-3" />
+                              <path d="M20 16H8l3 3-3 3" />
+                            </svg>
+                            <span>更换</span>
+                          </div>
+                        </div>
+                        <div v-else class="upload-placeholder">
+                          <el-icon :size="20"><Plus /></el-icon>
+                          <span>上传{{ frame.label }}</span>
+                        </div>
+                      </el-upload>
+                      <button
+                        v-if="hasFrameImage(frame.key)"
+                        type="button"
+                        class="upload-remove-btn"
+                        title="移除"
+                        @click.stop="clearFrameImage(frame.key)"
+                      >
+                        <el-icon><Close /></el-icon>
+                      </button>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="prompt-form-item">
+                <template #label><span>提示词</span></template>
+                <div class="prompt-block">
+                  <div v-loading="isOptimizing" element-loading-text="优化中..." class="prompt-wrapper">
+                    <el-input
+                      v-model="form.prompt"
+                      type="textarea"
+                      :rows="5"
+                      placeholder="请输入视频描述,可补充镜头与运动"
+                      maxlength="500"
+                      show-word-limit
+                      resize="none"
+                      class="prompt-textarea"
+                    />
+                  </div>
+                  <div class="prompt-actions-row">
+                    <el-button type="success" link size="small" :loading="isOptimizing" @click="optimizePrompt">
+                      <el-icon><MagicStick /></el-icon>{{ isOptimizing ? '优化中...' : '优化提示词' }}
+                    </el-button>
+                  </div>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item model-item">
+                  <span class="config-label">模型</span>
+                  <el-select
+                    v-model="form.model"
+                    placeholder="请选择模型"
+                    size="small"
+                    class="model-select"
+                    filterable
+                  >
+                    <el-option
+                      v-for="(item, idx) in modelList"
+                      :key="item?.id ?? item?.ID ?? idx"
+                      :label="item.model_alias || item.model_name || '-'"
+                      :value="item.model_name || item.model_alias || ''"
+                    />
+                  </el-select>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item size-item">
+                  <span class="config-label">时长</span>
+                  <div class="size-group-options">
+                    <span
+                      v-for="opt in durationOptions"
+                      :key="opt.value"
+                      class="size-chip"
+                      :class="{ active: form.seconds === opt.value }"
+                      @click="form.seconds = opt.value"
+                    >{{ opt.label }}</span>
+                  </div>
+                </div>
+              </el-form-item>
+
+              <el-form-item class="config-row-compact">
+                <div class="config-item size-item">
+                  <span class="config-label">分辨率</span>
+                  <div class="size-group-options">
+                    <span
+                      v-for="opt in resolutionOptions"
+                      :key="opt.value"
+                      class="size-chip"
+                      :class="{ active: form.size === opt.value }"
+                      @click="form.size = opt.value"
+                    >{{ opt.label }}</span>
+                  </div>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-card>
+      </div>
+
+      <div class="right-column">
+        <el-card class="result-card" shadow="hover">
+          <template #header>
+            <el-button size="small" class="record-btn-header" @click="openRecordDialog">
+              视频生成记录
+            </el-button>
+          </template>
+          <div class="result-area">
+            <div v-if="previewVideoUrl" class="result-preview">
+              <video :key="previewVideoUrl" :src="previewVideoUrl" controls class="result-video" />
+              <div v-if="currentVideoItem" class="result-meta">
+                <p class="result-meta-prompt" :title="currentVideoItem.prompt">{{ currentVideoItem.prompt || '-' }}</p>
+                <span class="result-meta-info">
+                  {{ currentVideoItem.model }} · {{ currentVideoItem.seconds }}秒 · {{ currentVideoItem.size }}
+                </span>
+              </div>
+              <div class="result-actions">
+                <el-button type="primary" size="small" :loading="isGenerating" @click="generateVideo">
+                  {{ isGenerating ? '生成中...' : '再次生成' }}
+                </el-button>
+                <el-button type="primary" size="small" :disabled="!previewVideoUrl" @click="downloadCurrentVideo">
+                  下载
+                </el-button>
+              </div>
+            </div>
+            <div v-else-if="isGenerating" class="result-loading">
+              <el-icon :size="48" class="loading-icon is-loading"><Loading /></el-icon>
+              <p>正在生成视频,请稍候...</p>
+            </div>
+            <div v-else class="result-empty">
+              <el-icon :size="60" class="empty-icon"><VideoCamera /></el-icon>
+              <p>视频将在此处显示</p>
+              <span>填写提示词或上传首帧/尾帧后点击下方按钮生成</span>
+              <el-button type="primary" :loading="isGenerating" size="large" class="generate-btn-center" @click="generateVideo">
+                {{ isGenerating ? '生成中...' : '立即生成' }}
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
     <el-dialog
     <el-dialog
-      v-model="dialogVisible"
-      title="视频播放"
-      width="80%"
-	  style="margin: 0px;margin-left: 10%;margin-top: 3%;"
-      center
+      v-model="recordDialogVisible"
+      title="视频生成记录"
+      align-center
+      width="min(95vw, 1200px)"
+      class="video-record-dialog"
       destroy-on-close
       destroy-on-close
+      @open="onRecordDialogOpen"
     >
     >
-      <div class="dialog-video-preview">
-        <div v-if="!dialogVideoUrl" class="loading-text">加载中...</div>
-        <video 
-          :src="dialogVideoUrl" 
-          controls 
-          width="100%" 
-          height="auto"
-          v-else
-          autoplay
-        ></video>
-      </div>
-      <template #footer>
-        <el-button @click="dialogVisible = false">关闭</el-button>
-      </template>
-    </el-dialog>
-    
-    <!-- 历史记录 -->
-    <el-card class="mt-1">
-      <el-table :data="historyList"
-                :row-style="{ height: '20px' }"
-                :cell-style="{ padding: '0px' }" :header-row-style="{ height: '20px' }"
-                :header-cell-style="{ padding: '0px' }"
-                style="width: 100%;height: 40vh">
-        <el-table-column align="center" prop="id" label="ID" width="80" />
-        <el-table-column prop="prompt" label="提示词" width="500">
+      <el-table :data="historyList" stripe size="small" max-height="min(60vh, 520px)" style="width: 100%">
+        <el-table-column align="center" prop="id" label="ID" width="70" />
+        <el-table-column label="首帧" width="72" align="center">
+          <template #default="scope">
+            <el-image
+              v-if="historyFrameThumb(scope.row, 'first')"
+              :src="historyFrameThumb(scope.row, 'first')"
+              fit="cover"
+              class="history-ref-thumb history-thumb-preview"
+              :preview-src-list="[historyFramePreview(scope.row, 'first')]"
+              preview-teleported
+              hide-on-click-modal
+              :z-index="3200"
+            >
+              <template #error>
+                <div class="history-thumb-fallback"><el-icon><Picture /></el-icon></div>
+              </template>
+            </el-image>
+            <span v-else class="history-ref-empty">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="尾帧" width="72" align="center">
+          <template #default="scope">
+            <el-image
+              v-if="historyFrameThumb(scope.row, 'last')"
+              :src="historyFrameThumb(scope.row, 'last')"
+              fit="cover"
+              class="history-ref-thumb history-thumb-preview"
+              :preview-src-list="[historyFramePreview(scope.row, 'last')]"
+              preview-teleported
+              hide-on-click-modal
+              :z-index="3200"
+            />
+            <span v-else class="history-ref-empty">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频" width="80" align="center">
           <template #default="scope">
           <template #default="scope">
-            <span :title="scope.row.prompt" class="text-ellipsis" style="display: inline-block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">{{ scope.row.prompt }}</span>
+            <div
+              v-if="getHistoryVideoPlayUrl(scope.row)"
+              class="history-video-cell"
+              title="点击预览视频"
+              @click="playHistoryVideo(scope.row)"
+            >
+              <el-image
+                v-if="historyFrameThumb(scope.row, 'first')"
+                :src="historyFrameThumb(scope.row, 'first')"
+                fit="cover"
+                class="history-ref-thumb"
+              >
+                <template #error>
+                  <div class="history-thumb-fallback"><el-icon><VideoCamera /></el-icon></div>
+                </template>
+              </el-image>
+              <div v-else class="history-video-placeholder">
+                <el-icon :size="22"><VideoCamera /></el-icon>
+              </div>
+              <span class="history-video-play-badge">
+                <el-icon :size="16"><CaretRight /></el-icon>
+              </span>
+            </div>
+            <span v-else class="history-ref-empty">-</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column prop="model" label="模型" width="100" />
-        <el-table-column prop="seconds" label="时长" width="80" />
+        <el-table-column prop="prompt" label="提示词" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="model" label="模型" width="100" show-overflow-tooltip />
+        <el-table-column prop="seconds" label="时长" width="64" align="center" />
         <el-table-column prop="size" label="分辨率" width="100" />
         <el-table-column prop="size" label="分辨率" width="100" />
-        <el-table-column prop="sys_rq" label="创建时间" width="180" />
-        <el-table-column label="状态" width="100">
+        <el-table-column prop="sys_rq" label="创建时间" width="168" />
+        <el-table-column label="状态" width="88" align="center">
           <template #default="scope">
           <template #default="scope">
-            <el-tag :type="scope.row.web_url ? 'success' : 'warning'">
+            <el-tag size="small" :type="scope.row.web_url ? 'success' : 'warning'">
               {{ scope.row.web_url ? '已完成' : '生成中' }}
               {{ scope.row.web_url ? '已完成' : '生成中' }}
             </el-tag>
             </el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="操作" width="300">
-            <template #default="scope">
-              <el-button 
-                size="small" 
-                @click="playHistoryVideo(scope.row)"
-                :disabled="!scope.row.web_url"
-              >
-                播放
-              </el-button>
-              <el-button 
-                size="small" 
-                type="primary" 
-                @click="refreshVideo(scope.row)"
-                v-if="!scope.row.web_url && scope.row.video_id"
-              >
-                重新获取视频
-              </el-button>
-              <!-- <el-button size="small" type="danger" @click="deleteHistoryVideo(scope.row.id)">删除</el-button> -->
-            </template>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="scope">
+            <el-button size="small" type="primary" link :disabled="!scope.row.web_url" @click="playHistoryVideo(scope.row)">
+              预览
+            </el-button>
+            <el-button
+              v-if="!scope.row.web_url && scope.row.video_id"
+              size="small"
+              type="primary"
+              link
+              @click="refreshVideo(scope.row)"
+            >
+              重新获取
+            </el-button>
+          </template>
         </el-table-column>
         </el-table-column>
       </el-table>
       </el-table>
-      <!-- 分页组件 -->
-      <div class="gva-pagination">
-          <el-pagination
-              v-model:current-page="page"
-              v-model:page-size="pageSize"
-              :page-sizes="[10, 30, 50, 100, 500, 1000]"
-              layout="total, sizes, prev, pager, next, jumper"
-              :total="total"
-              @size-change="handleSizeChange"
-              @current-change="handleCurrentChange">
-          </el-pagination>
+      <div class="gva-pagination record-dialog-pagination">
+        <el-pagination
+          v-model:current-page="page"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      v-model="recordPreviewVisible"
+      title="视频预览"
+      align-center
+      width="min(90vw, 900px)"
+      class="video-record-preview-dialog"
+      append-to-body
+      destroy-on-close
+    >
+      <div v-if="recordPreviewPlayUrl" class="record-preview-body">
+        <video :key="recordPreviewPlayUrl" :src="recordPreviewPlayUrl" controls class="record-preview-video" />
+        <div v-if="recordPreviewItem" class="record-preview-meta">
+          <p class="record-preview-prompt" :title="recordPreviewItem.prompt">{{ recordPreviewItem.prompt || '-' }}</p>
+          <span class="record-preview-info">
+            {{ recordPreviewItem.model }} · {{ recordPreviewItem.seconds }}秒 · {{ recordPreviewItem.size }}
+          </span>
         </div>
         </div>
-    </el-card>
+      </div>
+      <template #footer>
+        <el-button @click="recordPreviewVisible = false">关闭</el-button>
+        <el-button type="primary" :disabled="!recordPreviewPlayUrl" @click="downloadRecordPreview">下载</el-button>
+      </template>
+    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, shallowReactive, onMounted, onBeforeUnmount, toRaw } from 'vue'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
-import {Getvideolist,video,videoContent
-} from '@/api/mes/job'
+import { Plus, Picture, Close, Loading, VideoCamera, MagicStick, CaretRight } from '@element-plus/icons-vue'
+import { Getvideolist, Create_ImgToVideo, Get_ImgToVideo, GetAIModel, CallAIModelApi } from '@/api/mes/job'
+import { displayImageUrl, displayImageUrlWithReference, materialThumbnailUrl } from '@/utils/displayImageUrl.js'
+
+const modelList = ref([])
+const FRAME_KEYS = ['first', 'last']
+const frameUploadList = [
+  { key: 'first', label: '首帧图', field: 'first_image_url' },
+  { key: 'last', label: '尾帧图', field: 'last_image_url' }
+]
+const framePreviewUrls = reactive({ first: '', last: '' })
+/** 存 File 用 shallowReactive,避免 deep reactive 导致 instanceof File 失效 */
+const frameFiles = shallowReactive({ first: null, last: null })
+const MAX_REF_IMAGE_MB = 10
+const VIDEO_SCENE_TYPES = ['文本生视频', '文本图片生视频', '文本视频生视频']
+
+function parseModelTypes (modelType) {
+  if (modelType == null || modelType === '') return []
+  return String(modelType).split(/[,,]/).map((t) => t.trim()).filter(Boolean)
+}
+
+function modelSupportsScene (item, scenes = VIDEO_SCENE_TYPES) {
+  const types = parseModelTypes(item?.model_type)
+  return scenes.some((scene) => types.includes(scene))
+}
+
+function dedupeModelList (list) {
+  const seenName = new Set()
+  const seenLabel = new Set()
+  const out = []
+  for (const item of list) {
+    const name = (item.model_name || '').trim().toLowerCase()
+    const alias = (item.model_alias || '').trim().toLowerCase()
+    const label = (alias || name).trim().toLowerCase()
+    if (name && seenName.has(name)) continue
+    if (label && seenLabel.has(label)) continue
+    if (name) seenName.add(name)
+    if (label) seenLabel.add(label)
+    out.push(item)
+  }
+  return out
+}
+
+const defaultModelValue = () => {
+  const first = modelList.value[0]
+  return first ? (first.model_name || first.model_alias || '') : ''
+}
+
+const loadModelList = async () => {
+  try {
+    const res = await GetAIModel({})
+    const list = res?.data?.list ?? res?.data ?? []
+    const raw = (Array.isArray(list) ? list : []).filter((item) => modelSupportsScene(item))
+    modelList.value = dedupeModelList(raw)
+    if (modelList.value.length > 0) {
+      const cur = (form.model || '').trim()
+      const exists = modelList.value.some((m) => (m.model_name || m.model_alias || '') === cur)
+      if (!cur || !exists) form.model = defaultModelValue()
+    } else {
+      form.model = ''
+    }
+  } catch {
+    modelList.value = []
+    form.model = ''
+  }
+}
+
+const durationOptions = [
+  { label: '5秒', value: 5 },
+  { label: '8秒', value: 8 },
+  { label: '12秒', value: 12 }
+]
+
+const resolutionOptions = [
+  { label: '9:16', value: '9:16' },
+  { label: '16:9', value: '16:9' }
+]
 
 
-// 表单数据
 const form = reactive({
 const form = reactive({
   prompt: '',
   prompt: '',
-  model: 'sora-2',
-  seconds: 4,
-  size: '1280x720'
+  first_image_url: '',
+  last_image_url: '',
+  model: '',
+  seconds: 5,
+  size: '16:9'
 })
 })
 
 
-// 表单引用
 const formRef = ref()
 const formRef = ref()
-
-// 状态变量
 const isGenerating = ref(false)
 const isGenerating = ref(false)
-const videoUrl = ref('')
-const dialogVisible = ref(false)
-const dialogVideoUrl = ref('')
+const isOptimizing = ref(false)
+const previewVideoUrl = ref('')
+const recordDialogVisible = ref(false)
+const recordPreviewVisible = ref(false)
+const recordPreviewPlayUrl = ref('')
+const recordPreviewItem = ref(null)
 const currentVideoItem = ref(null)
 const currentVideoItem = ref(null)
 const historyList = ref([])
 const historyList = ref([])
-// 分页相关数据
 const page = ref(1)
 const page = ref(1)
 const pageSize = ref(30)
 const pageSize = ref(30)
 const total = ref(0)
 const total = ref(0)
-// 搜索关键词
 const searchInfo = ref('')
 const searchInfo = ref('')
 
 
-// 格式化视频URL
-const formatVideoUrl = (path) => {
-  if (!path) return ''
-  
-  // 避免重复拼接http://
-  if (path.startsWith('http://') || path.startsWith('https://')) {
-    return path
+const OSS_ASSET_FALLBACK = 'https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com'
+
+const getOssAssetBase = () => {
+  const v = import.meta.env.VITE_OSS_ASSET_BASE
+  return (v && String(v).trim()) ? String(v).trim().replace(/\/$/, '') : OSS_ASSET_FALLBACK.replace(/\/$/, '')
+}
+
+/** OSS / uploads 相对路径、完整 https 链统一为可访问地址(图片、视频共用) */
+const formatAssetUrl = (path, referenceUrl) => {
+  if (path == null || !String(path).trim()) return ''
+  const raw = String(path).trim()
+  let out = referenceUrl ? displayImageUrlWithReference(raw, referenceUrl) : ''
+  if (!out) out = displayImageUrl(raw)
+  if (!out) return ''
+  if (/^(https?:|\/\/|data:|blob:)/i.test(out)) return out
+  return `${getOssAssetBase()}/${out.replace(/^\//, '')}`
+}
+
+const formatRefImageUrl = (path, referenceUrl) => formatAssetUrl(path, referenceUrl)
+const formatVideoUrl = (path, referenceUrl) => formatAssetUrl(path, referenceUrl)
+
+const pickRowAssetPath = (row, ...keys) => {
+  for (const k of keys) {
+    const v = row?.[k]
+    if (v != null && String(v).trim()) return String(v).trim()
   }
   }
-  
-  const base = 'http://20.0.16.128:9093'
-  // 避免重复拼接斜杠
-  const normalizedPath = path.startsWith('/') ? path.substring(1) : path
-  const normalizedBase = base.endsWith('/') ? base : `${base}/`
-  
-  return `${normalizedBase}${normalizedPath}`
+  return ''
 }
 }
 
 
-// 加载历史视频列表
-const loadVideoHistory = async () => {
+const getHistoryRowAssetRef = (row) =>
+  pickRowAssetPath(row, 'web_url', 'first_image_url', 'last_image_url', 'image_url')
+
+const formatRefThumbUrl = (path, referenceUrl) => {
+  const full = formatAssetUrl(path, referenceUrl)
+  if (!full) return ''
+  return materialThumbnailUrl(full, 64) || full
+}
+
+/** 无首帧图时,用 OSS 视频截帧做列表缩略图(旧任务常只有 web_url) */
+const getOssVideoSnapshotUrl = (videoUrl, maxEdge = 128) => {
+  if (!videoUrl) return ''
+  let u = videoUrl
+  if (u.startsWith('//')) u = `https:${u}`
+  if (!/^https?:\/\//i.test(u)) return ''
   try {
   try {
-    const params = {
-      page: page.value,
-      limit: pageSize.value
-    }
-    // 如果有搜索关键词,添加到请求参数中
-    if (searchInfo.value.trim()) {
-      params.search = searchInfo.value.trim()
-    }
-    
-    const res = await Getvideolist(params)
-    if (res.code === 0) {
-      historyList.value = Array.isArray(res.data.list) ? res.data.list : res.data
-      // 如果后端返回total,使用返回值,否则保持现有值
-      // 设置总数
-      total.value = res.count || 0
+    const parsed = new URL(u)
+    if (!parsed.hostname.includes('aliyuncs.com')) return ''
+    if (parsed.searchParams.has('x-oss-process')) return u
+    parsed.searchParams.set(
+      'x-oss-process',
+      `video/snapshot,t_0,f_jpg,w_${maxEdge},h_${maxEdge},m_fast`
+    )
+    return parsed.toString()
+  } catch {
+    return ''
+  }
+}
+
+const getHistoryFrameCells = (row) => {
+  const ref = getHistoryRowAssetRef(row)
+  const firstRaw = pickRowAssetPath(row, 'first_image_url', 'first_image', 'FirstImageUrl', 'image_url')
+  const lastRaw = pickRowAssetPath(row, 'last_image_url', 'last_image', 'LastImageUrl')
+  const firstPreview = formatAssetUrl(firstRaw, ref)
+  const lastPreview = formatAssetUrl(lastRaw, ref)
+  let first = formatRefThumbUrl(firstRaw, ref)
+  let firstPreviewOut = firstPreview
+  if (!first && row?.web_url) {
+    const videoFull = formatAssetUrl(row.web_url, ref)
+    const snap = getOssVideoSnapshotUrl(videoFull, 64)
+    if (snap) {
+      first = snap
+      firstPreviewOut = getOssVideoSnapshotUrl(videoFull, 720) || snap
     }
     }
-  } catch (error) {
-    console.error('加载视频历史失败', error)
-    ElMessage.error('加载历史记录失败')
+  }
+  return {
+    first,
+    last: formatRefThumbUrl(lastRaw, ref),
+    firstPreview: firstPreviewOut,
+    lastPreview
   }
   }
 }
 }
 
 
-// 组件挂载时加载历史
-loadVideoHistory()
+const historyFrameThumb = (row, which) => {
+  const cells = getHistoryFrameCells(row)
+  return which === 'first' ? cells.first : cells.last
+}
 
 
-// 生成视频
-const generateVideo = async () => {
-// console.log(form);
-// return false;
-  // 表单验证
-  if (!form.prompt) {
-    ElMessage.warning('请输入提示词')
+const historyFramePreview = (row, which) => {
+  const cells = getHistoryFrameCells(row)
+  return which === 'first' ? cells.firstPreview : cells.lastPreview
+}
+
+const getHistoryVideoPlayUrl = (row) => {
+  if (!row?.web_url) return ''
+  return formatVideoUrl(row.web_url, getHistoryRowAssetRef(row))
+}
+
+const getFrameField = (key) => (key === 'first' ? 'first_image_url' : 'last_image_url')
+const getFrameLabel = (key) => (key === 'first' ? '首帧' : '尾帧')
+
+const hasFrameImage = (key) => {
+  return !!(frameFiles[key] || framePreviewUrls[key])
+}
+
+const getFrameDisplayUrl = (key) => {
+  if (framePreviewUrls[key]) return framePreviewUrls[key]
+  const field = getFrameField(key)
+  const ref = form.first_image_url || form.last_image_url
+  return formatRefImageUrl(form[field], ref)
+}
+
+const hasAnyFrameImage = () => FRAME_KEYS.some((key) => hasFrameImage(key))
+
+/** auto-upload=false 时不会走 before-upload,须在 on-change 里处理选图 */
+const getFrameRawFile = (key) => {
+  const f = frameFiles[key]
+  if (!f) return null
+  const raw = toRaw(f)
+  return raw instanceof File ? raw : (f instanceof File ? f : null)
+}
+
+const onFrameFileChange = (key, uploadFile) => {
+  const file = uploadFile?.raw
+  if (!file) return
+  const mimeOk = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg'].includes(file.type)
+  const extOk = /\.(jpe?g|png|webp)$/i.test(file.name || '')
+  if (!mimeOk && !extOk) {
+    ElMessage.error('仅支持 jpg、png、webp 格式')
     return
     return
   }
   }
-  
-  isGenerating.value = true
-  
-  try {
-    // 第一步:调用video接口生成视频ID
-    const videoIdRes = await video(form)
-    
-    if (videoIdRes.code === 0 && videoIdRes.data?.video_id) {
-      const videoId = videoIdRes.data.video_id
-      ElMessage.success('已提交视频生成请求,正在生成中...')
-      
-      // 第二步:使用videoContent接口根据ID生成视频
-      // 这里添加一个延迟,确保视频ID已经在后端系统中就绪
-      await new Promise(resolve => setTimeout(resolve, 1000))
-      
-      // 调用videoContent接口
-      const generateRes = await videoContent({ video_id: videoId })
-      
-      // 无论成功与否,都重新加载历史记录表格
-      await loadVideoHistory()
-      
-      if (generateRes.code === 0) {
-        // 如果视频已生成完成
-        if (generateRes.data && generateRes.data.web_url) {
-          ElMessage.success('视频生成成功')
-          
-          // 查找生成的视频项
-          const newVideoItem = historyList.value.find(item => item.video_id === videoId)
-          if (newVideoItem) {
-            // 自动打开视频播放对话框
-            playHistoryVideo(newVideoItem)
-          }
-        } else {
-          ElMessage.info('视频正在生成中,请稍后刷新查看')
-          
-          // 启动轮询检查进度
-          startCheckingProgress(videoId)
-        }
-      } else {
-        // 提取更详细的错误信息
-        let errorMsg = generateRes.msg || '未知错误'
-        if (generateRes.data && generateRes.data.error_message) {
-          try {
-            // 尝试解析JSON格式的错误信息
-            const errorData = JSON.parse(generateRes.data.error_message)
-            if (errorData.error && errorData.error.message) {
-              errorMsg += ' - ' + errorData.error.message
-            }
-          } catch (e) {
-            // 如果解析失败,直接使用原始错误信息
-            errorMsg += ' - ' + generateRes.data.error_message
-          }
-        }
-        
-        ElMessage.error('视频生成失败:' + errorMsg)
-      }
-    } else {
-      // 即使创建任务失败,也刷新表格数据
-      await loadVideoHistory()
-      
-      // 提取更详细的错误信息
-      let errorMsg = videoIdRes.msg || '未知错误'
-      if (videoIdRes.data && videoIdRes.data.error_message) {
-        try {
-          // 尝试解析JSON格式的错误信息
-          const errorData = JSON.parse(videoIdRes.data.error_message)
-          if (errorData.error && errorData.error.message) {
-            errorMsg += ' - ' + errorData.error.message
-          }
-        } catch (e) {
-          // 如果解析失败,直接使用原始错误信息
-          errorMsg += ' - ' + videoIdRes.data.error_message
-        }
-      }
-      
-      ElMessage.error('创建视频任务失败:' + errorMsg)
-    }
-  } catch (error) {
-    console.error('视频生成过程中发生错误', error)
-    // 发生异常也尝试刷新表格
-    try {
-      await loadVideoHistory()
-    } catch (e) {
-      console.error('刷新表格失败', e)
-    }
-    ElMessage.error('视频生成失败,请检查网络连接后重试')
-  } finally {
-    isGenerating.value = false
+  if (file.size / 1024 / 1024 > MAX_REF_IMAGE_MB) {
+    ElMessage.error(`图片大小不能超过 ${MAX_REF_IMAGE_MB}MB`)
+    return
   }
   }
+  if (framePreviewUrls[key]) URL.revokeObjectURL(framePreviewUrls[key])
+  frameFiles[key] = file
+  framePreviewUrls[key] = URL.createObjectURL(file)
+  form[getFrameField(key)] = ''
 }
 }
 
 
-// 轮询检查视频生成进度
-let checkInterval = null
-const startCheckingProgress = (videoId) => {
-  if (!videoId) return
-  
-  // 清除之前的轮询
-  if (checkInterval) {
-    clearInterval(checkInterval)
-  }
-  
-  // 最大检查次数,避免无限轮询
-  let checkCount = 0
-  const maxCheckCount = 20 // 最多检查20次(60秒)
-  
-  // 每3秒检查一次
-  checkInterval = setInterval(async () => {
-    checkCount++
-    
-    if (checkCount > maxCheckCount) {
-      ElMessage.info('视频生成可能需要更长时间,请手动刷新查看')
-      clearInterval(checkInterval)
-      return
-    }
-    
-    try {
-      // 使用videoContent接口检查视频状态
-      const statusRes = await videoContent({ video_id: videoId, action: 'status' })
-      
-      if (statusRes.code === 0 && statusRes.data?.status === 'completed' && statusRes.data?.web_url) {
-        // 视频生成完成
-        ElMessage.success('视频生成完成')
-        
-        // 重新加载历史记录
-        await loadVideoHistory()
-        
-        // 查找对应的视频记录
-        const videoItem = historyList.value.find(item => item.video_id === videoId)
-        if (videoItem) {
-          // 自动打开视频播放对话框
-          playHistoryVideo(videoItem)
+const clearFrameImage = (key, showMsg = true) => {
+  if (framePreviewUrls[key]) {
+    URL.revokeObjectURL(framePreviewUrls[key])
+    framePreviewUrls[key] = ''
+  }
+  frameFiles[key] = null
+  form[getFrameField(key)] = ''
+  if (showMsg) ElMessage.info(`已移除${getFrameLabel(key)}`)
+}
+
+const clearAllFrameImages = (showMsg = false) => {
+  FRAME_KEYS.forEach((key) => clearFrameImage(key, false))
+  if (showMsg) ElMessage.info('已移除首尾帧')
+}
+
+/** 压缩后转 data:image/xxx;base64,... */
+const fileToBase64Compressed = (file, maxSize = 1920, quality = 0.82) =>
+  new Promise((resolve, reject) => {
+    const url = URL.createObjectURL(file)
+    const img = new Image()
+    img.onload = () => {
+      URL.revokeObjectURL(url)
+      let w = img.naturalWidth
+      let h = img.naturalHeight
+      if (w > maxSize || h > maxSize) {
+        if (w > h) {
+          h = Math.round((h * maxSize) / w)
+          w = maxSize
+        } else {
+          w = Math.round((w * maxSize) / h)
+          h = maxSize
         }
         }
-        
-        clearInterval(checkInterval)
-      } else if (statusRes.code !== 0) {
-        console.error('检查视频状态失败', statusRes.msg)
       }
       }
-    } catch (error) {
-      console.error('检查视频进度失败', error)
+      const canvas = document.createElement('canvas')
+      canvas.width = w
+      canvas.height = h
+      const ctx = canvas.getContext('2d')
+      ctx.drawImage(img, 0, 0, w, h)
+      resolve(canvas.toDataURL('image/jpeg', quality))
     }
     }
-  }, 3000)
+    img.onerror = () => {
+      URL.revokeObjectURL(url)
+      reject(new Error('图片加载失败'))
+    }
+    img.src = url
+  })
+
+const setPreviewVideo = (row) => {
+  if (!row?.web_url) return
+  currentVideoItem.value = row
+  previewVideoUrl.value = formatVideoUrl(row.web_url, getHistoryRowAssetRef(row))
 }
 }
 
 
-// 重置表单
-const resetForm = () => {
-  form.prompt = ''
-  form.model = 'sora-2'
-  form.seconds = 4
-  form.size = '1280x720'
-  videoUrl.value = ''
+const openRecordPreview = (row) => {
+  if (!row?.web_url) {
+    ElMessage.warning('视频尚未生成完成')
+    return
+  }
+  recordPreviewItem.value = row
+  recordPreviewPlayUrl.value = formatVideoUrl(row.web_url, getHistoryRowAssetRef(row))
+  recordPreviewVisible.value = true
 }
 }
 
 
-// 下载视频
-const downloadVideo = () => {
-  if (!videoUrl.value) return
-  
-  // 创建下载链接
+const downloadRecordPreview = () => {
+  if (!recordPreviewPlayUrl.value) return
   const link = document.createElement('a')
   const link = document.createElement('a')
-  link.href = videoUrl.value
-  link.download = `video_${Date.now()}.mp4`
+  link.href = recordPreviewPlayUrl.value
+  link.download = `video_${recordPreviewItem.value?.video_id || Date.now()}.mp4`
   document.body.appendChild(link)
   document.body.appendChild(link)
   link.click()
   link.click()
   document.body.removeChild(link)
   document.body.removeChild(link)
-  
   ElMessage.success('视频下载中')
   ElMessage.success('视频下载中')
 }
 }
 
 
-// 下载历史视频
-const downloadHistoryVideo = (row) => {
-  if (!row.web_url) {
-    ElMessage.warning('视频尚未生成完成')
-    return
-  }
-  
-  const videoUrl = formatVideoUrl(row.web_url)
+const openRecordDialog = () => {
+  recordDialogVisible.value = true
+}
+
+const onRecordDialogOpen = () => {
+  loadVideoHistory()
+}
+
+const downloadCurrentVideo = () => {
+  if (!previewVideoUrl.value) return
   const link = document.createElement('a')
   const link = document.createElement('a')
-  link.href = videoUrl
-  link.download = `video_${row.video_id || Date.now()}.mp4`
+  link.href = previewVideoUrl.value
+  link.download = `video_${currentVideoItem.value?.video_id || Date.now()}.mp4`
   document.body.appendChild(link)
   document.body.appendChild(link)
   link.click()
   link.click()
   document.body.removeChild(link)
   document.body.removeChild(link)
-  
   ElMessage.success('视频下载中')
   ElMessage.success('视频下载中')
 }
 }
 
 
-// 下载对话框中的视频
-const downloadDialogVideo = () => {
-  if (!currentVideoItem.value || !currentVideoItem.value.web_url) return
-  
-  downloadHistoryVideo(currentVideoItem.value)
+const loadVideoHistory = async () => {
+  try {
+    const params = { page: page.value, limit: pageSize.value }
+    if (searchInfo.value.trim()) params.search = searchInfo.value.trim()
+    const res = await Getvideolist(params)
+    if (res.code === 0) {
+      historyList.value = Array.isArray(res.data?.list) ? res.data.list : res.data
+      total.value = res.count || 0
+    }
+  } catch (error) {
+    console.error('加载视频历史失败', error)
+    ElMessage.error('加载历史记录失败')
+  }
+}
+
+onMounted(() => {
+  loadModelList()
+})
+
+onBeforeUnmount(() => {
+  stopPollingVideoTask()
+  FRAME_KEYS.forEach((key) => {
+    if (framePreviewUrls[key]) URL.revokeObjectURL(framePreviewUrls[key])
+  })
+})
+
+/** 本页 AI 接口成功:code 为 0 或 1 */
+const isApiOk = (res) => res && (res.code === 0 || res.code === 1)
+
+const isVideoTaskDone = (data) => {
+  if (!data?.web_url) return false
+  const status = String(data.status || '').toLowerCase()
+  if (!status) return true
+  return status === 'succeeded' || status === 'completed' || status === 'success'
 }
 }
 
 
-// 重新生成
-const regenerateVideo = () => {
-  generateVideo()
+let pollVideoTimer = null
+let pollVideoInterval = null
+let pollVideoCount = 0
+const POLL_FIRST_DELAY_MS = 10000
+const POLL_INTERVAL_MS = 6000
+const POLL_MAX_COUNT = 80
+
+const stopPollingVideoTask = () => {
+  if (pollVideoTimer) {
+    clearTimeout(pollVideoTimer)
+    pollVideoTimer = null
+  }
+  if (pollVideoInterval) {
+    clearInterval(pollVideoInterval)
+    pollVideoInterval = null
+  }
+  pollVideoCount = 0
 }
 }
 
 
-// 播放历史视频
-const playHistoryVideo = (row) => {
-  if (!row.web_url) {
-    ElMessage.warning('视频尚未生成完成')
+const startPollingVideoTask = (taskId, meta = {}) => {
+  if (!taskId) return
+  stopPollingVideoTask()
+  isGenerating.value = true
+  pollVideoCount = 0
+
+  const finishPolling = (done) => {
+    stopPollingVideoTask()
+    isGenerating.value = false
+    if (!done) ElMessage.info('视频生成时间较长,可在「视频生成记录」中查看')
+  }
+
+  const handlePollResult = async (result) => {
+    if (result === 'pending') return
+    if (result === 'success') {
+      ElMessage.success('视频生成成功')
+      await loadVideoHistory()
+      finishPolling(true)
+      return
+    }
+    finishPolling(false)
+  }
+
+  const pollOnce = async () => {
+    pollVideoCount++
+    if (pollVideoCount > POLL_MAX_COUNT) return 'timeout'
+    try {
+      const res = await Get_ImgToVideo(
+        { task_id: taskId },
+        { donNotShowLoading: true, skipErrorMessage: true }
+      )
+      if (isApiOk(res) && isVideoTaskDone(res.data)) {
+        setPreviewVideo({
+          task_id: taskId,
+          video_id: res.data.video_id || taskId,
+          web_url: res.data.web_url,
+          prompt: meta.prompt ?? form.prompt,
+          model: meta.model ?? form.model,
+          seconds: meta.seconds ?? form.seconds,
+          size: meta.size ?? form.size
+        })
+        return 'success'
+      }
+    } catch (error) {
+      console.error('轮询视频状态失败', error)
+    }
+    return 'pending'
+  }
+
+  pollVideoTimer = setTimeout(async () => {
+    pollVideoTimer = null
+    const first = await pollOnce()
+    if (first !== 'pending') {
+      await handlePollResult(first)
+      return
+    }
+    pollVideoInterval = setInterval(async () => {
+      await handlePollResult(await pollOnce())
+    }, POLL_INTERVAL_MS)
+  }, POLL_FIRST_DELAY_MS)
+}
+
+const generateVideo = async () => {
+  if (!form.prompt?.trim() && !hasAnyFrameImage()) {
+    ElMessage.warning('请输入提示词或上传首帧/尾帧')
     return
     return
   }
   }
-  
-  // 设置当前视频项和URL
-  currentVideoItem.value = row
-  dialogVideoUrl.value = formatVideoUrl(row.web_url)
-  
-  console.log('正在播放视频:', dialogVideoUrl.value)
-  
-  // 显示对话框
-  dialogVisible.value = true
-  
-  // 填充表单
-  // form.prompt = row.prompt
-  // form.model = row.model
-  // form.seconds = row.seconds
-  // form.size = row.size
-}
-
-// 重新获取视频
-const refreshVideo = async (row) => {
-  if (!row.video_id) {
-    ElMessage.warning('视频ID不存在,无法重新获取')
+  if (!form.model) {
+    ElMessage.warning('请选择模型')
     return
     return
   }
   }
-    ElMessage.info('正在重新获取视频...')
-    // 调用videoContent接口重新获取视频
-    const res = await videoContent({ video_id: row.video_id })
-    // 无论获取结果如何,都刷新表格数据
-    await loadVideoHistory()
-    if (res.code === 0) {
-      // 如果获取到了视频URL
-      if (res.data && res.data.web_url) {
-        ElMessage.success('视频获取成功')
-        
-        // 查找更新后的视频项
-        const updatedVideo = historyList.value.find(item => item.video_id === row.video_id)
-        if (updatedVideo && updatedVideo.web_url) {
-          // 自动播放获取到的视频
-          playHistoryVideo(updatedVideo)
-        }
-      } else {
-        ElMessage.info('视频仍在生成中,请稍后再试')
-      }
+  stopPollingVideoTask()
+  isGenerating.value = true
+  let pollingStarted = false
+  try {
+    const firstFile = getFrameRawFile('first')
+    const lastFile = getFrameRawFile('last')
+    const first_image = firstFile ? await fileToBase64Compressed(firstFile) : ''
+    const last_image = lastFile ? await fileToBase64Compressed(lastFile) : ''
+    const params = {
+      prompt: (form.prompt || '').trim(),
+      model: form.model,
+      seconds: form.seconds,
+      size: form.size
+    }
+    if (first_image) params.first_image = first_image
+    if (last_image) params.last_image = last_image
+
+    const createRes = await Create_ImgToVideo(params, { skipErrorMessage: true })
+    const taskId = createRes?.data?.task_id || createRes?.data?.video_id
+    if (isApiOk(createRes) && taskId) {
+      ElMessage.success(createRes.msg || '任务创建成功,正在生成视频...')
+      pollingStarted = true
+      startPollingVideoTask(taskId, {
+        prompt: params.prompt,
+        model: params.model,
+        seconds: params.seconds,
+        size: params.size
+      })
+    } else {
+      ElMessage.error('创建视频任务失败:' + (createRes?.msg || '未知错误'))
     }
     }
+  } catch (error) {
+    console.error('视频生成过程中发生错误', error)
+    ElMessage.error('视频生成失败,请检查网络连接后重试')
+  } finally {
+    if (!pollingStarted) isGenerating.value = false
+  }
 }
 }
 
 
-// 搜索处理
-const handleSearch = () => {
-  // 重置页码为1
-  page.value = 1
-  // 重新加载数据
-  loadVideoHistory()
+const optimizePrompt = async () => {
+  if (!form.prompt?.trim()) {
+    ElMessage.warning('请先输入提示词')
+    return
+  }
+  isOptimizing.value = true
+  try {
+    const res = await CallAIModelApi(
+      {
+        status_val: '文生文',
+        status_type: 'ImgToVideo',
+        prompt: form.prompt,
+        model: 'gpt-4'
+      },
+      { donNotShowLoading: true }
+    )
+    const content = res?.data?.content ?? res?.content
+    if (res?.code === 0 && content) {
+      form.prompt = content
+      ElMessage.success('提示词优化完成')
+    } else {
+      ElMessage.error(res?.msg || '优化失败')
+    }
+  } catch (e) {
+    console.error('[优化提示词] 错误:', e)
+    ElMessage.error('优化失败: ' + (e?.message || '未知错误'))
+  } finally {
+    isOptimizing.value = false
+  }
+}
+
+const playHistoryVideo = (row) => {
+  openRecordPreview(row)
+}
+
+const refreshVideo = async (row) => {
+  const taskId = row.task_id || row.video_id
+  if (!taskId) {
+    ElMessage.warning('任务ID不存在,无法重新获取')
+    return
+  }
+  ElMessage.info('正在重新获取视频...')
+  const res = await Get_ImgToVideo({ task_id: taskId }, { skipErrorMessage: true })
+  await loadVideoHistory()
+  if (isApiOk(res) && isVideoTaskDone(res.data)) {
+    ElMessage.success(res.msg || '视频获取成功')
+    const updatedVideo = historyList.value.find(
+      (item) => (item.task_id || item.video_id) === taskId
+    )
+    if (updatedVideo) {
+      openRecordPreview(updatedVideo)
+    } else {
+      openRecordPreview({ ...row, web_url: res.data.web_url, task_id: taskId })
+    }
+  } else {
+    ElMessage.info(res?.msg || '视频仍在生成中,请稍后再试')
+  }
 }
 }
 
 
-// 分页大小变化处理
 const handleSizeChange = (size) => {
 const handleSizeChange = (size) => {
   pageSize.value = size
   pageSize.value = size
-  // 重新加载数据
   loadVideoHistory()
   loadVideoHistory()
 }
 }
 
 
-// 当前页码变化处理
 const handleCurrentChange = (current) => {
 const handleCurrentChange = (current) => {
   page.value = current
   page.value = current
-  // 重新加载数据
   loadVideoHistory()
   loadVideoHistory()
 }
 }
-
-// 删除历史视频
-const deleteHistoryVideo = async (id) => {
-  try {
-    // 这里应该调用实际的删除API
-    // const res = await deleteVideoApi({ id })
-    // if (res.code === 0) {
-      const index = historyList.value.findIndex(item => item.id === id)
-      if (index !== -1) {
-        historyList.value.splice(index, 1)
-        ElMessage.success('删除成功')
-      }
-    // }
-  } catch (error) {
-    console.error('删除视频失败', error)
-    ElMessage.error('删除失败,请重试')
-  }
-}
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
+.video-generation-page {
+  padding: 6px 10px;
+  height: calc(100vh - 200px);
+  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - 200px);
+  min-height: 480px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.two-column-layout {
+  display: flex;
+  gap: 10px;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
 
 
-::v-deep(.el-card__body){
-	padding: 0px;
+.left-column {
+  flex: 0 0 380px;
+  min-width: 280px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 }
-.video-generation-container {
-  padding: 0px;
-  
-  .card-header {
+
+.right-column {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.config-card,
+.result-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+
+  :deep(.el-card__body) {
+    padding: 0;
+    flex: 1;
+    min-height: 0;
     display: flex;
     display: flex;
-    justify-content: space-between;
-    align-items: center;
+    flex-direction: column;
+    overflow: hidden;
   }
   }
-  
-  .refresh-button {
-    margin-left: auto;
+}
+
+.config-card :deep(.el-card__header) {
+  display: none;
+}
+
+.result-card :deep(.el-card__header) {
+  padding: 10px 14px;
+  flex-shrink: 0;
+  background: linear-gradient(180deg, #fafbfc 0%, #f5f7fa 100%);
+  border-bottom: 1px solid #ebeef5;
+}
+
+.result-card :deep(.el-card__body) {
+  padding: 8px;
+}
+
+.result-card :deep(.record-btn-header.el-button) {
+  display: inline-flex;
+  align-items: center;
+  height: auto;
+  padding: 6px 14px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 8px;
+  color: #409eff !important;
+  background: #ecf5ff !important;
+  border: 1px solid #d9ecff !important;
+  box-shadow: none;
+
+  &:hover,
+  &:focus {
+    background: #409eff !important;
+    color: #fff !important;
+    border-color: #409eff !important;
   }
   }
 }
 }
 
 
-.el-form {
-  .el-form-item {
-    margin-bottom: 10px;
-    padding: 0px;
+.config-body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.config-form {
+  flex: 1;
+  min-height: 0;
+  padding: 6px 10px;
+  overflow-y: auto;
+  overflow-x: hidden;
+
+  :deep(.el-form-item) {
+    margin-bottom: 4px;
   }
   }
 }
 }
 
 
-.el-table {
-  .el-button {
-    margin-right: 5px;
+.upload-row-inner {
+  display: flex;
+  gap: 10px;
+  width: 100%;
+}
+
+.upload-cell {
+  flex: 1;
+  min-width: 0;
+  max-width: 140px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.upload-cell-inner {
+  position: relative;
+}
+
+.upload-label {
+  font-size: 12px;
+  color: #606266;
+
+  .optional {
+    color: #909399;
+    font-weight: normal;
   }
   }
 }
 }
 
 
-// 对话框视频样式
-.dialog-video-preview {
+.image-uploader.square {
+  width: 100%;
+
+  :deep(.el-upload) {
+    width: 100%;
+    display: block;
+  }
+
+  .upload-placeholder,
+  .uploaded-preview {
+    width: 100%;
+    aspect-ratio: 1;
+    height: auto !important;
+    min-height: 80px;
+    max-height: 130px;
+    max-width: 130px;
+    margin: 0 auto;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s;
+    background: #fafafa;
+    overflow: hidden;
+    box-sizing: border-box;
+
+    &:hover {
+      border-color: #409eff;
+      background: #f0f9ff;
+    }
+  }
+
+  .upload-placeholder {
+    color: #8c939d;
+
+    span {
+      font-size: 11px;
+      margin-top: 2px;
+    }
+  }
+
+  .uploaded-preview {
+    position: relative;
+
+    .preview-image {
+      width: 100%;
+      height: 100%;
+    }
+
+    .upload-mask {
+      position: absolute;
+      inset: 0;
+      background: rgba(0, 0, 0, 0.4);
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      color: #fff;
+      opacity: 0;
+      transition: opacity 0.2s;
+
+      span {
+        font-size: 11px;
+        margin-top: 2px;
+      }
+
+      .swap-icon {
+        width: 18px;
+        height: 18px;
+      }
+    }
+
+    &:hover .upload-mask {
+      opacity: 1;
+    }
+  }
+}
+
+.upload-remove-btn {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  z-index: 3;
+  width: 18px;
+  height: 18px;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.5);
+  color: #fff;
+  cursor: pointer;
   display: flex;
   display: flex;
+  align-items: center;
   justify-content: center;
   justify-content: center;
+  line-height: 1;
+
+  &:hover {
+    background: #f56c6c;
+  }
+}
+
+.image-error {
+  width: 100%;
+  height: 100%;
+  display: flex;
   align-items: center;
   align-items: center;
-  min-height: 400px;
-  padding: 20px 0;
-  
-  video {
-    max-height: 600px;
-    max-width: 100%;
-    border-radius: 4px;
-  }
-  
-  .loading-text {
-    font-size: 16px;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #909399;
+}
+
+.prompt-form-item {
+  margin-bottom: 4px;
+
+  :deep(.el-form-item__label) {
+    width: 100%;
+    display: flex;
+    justify-content: flex-start;
+    text-align: left;
+    padding-left: 0;
+    line-height: 1.4;
+  }
+}
+
+.prompt-block {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  width: 100%;
+}
+
+.prompt-wrapper {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  overflow: hidden;
+  background: #fff;
+
+  :deep(.el-textarea__inner) {
+    border: none;
+    border-radius: 0;
+    box-shadow: none;
+    padding-bottom: 28px;
+  }
+
+  :deep(.el-input__count) {
+    background: #fff;
+    padding-left: 6px;
+    font-size: 12px;
     color: #909399;
     color: #909399;
   }
   }
 }
 }
 
 
-// 最新生成结果样式
-.video-result-preview {
+.prompt-textarea {
+  width: 100%;
+
+  :deep(textarea) {
+    resize: none;
+    min-height: 100px;
+  }
+}
+
+.prompt-actions-row {
   display: flex;
   display: flex;
-  justify-content: space-between;
   align-items: center;
   align-items: center;
-  padding: 10px;
-  background-color: #fafafa;
-  border-radius: 4px;
-  
-  .video-result-info {
-    flex: 1;
-    p {
-      margin: 5px 0;
-      font-size: 14px;
+  justify-content: flex-end;
+  gap: 10px;
+  width: 100%;
+  padding: 8px 0;
+  background: transparent;
+}
+
+.config-row-compact {
+  margin-bottom: 0 !important;
+}
+
+.config-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  width: 100%;
+}
+
+.config-label {
+  font-size: 12px;
+  color: #606266;
+}
+
+.model-item {
+  min-width: 0;
+}
+
+.model-select {
+  width: 100%;
+
+  :deep(.el-select__wrapper) {
+    border-radius: 8px;
+    border: 1px solid #e4e7ed;
+    background: #fafbfc;
+    box-shadow: none;
+    min-height: 36px;
+    padding: 6px 14px;
+
+    &:hover {
+      border-color: #c0c4cc;
+      background: #fff;
     }
     }
   }
   }
-  
-  .video-result-actions {
-    display: flex;
-    gap: 10px;
+
+  :deep(.el-select__wrapper.is-focused) {
+    border-color: #409eff;
+    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+    background: #fff;
   }
   }
 }
 }
 
 
-// 响应式调整
-@media screen and (max-width: 768px) {
-  .dialog-video-preview {
-    min-height: 300px;
-    
-    video {
-      max-height: 400px;
-    }
+.size-item {
+  width: 100%;
+}
+
+.size-group-options {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 4px;
+}
+
+.size-chip {
+  padding: 2px 8px;
+  font-size: 12px;
+  border-radius: 4px;
+  cursor: pointer;
+  user-select: none;
+  background: #f5f7fa;
+  color: #606266;
+  border: 1px solid #e4e7ed;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: #409eff;
+    color: #409eff;
+  }
+
+  &.active {
+    background: #ecf5ff;
+    border-color: #409eff;
+    color: #409eff;
+    font-weight: 500;
+  }
+}
+
+.result-area {
+  flex: 1;
+  min-height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f9fafb;
+  border-radius: 6px;
+  padding: 12px;
+  overflow: hidden;
+}
+
+.result-preview {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+}
+
+.result-video {
+  width: 100%;
+  max-height: min(52vh, 480px);
+  border-radius: 8px;
+  background: #000;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
+}
+
+.result-meta {
+  width: 100%;
+  max-width: 720px;
+  text-align: center;
+  padding: 0 8px;
+}
+
+.result-meta-prompt {
+  margin: 0 0 4px;
+  font-size: 13px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.result-meta-info {
+  font-size: 12px;
+  color: #909399;
+}
+
+.result-actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+.result-empty {
+  text-align: center;
+  color: #909399;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+
+  .empty-icon {
+    color: #dcdfe6;
+  }
+
+  p {
+    margin: 0;
+    font-size: 14px;
+  }
+
+  span {
+    font-size: 12px;
+    color: #c0c4cc;
+  }
+}
+
+.generate-btn-center {
+  margin-top: 8px;
+}
+
+.result-loading {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  color: #606266;
+
+  .loading-icon {
+    color: #409eff;
+  }
+}
+
+.history-ref-thumb {
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  cursor: zoom-in;
+}
+
+.history-thumb-preview {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.history-video-cell {
+  position: relative;
+  display: inline-block;
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  overflow: hidden;
+  cursor: pointer;
+
+  .history-ref-thumb {
+    cursor: pointer;
   }
   }
-  
-  .video-result-preview {
+}
+
+.history-video-placeholder {
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  background: #f0f2f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #909399;
+}
+
+.history-video-play-badge {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.35);
+  color: #fff;
+  pointer-events: none;
+}
+
+.history-ref-empty {
+  color: #c0c4cc;
+  font-size: 12px;
+}
+
+.history-thumb-fallback {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f0f2f5;
+  color: #909399;
+  border-radius: 4px;
+}
+
+.history-frame-thumbs {
+  display: inline-flex;
+  gap: 4px;
+  justify-content: center;
+  flex-wrap: wrap;
+}
+
+.record-dialog-pagination {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.record-preview-body {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.record-preview-video {
+  width: 100%;
+  max-height: min(70vh, 520px);
+  border-radius: 6px;
+  background: #000;
+}
+
+.record-preview-meta {
+  .record-preview-prompt {
+    margin: 0 0 6px;
+    font-size: 13px;
+    color: #303133;
+    line-height: 1.5;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+  }
+
+  .record-preview-info {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+
+@media screen and (max-width: 992px) {
+  .two-column-layout {
     flex-direction: column;
     flex-direction: column;
-    align-items: flex-start;
-    gap: 15px;
-    
-    .video-result-actions {
-      width: 100%;
-      justify-content: flex-start;
-    }
   }
   }
+
+  .left-column {
+    flex: 0 0 auto;
+    max-height: 50vh;
+    min-width: 100%;
+  }
+
+  .right-column {
+    flex: 1;
+    min-height: 280px;
+  }
+}
+</style>
+
+<style lang="scss">
+/* 记录弹窗内图片放大:层级高于 dialog,点击遮罩空白处关闭 */
+.el-image-viewer__wrapper {
+  z-index: 3200 !important;
+}
+
+.el-image-viewer__mask {
+  cursor: zoom-out;
 }
 }
-</style>
+</style>