<template>
	<div :class="prefixCls">
		<section class="vg-chat-section">
			<div class="vg-chat-head">
				<h3 class="vg-chat-title">对话记录</h3>
				<button type="button" class="vg-task-refresh" @click="refreshChatList">刷新</button>
			</div>

			<p v-if="!sortedTaskRows.length" class="vg-task-empty">暂无对话，开始生成后将显示在此</p>

			<div
				v-else
				class="vg-chat-list"
				ref="chatListRef"
				@scroll="onChatScroll">
				<div
					v-for="row in sortedTaskRows"
					:key="row.id"
					class="vg-chat-block"
					:class="{ 'vg-chat-block--compact': !isChatRowSuccessWithMedia(row) }">
					<!-- 用户输入部分 -->
					<div
						class="vg-chat-user-section"
						:class="{ 'vg-chat-user-section--compact': !isChatRowSuccessWithMedia(row) }">
						<div v-if="isChatRowSuccessWithMedia(row)" class="vg-chat-user-block">
							<div class="vg-chat-user-text">
								<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
									<span v-if="seg.type === 'text'">{{ seg.text }}</span>
									<img
										v-else-if="seg.type === 'image'"
										class="vg-chat-inline-ref-image"
										:src="seg.url"
										:alt="seg.token || ''" />
									<video
										v-else-if="seg.type === 'video'"
										class="vg-chat-inline-ref-video"
										:src="seg.url"
										controls
										muted
										preload="metadata" />
									<audio
										v-else-if="seg.type === 'audio'"
										class="vg-chat-inline-audio"
										:src="seg.url"
										controls
										preload="metadata" />
								</template>
							</div>
							<div class="vg-chat-params" v-if="rowChatParamsVisible(row)">
								<span class="vg-chat-param-chip">{{ rowChatParams(row).modeLabel }}</span>
								<span class="vg-chat-param-chip">模型 {{ rowChatParams(row).modelLabel }}</span>
								<span class="vg-chat-param-chip">比例 {{ rowChatParams(row).ratio }}</span>
								<span class="vg-chat-param-chip">时长 {{ rowChatParams(row).duration }}</span>
								<span class="vg-chat-param-chip">分辨率 {{ rowChatParams(row).resolution }}</span>
							</div>
							<div class="vg-chat-task-actions">
								<button
									type="button"
									class="vg-link vg-chat-action-btn"
									:disabled="generateLoading"
									@click="editFromTaskRow(row)">
									重新编辑
								</button>
								<button
									type="button"
									class="vg-link vg-chat-action-btn"
									:disabled="generateLoading"
									@click="regenerateFromTaskRow(row)">
									重新生成
								</button>
								<button
									type="button"
									class="vg-link vg-chat-action-btn favorite-btn"
									:class="{ 'is-favorited': row.isTop === 'Y' }"
									@click="toggleFavorite(row)">
									<template v-if="row.isTop === 'Y'">
										★ 已收藏
									</template>
									<template v-else>
										☆ 收藏
									</template>
								</button>
							</div>
							<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
						</div>
						<div v-else class="vg-chat-user-row">
							<div class="vg-chat-user-col-main">
								<div class="vg-chat-user-text">
									<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
										<span v-if="seg.type === 'text'">{{ seg.text }}</span>
										<img
											v-else-if="seg.type === 'image'"
											class="vg-chat-inline-ref-image"
											:src="seg.url"
											:alt="seg.token || ''" />
										<video
											v-else-if="seg.type === 'video'"
											class="vg-chat-inline-ref-video"
											:src="seg.url"
											controls
											muted
											preload="metadata" />
										<audio
											v-else-if="seg.type === 'audio'"
											class="vg-chat-inline-audio"
											:src="seg.url"
											controls
											preload="metadata" />
									</template>
								</div>
								<div class="vg-chat-params" v-if="rowChatParamsVisible(row)">
									<span class="vg-chat-param-chip">{{ rowChatParams(row).modeLabel }}</span>
									<span class="vg-chat-param-chip">模型 {{ rowChatParams(row).modelLabel }}</span>
									<span class="vg-chat-param-chip">比例 {{ rowChatParams(row).ratio }}</span>
									<span class="vg-chat-param-chip">时长 {{ rowChatParams(row).duration }}</span>
									<span class="vg-chat-param-chip">分辨率 {{ rowChatParams(row).resolution }}</span>
								</div>
								<div class="vg-chat-task-actions">
									<button
										type="button"
										class="vg-link vg-chat-action-btn"
										:disabled="generateLoading"
										@click="editFromTaskRow(row)">
										重新编辑
									</button>
									<button
										type="button"
										class="vg-link vg-chat-action-btn"
										:disabled="generateLoading"
										@click="regenerateFromTaskRow(row)">
										重新生成
									</button>
								<button
									type="button"
									class="vg-link vg-chat-action-btn favorite-btn"
									:class="{ 'is-favorited': row.isTop === 'Y' }"
									@click="toggleFavorite(row)">
									<template v-if="row.isTop === 'Y'">
										★ 已收藏
									</template>
									<template v-else>
										☆ 收藏
									</template>
								</button>
								</div>
								<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
							</div>
							<div class="vg-chat-user-col-status">
								<span class="vg-chat-inline-status" :class="chatRowInlineStatusClass(row)">
									{{ taskStatusText(row) }}
								</span>
								<button
									type="button"
									class="vg-link vg-chat-inline-cancel"
									v-if="row.result && row.status === 0"
									@click="cancelRowTask(row)">
									取消
								</button>
							</div>
						</div>
					</div>

					<!-- 仅「已完成且可播放/下载」保留下方结果区 -->
					<template v-if="isChatRowSuccessWithMedia(row)">
						<div class="vg-chat-divider"></div>
						<div class="vg-chat-ai-section">
							<div class="vg-chat-ai-top">
								<div class="vg-chat-ai-status">
									{{ taskStatusText(row) }}
								</div>
								<button
									type="button"
									class="vg-chat-download-btn"
									@click="downloadChatVideo(row)"
									title="下载到本地">
									下载视频
								</button>
								<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
							</div>
							<div class="vg-chat-result">
								<video
									v-if="isVideoUrl(row.result)"
									:src="row.result"
									controls
									preload="metadata" />
								<div v-else class="vg-chat-result-link">
									<a :href="row.result" target="_blank" rel="noreferrer">查看结果</a>
								</div>
							</div>
						</div>
					</template>
				</div>
			</div>
			<div v-if="chatLoadingMore" class="vg-chat-loadmore">
				加载更多…
			</div>
		</section>

		<div class="vg-body">
			<!-- 中间：生成器 -->
			<div class="vg-generator">
				<div class="vg-generator-inner">
					<div class="vg-main-column">
						<VideoComposeCard
							ref="videoComposeRef"
							v-model="promptText"
							v-model:mediaList="mediaList"
							:max-media-count="maxMediaCount"
							:allowed-media-types="allowedMediaTypes"
							:placeholder="
								$t('common.textVideoPlaceholder') ||
								'描述画面与动态，例如：阳光下的女孩在海边起舞…'
							"
							:video-mode="videoMode">
							<template #reference-params>
								<div class="vg-params-row vg-params-row--reference-col">
									<div class="vg-param">
										<span class="vg-param-label">生成模式</span>
										<a-select
											v-model="videoMode"
											class="vg-param-select"
											placeholder="请选择模式"
											@change="pickVideoMode">
											<a-option v-for="opt in videoModeOptions" :key="opt.value" :value="opt.value">
												{{ opt.label }}
											</a-option>
										</a-select>
									</div>
									<div class="vg-param">
										<span class="vg-param-label">模型</span>
										<a-select
											v-model="selectedModel"
											class="vg-param-select"
											placeholder="请选择模型"
											allow-clear>
											<a-option v-for="opt in modelOptions" :key="opt.value" :value="opt.value">
												{{ opt.label }}
											</a-option>
										</a-select>
									</div>
									<div class="vg-param">
										<span class="vg-param-label">比例</span>
										<a-select v-model="selectedRatio" class="vg-param-select" placeholder="画幅比例" allow-clear>
											<a-option v-for="r in ratioOptions" :key="r" :value="r">{{ r }}</a-option>
										</a-select>
									</div>
									<div class="vg-param">
										<span class="vg-param-label">时长</span>
										<a-select v-model="selectedDuration" class="vg-param-select" placeholder="秒" allow-clear>
											<a-option v-for="d in durationOptions" :key="d" :value="d">{{ d }} 秒</a-option>
										</a-select>
									</div>
									<div class="vg-param">
										<span class="vg-param-label">分辨率</span>
										<a-select
											v-model="selectedResolution"
											class="vg-param-select"
											placeholder="分辨率"
											allow-clear>
											<a-option v-for="r in resolutionOptions" :key="r" :value="r">{{ r }}</a-option>
										</a-select>
									</div>
								</div>
							</template>
							<template #toolbar>
								<div class="vg-toolbar vg-toolbar--reference-submit">
									<div
										class="vg-toolbar-actions"
										:class="{ 'vg-toolbar-actions--reference': true }">
										<mf-button
											size="small"
											type="text"
											class="vg-toolbar-clear"
											@click="clearReferenceCompose">
											清空
										</mf-button>
										<button
											type="button"
											class="vg-submit-circle"
											:disabled="generateLoading"
											@click="generateVideo"
											:title="price ? $t('common.createVideo', { price }) : $t('common.generateVideo')">
											<span class="vg-submit-circle-inner">
												<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
													<path fill="currentColor" d="M12.002 3c.424 0 .806.177 1.079.46l5.98 5.98.103.114a1.5 1.5 0 0 1-2.225 2.006l-3.437-3.436V19.5l-.008.153a1.5 1.5 0 0 1-2.985 0l-.007-.153V8.122l-3.44 3.438a1.5 1.5 0 0 1-2.225-2.006l.103-.115 6-5.999.025-.025.059-.052.044-.037c.029-.023.06-.044.09-.065l.014-.01a1.43 1.43 0 0 1 .101-.062l.03-.017c.209-.11.447-.172.699-.172Z" />
												</svg>
											</span>
										</button>
									</div>
								</div>
							</template>
						</VideoComposeCard>

					</div>
				</div>
			</div>

		</div>
	</div>
</template>

<script>
import { mapGetters } from 'vuex'
import VideoComposeCard from '@/components/VideoComposeCard.vue'

const REFERENCE_DRAFT_STORAGE_KEY = 'portalVideoGen_referenceDraft_v1'

export default {
	name: 'VideoGen',
	data() {
		return {
			prefixCls: 'video-gen',
			videoMode: 'image-reference',
			videoModeOptions: [
				{ value: 'text-to-video', label: '文生视频' },
				{ value: 'image-first-frame', label: '图生视频 · 首帧' },
				{ value: 'image-first-last-frame', label: '图生视频 · 首尾帧' },
				{ value: 'image-reference', label: '图生视频 · 参考图' }
			],
			promptText: '',
			mediaList: [],
			editorVisualTick: 0,
			interval: null,
			videoUrl: null,
			videoLoading: false,
			generateLoading: false,
			videoId: null,
			showResult: false,
			price: null,
			id: null,
			modelOptions: [],
			ratioOptions: [],
			durationOptions: [],
			resolutionOptions: [],
			selectedModel: '',
			selectedRatio: '16:9',
			selectedDuration: null,
			selectedResolution: '',
			// 每 30 秒查询任务状态，最多 30 次（约 15 分钟）
			maxPollAttempts: 30,
			pollIntervalMs: 30000,
			taskRows: [],

			// 对话历史分页加载（仅用于对话记录区域）
			chatPageNum: 1,
			chatPageSize: 10,
			chatMaxTotal: 50,
			chatLoadingMore: false,
			chatHasMore: true,
			chatMaxHintShown: false
		}
	},
	components: {
		VideoComposeCard
	},
	computed: {
		canCancel() {
			return !!this.videoId && !this.videoUrl
		},
		maxMediaCount() {
			if (this.videoMode === 'image-first-frame') return 1
			if (this.videoMode === 'image-first-last-frame') return 2
			if (this.videoMode === 'image-reference') return 12
			return 12
		},
		allowedMediaTypes() {
			if (this.videoMode === 'image-first-frame' || this.videoMode === 'image-first-last-frame') return ['image']
			if (this.videoMode === 'image-reference') return ['image', 'video', 'audio']
			return ['image', 'video']
		},
		posterUrl() {
			const list = Array.isArray(this.mediaList) ? this.mediaList : []
			const firstImage = list.find((i) => i?.mediaType === 'image') || list[0]
			if (!firstImage?.url) return ''
			// 预览封面主要用于参考图/首帧类模式
			if (this.videoMode === 'image-reference') return firstImage.url
			if (this.videoMode === 'image-first-frame' || this.videoMode === 'image-first-last-frame') return firstImage.url
			return ''
		},
		sortedTaskRows() {
			const list = Array.isArray(this.taskRows) ? this.taskRows : []
			return [...list].sort((a, b) => {
				const at = this.parseDateTimeToMs(a?.createTime)
				const bt = this.parseDateTimeToMs(b?.createTime)
				// 升序：对话越往下越新（底部最新）
				if (at !== bt) return at - bt
				// createTime 相同：id 小的更旧，排前；id 大的更新，排后
				return Number(a?.id || 0) - Number(b?.id || 0)
			})
		},
		...mapGetters(['token'])
	},
	watch: {
		token(newVal, oldVal) {
			if (!newVal || newVal === oldVal) return
			// 登录成功（token 从无到有或切换账号）后补拉依赖鉴权的接口
			this.loadInitialData()
		}
	},
	mounted() {
		this.loadInitialData()
	},
	beforeUnmount() {
		this.destroyInterval()
	},
	methods: {
		loadInitialData() {
			this.loadPriceInfo()
			this.loadTaskList(true).then(() => {
				this.$nextTick(() => {
					this.scrollChatToBottom()
				})
			})
			this.loadVideoParams()
		},

		plainPromptText() {
			return this.$refs.videoRichEditor?.getPlainText?.() || ''
		},

		bumpEditorVisualTick() {
			this.editorVisualTick++
		},

		onVideoEditorTextChange() {
			this.bumpEditorVisualTick()
			if (this.videoMode === 'image-reference') {
				this.saveReferenceDraftFromEditor()
			}
		},

		saveReferenceDraftFromEditor() {
			const ed = this.$refs.videoRichEditor
			if (!ed || typeof ed.getDraftState !== 'function') return
			try {
				const draft = ed.getDraftState()
				localStorage.setItem(REFERENCE_DRAFT_STORAGE_KEY, JSON.stringify(draft))
			} catch (_) {
				/* ignore quota / private mode */
			}
		},

		restoreReferenceDraftToEditor() {
			const ed = this.$refs.videoRichEditor
			if (!ed || typeof ed.applyDraftState !== 'function') return
			try {
				const raw = localStorage.getItem(REFERENCE_DRAFT_STORAGE_KEY)
				if (!raw) {
					ed.clear?.()
					return
				}
				const draft = JSON.parse(raw)
				ed.applyDraftState(draft)
			} catch (_) {
				ed.clear?.()
			}
		},

		/** 编辑器内插入的参考图 URL（与 getContentItems 一致） */
		firstReferenceImageUrlFromEditor() {
			const items = this.$refs.videoRichEditor?.getContentItems?.() || []
			for (const i of items) {
				if (i.type === 'image_url' && i.image_url?.url) {
					if (!i.role || i.role === 'reference_image') return i.image_url.url
				}
			}
			return ''
		},

		pickVideoMode(m) {
			this.videoMode = m
			// 切换生成模式后清空参考素材，避免不同后端入参结构混用
			this.mediaList = []
		},

		async loadTaskList(reset = false) {
			try {
				if (reset) {
					this.chatPageNum = 1
					this.chatHasMore = true
					this.chatMaxHintShown = false
					this.chatLoadingMore = false
					this.taskRows = []
				}

				if (!this.chatHasMore || this.chatLoadingMore) return

				const pageNum = this.chatPageNum
				const pageSize = this.chatPageSize
				this.chatLoadingMore = true

				const res = await this.$axios({
					url: 'api/portal/video/tasks',
					method: 'GET',
					params: { pageNum, pageSize }
				})

				const newRows = res.code === 200 ? res.rows || [] : []

				// 合并去重（按 id）
				const existMap = new Map((this.taskRows || []).map((x) => [x.id, x]))
				for (const row of newRows) {
					if (!row || row.id == null) continue
					if (existMap.has(row.id)) {
						existMap.set(row.id, { ...existMap.get(row.id), ...row })
					} else {
						existMap.set(row.id, row)
					}
				}

				const merged = Array.from(existMap.values())
				// 始终保留“最新的最多 50 条”，避免向上翻页时截断造成顺序/条数错乱
				this.taskRows = merged
					.sort(
						(a, b) =>
							(this.parseDateTimeToMs(b?.createTime) -
								this.parseDateTimeToMs(a?.createTime)) ||
							(Number(b?.id || 0) - Number(a?.id || 0))
					)
					.slice(0, this.chatMaxTotal)

				// 更新分页状态
				if (this.taskRows.length >= this.chatMaxTotal) {
					this.chatHasMore = false
				} else {
					if (newRows.length < pageSize) this.chatHasMore = false
					this.chatPageNum = pageNum + 1
				}
			} catch (_) {
				/* 未登录等忽略 */
			} finally {
				this.chatLoadingMore = false
			}
		},

		async onChatScroll(e) {
			const el = e?.target
			if (!el) return
			if (this.chatLoadingMore) return
			// 向上滚动加载更早历史：滚到顶部附近（60px 内）触发下一页
			if (el.scrollTop > 60) return

			if (this.taskRows.length >= this.chatMaxTotal) {
				if (!this.chatMaxHintShown) {
					this.chatMaxHintShown = true
					this.$message.warning(`最多加载 ${this.chatMaxTotal} 条历史数据`)
				}
				this.chatHasMore = false
				return
			}
			// 记录旧高度，避免加载更多后用户位置跳动
			const prevScrollHeight = el.scrollHeight
			const prevScrollTop = el.scrollTop
			await this.loadTaskList(false)
			this.$nextTick(() => {
				const newScrollHeight = el.scrollHeight
				const diff = newScrollHeight - prevScrollHeight
				// 保持相对视口位置：滚动高度增加多少，就把 scrollTop 增加多少
				el.scrollTop = diff >= 0 ? prevScrollTop + diff : prevScrollTop
			})
		},

		// 轮询成功/失败/取消后，只刷新第一页数据，避免把用户已滚动加载的历史重置回 10 条
		async refreshChatFirstPage() {
			try {
				const res = await this.$axios({
					url: 'api/portal/video/tasks',
					method: 'GET',
					params: { pageNum: 1, pageSize: this.chatPageSize }
				})
				const newRows = res.code === 200 ? res.rows || [] : []
				const existMap = new Map((this.taskRows || []).map((x) => [x.id, x]))
				for (const row of newRows) {
					if (!row || row.id == null) continue
					if (existMap.has(row.id)) {
						existMap.set(row.id, { ...existMap.get(row.id), ...row })
					} else {
						existMap.set(row.id, row)
					}
				}
				this.taskRows = Array.from(existMap.values())
					.sort(
						(a, b) =>
							(this.parseDateTimeToMs(b?.createTime) -
								this.parseDateTimeToMs(a?.createTime)) ||
							(Number(b?.id || 0) - Number(a?.id || 0))
					)
					.slice(0, this.chatMaxTotal)
			} catch (_) {
				/* ignore */
			}
		},

		async refreshChatList() {
			await this.loadTaskList(true)
			this.$nextTick(() => {
				this.scrollChatToBottom()
			})
		},

		scrollChatToBottom() {
			const el = this.$refs.chatListRef
			if (!el) return
			el.scrollTop = el.scrollHeight
		},

		taskRowResultTrim(row) {
			let r = String(row?.result ?? '').trim()
			if (r) return r
			const raw = row?.videoParams
			if (raw == null || raw === '') return ''
			if (typeof raw !== 'string') return ''
			try {
				const o = JSON.parse(raw)
				const id = o?.volcTaskId
				if (id != null && String(id).trim() !== '') return String(id).trim()
			} catch (_) {
				/* ignore */
			}
			return ''
		},

		taskStatusText(row) {
			const st = row?.status
			if (st === 2 || st === '2') return '已失败/已取消'
			if (st === 0 || st === '0') {
				const extStatus = row?.extStatus
				if (extStatus === 0) {
					return '队列中'
				}
				return '执行中'
			}
			if (st === 1 || st === '1') {
				const r = this.taskRowResultTrim(row)
				if (!r) return '失败'
				if (this.isHttpOrHttpsUrl(r)) {
					return '已完成'
				}
				// 任务 id 格式由火山侧决定，不再假定 cgt 前缀；非 URL 的中间态均视为执行中
				const extStatus = row?.extStatus
				if (extStatus === 0) {
					return '队列中'
				}
				return '任务执行中'
			}
			const extStatus = row?.extStatus
			if (extStatus === 0) {
				return '队列中'
			}
			return '执行中'
		},

		/** 是否展示完整「结果区」（视频 / 链接）；其余状态仅紧凑展示在用户行右侧 */
		isChatRowSuccessWithMedia(row) {
			const r = this.taskRowResultTrim(row)
			const st = row?.status
			return (st === 1 || st === '1') && !!r && this.isHttpOrHttpsUrl(r)
		},

		chatRowInlineStatusClass(row) {
			const st = row?.status
			if (st === 2 || st === '2') return 'vg-chat-inline-status--failed'
			if (st === 1 || st === '1') {
				const r = this.taskRowResultTrim(row)
				if (!r) return 'vg-chat-inline-status--failed'
				return 'vg-chat-inline-status--running'
			}
			return 'vg-chat-inline-status--running'
		},

		parseDateTimeToMs(value) {
			if (!value) return 0
			const ms = new Date(value).getTime()
			return Number.isFinite(ms) ? ms : 0
		},

		formatCreateTime(value) {
			const ms = this.parseDateTimeToMs(value)
			if (!ms) return ''
			return new Date(ms).toLocaleString()
		},

		/** 对话结果是否为可访问的 http(s) 链接（任务 id 等非 URL 不视为已完成） */
		isHttpOrHttpsUrl(value) {
			const u = String(value || '').trim()
			return /^https?:\/\//i.test(u)
		},

		isVideoUrl(url) {
			const u = String(url || '')
			if (!u) return false
			return /\.(mp4|webm|mov|ogg|m4v|avi|mkv)(\?.*)?$/i.test(u)
		},

		getRowAttachmentList(row) {
			const list = []
			if (row?.img1) list.push(row.img1)
			if (row?.img2 && row.img2 !== row.img1) list.push(row.img2)

			// text-to-video：附件 url 可能落在 videoParams.content 里
			if (!list.length && row?.videoParams) {
				try {
					const vp = typeof row.videoParams === 'string' ? JSON.parse(row.videoParams) : row.videoParams
					const content = vp?.content
					if (Array.isArray(content)) {
						for (const item of content) {
							const url = item?.image_url?.url
							if (url) list.push(url)
						}
					}
				} catch (_) {
					// ignore parse error
				}
			}

			return list
				.filter((url) => !!url)
				.map((url) => ({
					url,
					mediaType: this.isVideoUrl(url) ? 'video' : 'image'
				}))
		},

		getRowReferenceMediaByType(row) {
			const images = []
			const videos = []
			const audios = []
			if (!row?.videoParams) return { images, videos, audios }
			try {
				const vp = typeof row.videoParams === 'string' ? JSON.parse(row.videoParams) : row.videoParams
				const content = Array.isArray(vp?.content) ? vp.content : []
				for (const item of content) {
					if (item?.type === 'image_url' && (!item.role || item.role === 'reference_image')) {
						const url = item?.image_url?.url
						if (url) images.push(url)
					}
					if (item?.type === 'video_url' && item?.role === 'reference_video') {
						const url = item?.video_url?.url
						if (url) videos.push(url)
					}
					if (item?.type === 'audio_url' && item?.role === 'reference_audio') {
						const url = item?.audio_url?.url
						if (url) audios.push(url)
					}
				}
			} catch (_) {
				// ignore parse error
			}
			return { images, videos, audios }
		},

		getRowPromptSegments(row) {
			const rawText = row?.text || '—'
			const text = String(rawText)
			const mode = String(row?.mode || '').trim()
			if (mode === 'image-first-frame') {
				const segs = []
				if (row?.img1) segs.push({ type: 'image', url: row.img1, token: '[首帧]' })
				segs.push({ type: 'text', text })
				return segs
			}
			if (mode === 'image-first-last-frame') {
				const segs = []
				if (row?.img1) segs.push({ type: 'image', url: row.img1, token: '[首帧]' })
				segs.push({ type: 'text', text })
				if (row?.img2) segs.push({ type: 'image', url: row.img2, token: '[尾帧]' })
				return segs
			}

			// 参考素材：[图片n][视频n][音频n] 与 content 中同类序号对应；兼容旧 [图n]
			const refs = this.getRowReferenceMediaByType(row)
			const hasRefPayload =
				refs.images.length > 0 || refs.videos.length > 0 || refs.audios.length > 0
			const hasRefTokens = /\[图片\d+\]|\[视频\d+\]|\[音频\d+\]|\[图\d+\]/.test(text)
			if (!hasRefPayload && !hasRefTokens) {
				return [{ type: 'text', text }]
			}
			const tokenReg = /(\[图片(\d+)\]|\[视频(\d+)\]|\[音频(\d+)\]|\[图(\d+)\])/g
			const segments = []
			let last = 0
			let m
			while ((m = tokenReg.exec(text)) !== null) {
				const token = m[0]
				const start = m.index
				if (start > last) {
					segments.push({ type: 'text', text: text.slice(last, start) })
				}
				let url = ''
				let segType = 'image'
				if (m[2] != null) {
					url = refs.images[Number(m[2]) - 1] || ''
					segType = 'image'
				} else if (m[3] != null) {
					url = refs.videos[Number(m[3]) - 1] || ''
					segType = 'video'
				} else if (m[4] != null) {
					url = refs.audios[Number(m[4]) - 1] || ''
					segType = 'audio'
				} else if (m[5] != null) {
					url = refs.images[Number(m[5]) - 1] || ''
					segType = 'image'
				}
				if (url && /^asset:\/\//i.test(url)) {
					segments.push({ type: 'text', text: token })
				} else if (url) {
					segments.push({ type: segType, url, token })
				} else {
					segments.push({ type: 'text', text: token })
				}
				last = start + token.length
			}
			if (last < text.length) {
				segments.push({ type: 'text', text: text.slice(last) })
			}
			return segments.length ? segments : [{ type: 'text', text }]
		},

		async cancelRowTask(row) {
			const tid = row.result
			if (!tid) return
			await this.cancelTaskById(tid)
			await this.refreshChatFirstPage()
		},

		parseVideoParams(row) {
			if (!row?.videoParams) return null
			try {
				return typeof row.videoParams === 'string' ? JSON.parse(row.videoParams) : row.videoParams
			} catch (_) {
				return null
			}
		},

		videoModeLabel(mode) {
			const m = String(mode || '').trim()
			if (!m) return '—'
			const hit = this.videoModeOptions.find((o) => o.value === m)
			return hit ? hit.label : m
		},

		modelLabelForValue(val) {
			if (!val) return ''
			const hit = this.modelOptions.find((o) => o.value === val)
			return hit ? hit.label : ''
		},

		rowChatParams(row) {
			const vp = this.parseVideoParams(row)
			const modelVal = row?.model ?? vp?.model ?? ''
			const ratio = row?.ratio ?? vp?.ratio ?? ''
			const durRaw = row?.duration != null ? row.duration : vp?.duration
			const resolution = row?.resolution ?? vp?.resolution ?? ''
			const mode = row?.mode || vp?.generationMode || ''
			return {
				modeLabel: this.videoModeLabel(mode),
				modelLabel: this.modelLabelForValue(modelVal) || modelVal || '—',
				ratio: ratio || '—',
				duration: durRaw != null && durRaw !== '' ? `${durRaw} 秒` : '—',
				resolution: resolution || '—'
			}
		},

		rowChatParamsVisible(row) {
			return !!(
				row &&
				(row.mode ||
					row.model ||
					row.ratio ||
					row.duration != null ||
					row.resolution ||
					row.videoParams)
			)
		},

		getRowPromptTextForRestore(row) {
			if (row?.text) return row.text
			const vp = this.parseVideoParams(row)
			const c = vp?.content?.[0]
			if (c?.type === 'text' && c.text) return c.text
			return ''
		},

		buildMediaListForTextToVideo(row) {
			const out = []
			try {
				const vp = this.parseVideoParams(row)
				const content = vp?.content
				if (Array.isArray(content)) {
					for (const item of content) {
						if (item?.type === 'image_url' && item.image_url?.url) {
							out.push({
								id: `hist_${out.length}_${Date.now()}`,
								url: item.image_url.url,
								mediaType: 'image',
								name: ''
							})
						}
					}
				}
			} catch (_) {}
			if (!out.length) {
				return this.getRowAttachmentList(row).map((x, i) => ({
					id: `att_${row.id || ''}_${i}`,
					url: x.url,
					mediaType: x.mediaType || 'image',
					name: ''
				}))
			}
			return out
		},

		async applyTaskRowToForm(row) {
			if (!row) return
			const mode = row.mode || 'text-to-video'
			if (row.model && this.modelOptions.some((o) => o.value === row.model)) {
				this.selectedModel = row.model
			}
			if (row.ratio && this.ratioOptions.includes(row.ratio)) {
				this.selectedRatio = row.ratio
			}
			const dur = row.duration != null ? Number(row.duration) : null
			if (dur != null && this.durationOptions.includes(dur)) {
				this.selectedDuration = dur
			}
			if (row.resolution && this.resolutionOptions.includes(row.resolution)) {
				this.selectedResolution = row.resolution
			}
			if (mode === 'image-reference') {
				try {
					localStorage.removeItem('video_reference_media')
				} catch (_) {}
			}
			this.videoMode = mode
			const prompt = this.getRowPromptTextForRestore(row)
			if (mode !== 'image-reference') {
				this.promptText = prompt
			} else {
				this.promptText = ''
			}
			await this.$nextTick()
			if (mode === 'text-to-video') {
				this.mediaList = this.buildMediaListForTextToVideo(row)
			} else if (mode === 'image-first-frame') {
				this.mediaList = row.img1
					? [{ id: 'r1', url: row.img1, mediaType: 'image', name: '' }]
					: []
			} else if (mode === 'image-first-last-frame') {
				const list = []
				if (row.img1) list.push({ id: 'r1', url: row.img1, mediaType: 'image', name: '' })
				if (row.img2) list.push({ id: 'r2', url: row.img2, mediaType: 'image', name: '' })
				this.mediaList = list
			} else if (mode === 'image-reference') {
				this.mediaList = []
				await this.$nextTick()
				await this.$refs.videoComposeRef?.loadReferenceFromTaskRow?.(row)
			}
		},

		async editFromTaskRow(row) {
			try {
				await this.applyTaskRowToForm(row)
				this.$message.success('已载入该条任务到编辑区，可修改后再次生成')
				this.$nextTick(() => {
					const el = this.$refs.videoComposeRef?.$el
					if (el && typeof el.scrollIntoView === 'function') {
						el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
					}
				})
			} catch (_) {
				this.$message.error('载入失败，请重试')
			}
		},

		regenerateFromTaskRow(row) {
			this.$confirm({
				title: '重新生成',
				content: '将按该条记录的提示词与参数再次提交任务，是否继续？',
				okText: '确定',
				cancelText: '取消',
				onOk: async () => {
					await this.applyTaskRowToForm(row)
					await this.$nextTick()
					await this.generateVideo()
				}
			})
		},

		downloadChatVideo(row) {
			const url = row?.result
			if (!url || !this.isHttpOrHttpsUrl(url)) return
			const a = document.createElement('a')
			a.href = url
			a.download = `video_${row.id || Date.now()}.mp4`
			a.target = '_blank'
			a.rel = 'noreferrer'
			document.body.appendChild(a)
			a.click()
			document.body.removeChild(a)
		},

		async loadPriceInfo() {
			this.$axios({
				url: 'api/manager/selectInfo',
				method: 'GET',
				data: { aiType: '21' }
			}).then((res) => {
				this.price = res.data?.price
				this.id = res.data?.id
			})
		},

		async loadVideoParams() {
			try {
				const res = await this.$axios({
					url: 'api/portal/video/options',
					method: 'GET'
				})
				if (res.code !== 200 || !res.data) return
				const { defaults, models, ratios, durations, resolutions } = res.data
				this.modelOptions = Array.isArray(models) ? models : []
				this.ratioOptions = Array.isArray(ratios) ? ratios : []
				this.durationOptions = Array.isArray(durations) ? durations.map((n) => Number(n)) : []
				this.resolutionOptions = Array.isArray(resolutions) ? resolutions : []
				const d = defaults || {}
				if (d.model) {
					this.selectedModel = d.model
				} else if (this.modelOptions.length) {
					this.selectedModel = this.modelOptions[0].value
				}
				if (this.ratioOptions.includes('16:9')) {
					this.selectedRatio = '16:9'
				} else if (d.ratio && this.ratioOptions.includes(d.ratio)) {
					this.selectedRatio = d.ratio
				} else if (this.ratioOptions.length) {
					this.selectedRatio = this.ratioOptions[0]
				}
				const durNum = d.duration != null ? Number(d.duration) : null
				if (durNum != null && this.durationOptions.includes(durNum)) {
					this.selectedDuration = durNum
				} else if (this.durationOptions.length) {
					this.selectedDuration = this.durationOptions[0]
				}
				if (d.resolution && this.resolutionOptions.includes(d.resolution)) {
					this.selectedResolution = d.resolution
				} else if (this.resolutionOptions.length) {
					this.selectedResolution = this.resolutionOptions[0]
				}
			} catch (_) {
				this.$message?.warning?.('加载视频参数配置失败')
			}
		},

		clearReferenceCompose() {
			this.$refs.videoComposeRef?.clearAll?.()
		},

		async generateVideo() {
			if (this.generateLoading) return

			const prompt = (this.promptText || '').trim()
			const text = prompt || '一个优雅的女孩在阳光下跳舞'
			const list = Array.isArray(this.mediaList) ? this.mediaList : []
			if (list.some((x) => x && x.isUploading)) {
				this.$message.warning('素材正在上传中，请稍后再生成')
				return
			}
			const attachments = list.filter((x) => x && x.url)

			if (!this.selectedModel || !this.selectedRatio || this.selectedDuration == null || !this.selectedResolution) {
				this.$message.error('请选择模型、比例、时长与分辨率')
				return
			}

			// 组装入参：只依赖 promptText + mediaList
			const params = {
				text,
				functionType: '21',
				model: this.selectedModel,
				ratio: this.selectedRatio,
				duration: this.selectedDuration,
				resolution: this.selectedResolution
			}

			if (this.videoMode === 'text-to-video') {
				if (!prompt && attachments.length === 0) {
					this.$message.error('请输入视频描述或添加参考素材')
					return
				}
				const contentItems = []
				if (prompt) {
					contentItems.push({
						type: 'text',
						text: prompt
					})
				}
				if (attachments.length) {
					contentItems.push(
						...attachments.map((item) => ({
							type: 'image_url',
							image_url: { url: item.url },
							role: 'reference_image'
						}))
					)
				}
				if (contentItems.length) {
					params.content = contentItems
				}
			}

			if (this.videoMode === 'image-first-frame') {
				const first = attachments[0]
				if (!first) {
					this.$message.error(this.$t('common.uploadFirstImageError') || '请上传首帧图')
					return
				}
				if (first.mediaType !== 'image') {
					this.$message.error('首帧模式仅支持图片素材')
					return
				}
				params.firstUrl = first.url
			}

			if (this.videoMode === 'image-first-last-frame') {
				const first = attachments[0]
				const last = attachments[1]
				if (!first) {
					this.$message.error(this.$t('common.uploadFirstImageError') || '请上传首帧图')
					return
				}
				if (!last) {
					this.$message.error('请上传尾帧图')
					return
				}
				if (first.mediaType !== 'image' || last.mediaType !== 'image') {
					this.$message.error('首尾帧模式仅支持图片素材')
					return
				}
				params.firstUrl = first.url
				params.lastUrl = last.url
			}

			if (this.videoMode === 'image-reference') {
				const compose = this.$refs.videoComposeRef
				const contentItems =
					compose && typeof compose.getImageReferenceContentItems === 'function'
						? compose.getImageReferenceContentItems()
						: []
				const first = contentItems[0]
				if (!first || first.type !== 'text') {
					this.$message.error('参考图内容格式异常，请重试')
					return
				}
				params.text = first.text || text
				params.content = contentItems
				// 后端要求：reference_url 需要使用资产标识（asset://assetId）时优先传 asset://
				// 展示用仍然可以是 https url，但提交 content/reference_url 要与 assetId 关联。
				const firstPreview = attachments.find((x) => x?.mediaType === 'image')
				if (firstPreview) {
					const au = String(firstPreview?.assetUrl || '').trim()
					const aid = String(firstPreview?.assetId || '').trim()
					params.referenceUrl = au || (aid ? `asset://${aid}` : firstPreview.url)
				}
			}

			const urlMap = {
				'text-to-video': 'api/portal/video/text-to-video',
				'image-first-frame': 'api/portal/video/image-first-frame',
				'image-first-last-frame': 'api/portal/video/image-first-last-frame',
				'image-reference': 'api/portal/video/image-reference'
			}
			const path = urlMap[this.videoMode]

			this.generateLoading = true
			try {
				const res = await this.$axios({
					url: path,
					method: 'POST',
					data: params
				})
				if (res.code == 200) {
					this.$message.success('已经提交任务，请等待或者稍后到个人中心查看作品')
					this.videoId = res.data.id
					this.showResult = true
					this.$refs.videoComposeRef?.clearPromptOnly?.()
					this.getVideo(res.data.id)
					this.refreshChatFirstPage()
				} else if (res.code == -1) {
					this.$confirm({
						title: this.$t('common.notice'),
						content: this.$t('common.balenceLow'),
						onOk: () => this.$router.push('/recharge')
					})
				} else {
					const reason = res.msg || res.message || '提交失败'
					this.$message.error(`${reason}，如需帮助请联系管理员`)
				}
			} catch (err) {
				const tip =
					typeof err === 'string' && err
						? err
						: err?.message || err?.response?.data?.msg || '网络或服务异常，请稍后重试'
				this.$message.error(`${tip}，如需帮助请联系管理员`)
			} finally {
				this.generateLoading = false
			}
		},

		getVideo(videoId) {
			let attempts = 0
			this.interval = setInterval(() => {
				attempts++
				if (attempts > this.maxPollAttempts) {
					this.$message.warning(
						'等待结果超时：任务可能仍在云端生成，可稍后在「对话记录」刷新或向客服提供任务 ID'
					)
					this.destroyInterval()
					return
				}

				this.$axios({
					url: `api/portal/video/tasks/${videoId}`,
					method: 'GET'
				}).then((res) => {
					if (res.code != 200) {
						this.destroyInterval()
						this.$message.error(res.msg || '查询生成状态失败')
						return
					}
					const st = res.data != null && res.data.status != null ? String(res.data.status).toLowerCase().trim() : ''
					if (st === 'succeeded') {
						this.videoUrl = res.data.content?.video_url || res.data.video_url
						if (!this.videoUrl) {
							this.$message.warning('任务已完成但未返回视频地址，请稍后重试或联系管理员')
						}
						this.destroyInterval()
						this.refreshChatFirstPage()
						return
					}
					if (st === 'failed' || st === 'cancelled' || st === 'canceled') {
						this.destroyInterval()
						this.$message.error(
							res.data.error?.message || res.data.message || '视频生成失败或已取消'
						)
						this.refreshChatFirstPage()
					}
				}).catch(() => {
					/* 单次网络错误不停止轮询，避免偶发抖动 */
				})
			}, this.pollIntervalMs)
		},

		destroyInterval() {
			if (this.interval) {
				clearInterval(this.interval)
				this.interval = null
			}
		},

		saveVideo() {
			if (!this.videoUrl) return
			const link = document.createElement('a')
			link.href = this.videoUrl
			link.download = `video_${Date.now()}.mp4`
			document.body.appendChild(link)
			link.click()
			document.body.removeChild(link)
		},

		closeResult() {
			this.showResult = false
			this.videoUrl = null
			this.videoId = null
			this.taskStatus = null
		},

		async cancelTask() {
			if (!this.videoId) return
			await this.cancelTaskById(this.videoId)
		},

		async cancelTaskById(taskId) {
			return new Promise((resolve) => {
				this.$confirm({
					title: '取消任务',
					content: '确定要取消当前视频生成任务吗？取消后余额将退回。',
					okText: '确定取消',
					cancelText: '关闭',
					onOk: async () => {
						try {
							const res = await this.$axios({
								url: `api/portal/video/tasks/${taskId}`,
								method: 'DELETE'
							})

							if (res.code === 200) {
								this.$message.success(res.msg || '任务已取消，余额已退回')
								this.destroyInterval()
								this.showResult = false
								this.videoUrl = null
								this.videoId = null
								this.refreshChatFirstPage()
							} else {
								this.$message.error(res.msg || '取消失败')
							}
						} catch (error) {
							this.$message.error('取消请求失败')
						}
						resolve()
					},
					onCancel: () => resolve()
				})
			})
		},

		// 收藏/取消收藏 - 复用生成库接口
		async toggleFavorite(row) {
			if (!row || !row.id) {
				this.$message.error('无效的任务记录')
				return
			}

			const newIsTop = row.isTop === 'Y' ? 'N' : 'Y'

			try {
				const res = await this.$axios({
					url: '/api/portal/assets/favorite',
					method: 'POST',
					data: {
						id: row.id,
						isTop: newIsTop
					}
				})

				if (res.code === 200) {
					// 更新本地数据
					row.isTop = newIsTop
					this.$message.success(newIsTop === 'Y' ? '收藏成功' : '已取消收藏')
				} else {
					this.$message.error(res.msg || '操作失败')
				}
			} catch (err) {
				this.$message.error(err?.message || '收藏操作失败')
			}
		}
	}
}
</script>

<style lang="less" scoped>
.video-gen {
	--vg-cyan: #00cae0;
	--vg-ink: #0a0b0d;
	--vg-panel: rgba(22, 24, 30, 0.92);
	--vg-border: rgba(255, 255, 255, 0.08);
	--vg-text: rgba(255, 255, 255, 0.88);
	--vg-muted: rgba(255, 255, 255, 0.45);

	display: flex;
	flex-direction: column;
	min-height: 100%;
	height: 100%;
	gap: 14px;
	padding: 20px 20px 16px;
	box-sizing: border-box;
	overflow-y: hidden;
	overflow-x: hidden;
	background: radial-gradient(120% 80% at 50% -20%, rgba(0, 202, 224, 0.12), transparent 55%),
		radial-gradient(80% 50% at 100% 30%, rgba(33, 151, 255, 0.08), transparent 45%), var(--vg-ink);
}

/* —— Body：与对话区约 1:3，整体约 25% / 75% —— */
.vg-body {
	display: flex;
	flex: 1 1 0%;
	order: 1;
	min-height: 0;
	max-height: none;
	gap: 12px;
	align-items: stretch;
}

.vg-left-rail {
	flex: 0 0 200px;
	min-width: 176px;
	min-height: 0;
	overflow-y: auto;
	padding: 18px 14px;
	background: var(--vg-panel);
	border: 1px solid var(--vg-border);
	border-radius: 20px;
	box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
	align-self: stretch;
}

.vg-rail-title {
	margin: 0 0 14px;
	font-size: 12px;
	color: var(--vg-muted);
	font-weight: 600;
	letter-spacing: 0.02em;
	text-transform: uppercase;
}

.vg-mode-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.vg-mode-item {
	padding: 11px 12px;
	border-radius: 12px;
	font-size: 13px;
	font-weight: 600;
	line-height: 1.35;
	color: var(--vg-muted);
	cursor: pointer;
	border: 1px solid transparent;
	background: rgba(0, 0, 0, 0.22);
	transition: color 0.15s, border-color 0.15s, background 0.15s;

	&:hover {
		color: var(--vg-text);
		background: rgba(255, 255, 255, 0.05);
	}

	&.active {
		color: var(--vg-cyan);
		border-color: rgba(0, 202, 224, 0.35);
		background: rgba(0, 202, 224, 0.1);
	}
}

.vg-generator {
	flex: 1;
	min-width: 280px;
	min-height: 0;
	display: flex;
	flex-direction: column;
	overflow: hidden;
}

.vg-generator-inner {
	flex: 1;
	min-height: 0;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	gap: 10px;
	padding: 12px 14px;
	background: var(--vg-panel);
	border: 1px solid var(--vg-border);
	border-radius: 20px;
	box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

.vg-refs {
	flex-shrink: 0;
}

.vg-ref-group {
	position: relative;
	border-radius: 16px;
	overflow: hidden;
	min-height: 92px;
}

.vg-ref-bg {
	position: absolute;
	inset: 0;
	background: linear-gradient(135deg, rgba(0, 202, 224, 0.08), rgba(33, 151, 255, 0.05));
	border: 1px dashed rgba(255, 255, 255, 0.12);
	border-radius: 16px;
	pointer-events: none;
}

.vg-ref-content {
	position: relative;
	padding: 12px;
}

.vg-ref-tilt {
	transform: rotate(-1.5deg);
	transition: transform 0.25s ease;

	&:hover {
		transform: rotate(0deg);
	}
}

.upload-inner {
	transform: rotate(1.5deg);

	.upload-title {
		margin-bottom: 10px;
	}
	.upload-title-left {
		color: var(--vg-text);
		font-weight: 600;
		font-size: 14px;
	}
	.upload-title-tip {
		color: var(--vg-muted);
		font-size: 12px;
		margin-top: 4px;
	}
	.last-frame {
		margin-top: 14px;
	}
}

.vg-ref-placeholder {
	position: relative;
	display: flex;
	align-items: center;
	gap: 14px;
	padding: 18px 20px;
	min-height: 88px;
}

.vg-ref-placeholder-icon {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 14px;
	background: rgba(255, 255, 255, 0.06);
	color: var(--vg-cyan);
}

.vg-ref-placeholder-text {
	margin: 0;
	font-size: 13px;
	line-height: 1.5;
	color: var(--vg-muted);
}

.vg-main-column {
	display: flex;
	flex-direction: column;
	flex: 1;
	min-height: 0;
	overflow: hidden;

	:deep(.vg-compose-root) {
		flex: 1;
		min-height: 0;
		max-height: 100%;
	}
}

.rich-editor-container {
	flex: 1;
	min-height: 180px;
	margin: 0;
	display: flex;
	flex-direction: column;

	:deep(.video-editor-root) {
		flex: 1;
		display: flex;
		flex-direction: column;
		min-height: 200px;
	}
}

.vg-toolbar {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 10px;
	margin-top: 8px;
	padding-top: 10px;
	border-top: 1px solid var(--vg-border);
}

.vg-toolbar-settings {
	flex: 1;
	min-width: 0;
}

.vg-params-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px 10px;
	padding: 4px 8px;
	background: rgba(0, 0, 0, 0.2);
	border-radius: 10px;
	border: 1px solid var(--vg-border);
}

.vg-param {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: 6px;
	flex: 1 1 160px;
	min-width: 0;
}

.vg-param-label {
	font-size: 11px;
	color: var(--vg-muted);
	font-weight: 600;
	white-space: nowrap;
	min-width: 44px;
}

.vg-param-select {
	flex: 1;
	min-width: 0;

	:deep(.arco-select-view-single) {
		background: rgba(0, 0, 0, 0.2) !important;
		border: 1px solid var(--vg-border) !important;
		border-radius: 8px !important;
		color: var(--vg-text) !important;
		min-height: 30px !important;
	}
	:deep(.arco-select-view-value) {
		color: var(--vg-text) !important;
		font-size: 12px;
	}
}

/* 参考图：工具条在富文本右侧窄列内，清空与提交纵向排列 */
.vg-toolbar--reference-submit {
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: 0;
	margin-top: 0;
	padding-top: 0;
	border-top: none;
	width: 100%;
	min-height: 0;
}

.vg-toolbar-actions {
	flex-shrink: 0;
	align-self: center;
	display: flex;
	align-items: center;
	gap: 8px;
}

.vg-toolbar-actions--reference {
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: 14px;
	width: 100%;
	align-self: center;
	padding-top: 0;
}

.vg-toolbar-clear:deep(.arco-btn) {
	color: rgba(255, 255, 255, 0.75);
}

.vg-toolbar-clear:deep(.arco-btn:hover) {
	color: #7eeaf2;
	background: rgba(0, 202, 224, 0.1);
}

/* 参考图模式：生成参数两列网格 */
.vg-params-row--reference-col {
	display: grid;
	/* 两列：
	   左列：生成模式 / 模型 / 分辨率（同一列，上下排列）
	   右列：比例 / 时长（同一列，上下排列）
	   且左列更宽，保证左列下拉框宽度 > 右列 */
	grid-template-columns: 2fr 1fr;
	grid-auto-rows: auto;
	align-items: start;
	gap: 8px 10px;
	padding: 6px 8px;
	min-height: 0;
	flex-wrap: nowrap;
}

.vg-params-row--reference-col .vg-param {
	flex: none;
	width: 100%;
	min-width: 0;
	flex-direction: column;
	align-items: stretch;
	gap: 4px;
}

.vg-params-row--reference-col > .vg-param:nth-child(1) {
	grid-column: 1;
	grid-row: 1;
}

.vg-params-row--reference-col > .vg-param:nth-child(2) {
	grid-column: 1;
	grid-row: 2;
}

.vg-params-row--reference-col > .vg-param:nth-child(3) {
	grid-column: 2;
	grid-row: 1;
}

.vg-params-row--reference-col > .vg-param:nth-child(4) {
	grid-column: 2;
	grid-row: 2;
}

.vg-params-row--reference-col > .vg-param:nth-child(5) {
	grid-column: 1;
	grid-row: 3;
}

.vg-params-row--reference-col .vg-param-label {
	min-width: 0;
}

@media (max-width: 520px) {
	.vg-params-row--reference-col {
		grid-template-columns: 1fr;
	}
}

.vg-submit-circle {
	width: 42px;
	height: 42px;
	padding: 0;
	border: none;
	border-radius: 50%;
	cursor: pointer;
	/* 稍微下移：让按钮对齐参数区视觉中线 */
	margin-top: 6px;
	background: linear-gradient(145deg, #00d4e8, #0090a8);
	box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.35), 0 8px 24px rgba(0, 150, 180, 0.35);
	color: #fff;
	display: flex;
	align-items: center;
	justify-content: center;
	transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;

	&:hover:not(:disabled) {
		transform: scale(1.04);
		box-shadow: 0 0 0 3px rgba(0, 202, 224, 0.5), 0 12px 28px rgba(0, 150, 180, 0.45);
	}

	&:disabled {
		opacity: 0.45;
		cursor: not-allowed;
		transform: none;
	}
}

.vg-submit-circle-inner {
	display: flex;
	align-items: center;
	justify-content: center;
}

/* —— 右侧预览（收窄宽度、视频限高，内容可滚动） —— */
.right {
	flex: 0 1 360px;
	min-width: 260px;
	max-width: min(360px, 100%);
	min-height: 0;
	background: rgba(0, 0, 0, 0.35);
	border: 1px solid var(--vg-border);
	border-radius: 20px;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	position: relative;
	overflow-x: hidden;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;

	&.has-result {
		align-items: stretch;
		justify-content: flex-start;
		padding: 16px;
	}
}

.empty-state {
	text-align: center;
	color: var(--vg-muted);
	padding: 24px;

	.empty-visual {
		position: relative;
		display: inline-block;
		margin-bottom: 16px;
	}

	.empty-ring {
		position: absolute;
		inset: -12px;
		border-radius: 50%;
		border: 1px solid rgba(0, 202, 224, 0.2);
		animation: vg-pulse 2.4s ease-in-out infinite;
	}

	img {
		position: relative;
		width: 120px;
		opacity: 0.55;
	}

	p {
		margin: 0;
		font-size: 14px;
	}
}

.result-container {
	width: 100%;
	box-sizing: border-box;
}

@keyframes vg-pulse {
	0%,
	100% {
		transform: scale(1);
		opacity: 0.5;
	}
	50% {
		transform: scale(1.08);
		opacity: 0.85;
	}
}

.result-video-wrapper {
	width: 100%;
	max-width: 320px;
	margin: 0 auto;
}

.result-video {
	display: block;
	width: 100%;
	max-width: 100%;
	max-height: min(42vh, 360px);
	height: auto;
	object-fit: contain;
	background: #000;
	border-radius: 12px;
}

.result-actions {
	margin-top: 16px;
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
}

@media (max-width: 768px) {
	.vg-body {
		flex: 1 1 auto;
		flex-direction: column;
		max-height: none;
		min-height: min(36vh, 400px);
	}
	.vg-left-rail {
		flex: none;
		width: 100%;
		min-width: 0;
	}
	.vg-mode-list {
		flex-direction: row;
		flex-wrap: wrap;
	}
	.vg-mode-item {
		flex: 1 1 calc(50% - 4px);
		min-width: 140px;
		text-align: center;
	}
	.vg-generator {
		flex: none;
		width: 100%;
		max-height: none;
	}
	.right {
		flex: 1 1 auto;
		max-width: none;
		min-height: 260px;
		max-height: min(55vh, 480px);
	}

	/* 对话内视频小屏占满宽，避免横向截断 */
	.vg-chat-result video {
		width: 100%;
		max-width: 100%;
	}
}

.vg-task-empty {
	margin: 0;
	font-size: 13px;
	color: var(--vg-muted);
}

.vg-chat-section {
	order: 0;
	flex: 3 1 0%;
	margin-top: 0;
	padding: 14px 18px;
	background: var(--vg-panel);
	border: 1px solid var(--vg-border);
	border-radius: 16px;
	min-height: 0;
	display: flex;
	flex-direction: column;
}

.vg-chat-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	margin-bottom: 12px;
}

.vg-chat-title {
	margin: 0;
	font-size: 15px;
	font-weight: 600;
	color: var(--vg-text);
}

.vg-chat-list {
	display: flex;
	flex-direction: column;
	flex: 1;
	min-height: 0;
	gap: 14px;
	max-height: none;
	overflow: auto;
	padding-right: 4px;
}

.vg-chat-block {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: 0;
	padding: 16px;
	border-radius: 16px;
	background: rgba(0, 0, 0, 0.25);
	border: 1px solid rgba(255, 255, 255, 0.1);
	min-width: 0;
	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.vg-chat-block--compact {
	padding: 12px 16px;
}

.vg-chat-divider {
	height: 1px;
	margin: 4px 0 0;
	background: rgba(255, 255, 255, 0.08);
	border: none;
	flex-shrink: 0;
}

/* 用户输入区域 */
.vg-chat-user-section {
	padding-bottom: 16px;
	border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.vg-chat-user-section--compact {
	padding-bottom: 0;
	border-bottom: none;
}

.vg-chat-ai-section {
	padding-top: 16px;
}

.vg-chat-user-text {
	font-size: 13px;
	color: var(--vg-text);
	white-space: pre-wrap;
	line-height: 1.7;
	margin-bottom: 12px;
}

.vg-chat-inline-ref-image {
	display: inline-block;
	width: 44px;
	height: 44px;
	object-fit: cover;
	border-radius: 6px;
	vertical-align: middle;
	margin: 0 4px;
	border: 1px solid rgba(255, 255, 255, 0.12);
}

.vg-chat-inline-ref-video {
	display: inline-block;
	width: 120px;
	max-height: 72px;
	vertical-align: middle;
	margin: 0 4px;
	border-radius: 6px;
	border: 1px solid rgba(255, 255, 255, 0.12);
}

.vg-chat-inline-audio {
	display: inline-block;
	vertical-align: middle;
	margin: 0 4px;
	max-width: 200px;
	height: 32px;
}

.vg-chat-time {
	margin-top: 8px;
	font-size: 12px;
	color: var(--vg-muted);
}

.vg-chat-user-row {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 12px;
}

.vg-chat-user-col-main {
	flex: 1;
	min-width: 0;
}

.vg-chat-user-col-main .vg-chat-time {
	margin-top: 8px;
}

.vg-chat-user-col-status {
	flex-shrink: 0;
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 6px;
	max-width: 42%;
	text-align: right;
}

.vg-chat-inline-status {
	font-size: 12px;
	font-weight: 600;
	line-height: 1.35;
	text-align: right;
	word-break: break-word;
}

.vg-chat-inline-status--running {
	color: var(--vg-cyan);
}

.vg-chat-inline-status--failed {
	color: rgba(255, 107, 107, 0.95);
}

.vg-chat-inline-cancel {
	font-size: 12px;
	padding: 0;
}

.vg-chat-params {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	margin-top: 10px;
	margin-bottom: 6px;
}

.vg-chat-param-chip {
	font-size: 11px;
	line-height: 1.35;
	padding: 3px 8px;
	border-radius: 6px;
	background: rgba(255, 255, 255, 0.06);
	border: 1px solid rgba(255, 255, 255, 0.08);
	color: rgba(255, 255, 255, 0.78);
}

.vg-chat-task-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 12px;
	margin-bottom: 4px;
}

.vg-chat-action-btn {
	font-size: 12px;
	padding: 0;
}

.vg-chat-action-btn:disabled {
	opacity: 0.45;
	cursor: not-allowed;
}

.favorite-btn {
	color: #ff9800 !important;
}

.favorite-btn.is-favorited {
	color: #ffeb3b !important;
	font-weight: 600;
}

.vg-chat-ai-top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
	flex-wrap: wrap;
	margin-bottom: 12px;
}

.vg-chat-download-btn {
	flex-shrink: 0;
	font-size: 12px;
	font-weight: 600;
	padding: 6px 12px;
	border-radius: 8px;
	border: 1px solid rgba(0, 202, 224, 0.45);
	background: rgba(0, 202, 224, 0.12);
	color: var(--vg-cyan);
	cursor: pointer;
}

.vg-chat-download-btn:hover {
	background: rgba(0, 202, 224, 0.2);
}

.vg-chat-ai-status {
	font-size: 13px;
	color: var(--vg-cyan);
	font-weight: 600;
}

.vg-chat-ai-time {
	font-size: 12px;
	color: var(--vg-muted);
}

.vg-chat-loading {
	padding: 16px 0;
	font-size: 13px;
	color: var(--vg-muted);
	text-align: center;
}

.vg-chat-failed {
	padding: 16px 0;
	font-size: 13px;
	color: rgba(255, 107, 107, 0.95);
	text-align: center;
}

.vg-chat-result {
	margin-top: 8px;
}

.vg-chat-result video {
	width: 33%;
	max-width: 33%;
	margin: 0;
	display: block;
	max-height: 240px;
	border-radius: 12px;
	background: #000;
}

.vg-chat-result-link {
	padding: 10px 0;
}

.vg-chat-result-link a {
	color: var(--vg-cyan);
}

.vg-chat-ai-actions {
	margin-top: 10px;
	display: flex;
	justify-content: flex-end;
}

.vg-chat-loadmore {
	padding: 6px 0;
	font-size: 12px;
	color: var(--vg-muted);
	text-align: center;
}

.vg-task-section {
	flex-shrink: 0;
	margin-top: 8px;
	padding: 18px 22px;
	background: var(--vg-panel);
	border: 1px solid var(--vg-border);
	border-radius: 16px;
	min-height: 0;
}

.vg-task-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	margin-bottom: 12px;
}

.vg-task-title {
	margin: 0;
	font-size: 15px;
	font-weight: 600;
	color: var(--vg-text);
}

.vg-task-refresh {
	font-size: 13px;
	padding: 6px 12px;
	border-radius: 8px;
	border: 1px solid var(--vg-border);
	background: rgba(255, 255, 255, 0.06);
	color: var(--vg-muted);
	cursor: pointer;
}

.vg-task-refresh:hover {
	color: var(--vg-cyan);
	border-color: rgba(0, 202, 224, 0.35);
}

.vg-task-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 8px;
	max-height: 200px;
	overflow: auto;
}

.vg-task-item {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 10px 12px;
	border-radius: 10px;
	background: rgba(0, 0, 0, 0.2);
	font-size: 12px;
}

.vg-task-meta {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
}

.vg-task-id {
	color: var(--vg-muted);
	word-break: break-all;
}

.vg-task-st {
	color: var(--vg-cyan);
	font-weight: 600;
}

.vg-link {
	background: none;
	border: none;
	padding: 0;
	color: #ff6b6b;
	cursor: pointer;
	font-size: 12px;
}

.vg-link:hover {
	text-decoration: underline;
}

/* 小于 1920×1080：工具条参数纵向排布；整体仍保持约 75%/25% 弹性分配 */
@media (max-width: 1919px) {
	.video-gen {
		padding: 14px 12px 12px;
		gap: 10px;
	}

	.vg-chat-section {
		padding: 12px 14px;
	}

	.vg-generator-inner {
		padding: 10px 12px;
		gap: 8px;
	}

	.vg-toolbar {
		flex-direction: column;
		align-items: stretch;
		gap: 8px;
	}

	.vg-toolbar-actions {
		align-self: flex-end;
	}

	.vg-params-row {
		flex-direction: column;
		align-items: stretch;
		gap: 8px;
	}

	.vg-param {
		flex: 1 1 auto;
		width: 100%;
		min-width: 0;
	}
}
</style>
