1039 lines
24 KiB
Vue
1039 lines
24 KiB
Vue
<template>
|
||
<view class="page">
|
||
<scroll-view scroll-y class="page-scroll">
|
||
<view class="hero-card">
|
||
<view class="hero-title">AI 创意助手</view>
|
||
<view class="hero-desc">
|
||
支持文本指令与多类型附件联合驱动工作流,自动返回视频、脚本 Markdown 或图像 / 排版结果。
|
||
</view>
|
||
<view class="hero-actions">
|
||
<view class="hero-action" @tap="fillExample('video')">短视频创意</view>
|
||
<view class="hero-action" @tap="fillExample('script')">脚本创作</view>
|
||
<view class="hero-action" @tap="fillExample('image')">海报排版</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel-card">
|
||
<view class="panel-title">输入配置</view>
|
||
<view class="field-list">
|
||
<view class="field-item">
|
||
<view class="field-label">文本指令</view>
|
||
<textarea
|
||
v-model="content"
|
||
class="field-textarea field-textarea--large"
|
||
placeholder="请输入用户的文本指令或需求描述,例如:根据活动照片和通知文档,生成一条公众号推文封面及 60 秒短视频脚本。"
|
||
placeholder-style="color: #c0c4cc;"
|
||
maxlength="-1"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field-item">
|
||
<view class="field-label">上传附件</view>
|
||
<view class="upload-bar">
|
||
<view class="upload-btn" @tap="chooseFiles">{{ uploading ? '上传中...' : '添加附件' }}</view>
|
||
<view class="upload-tip">支持图片、视频、音频和常见文档类型。</view>
|
||
</view>
|
||
<view v-if="uploadedFiles.length" class="file-list">
|
||
<view
|
||
v-for="(item, index) in uploadedFiles"
|
||
:key="`${item.url || item.path || item.name}-${index}`"
|
||
class="file-item-card"
|
||
>
|
||
<view class="file-main">
|
||
<view class="file-badge">{{ getFileTypeLabel(item) }}</view>
|
||
<view class="file-text">
|
||
<view class="file-name">{{ item.name || getFileName(item) }}</view>
|
||
<view class="file-url">{{ resolveFileUrl(item) || '-' }}</view>
|
||
</view>
|
||
</view>
|
||
<view class="file-remove" @tap="removeUploadedFile(index)">移除</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="submit-bar">
|
||
<view class="ghost-btn" @tap="resetAll">重置</view>
|
||
<view class="primary-btn" :class="{ 'primary-btn--disabled': !canSubmit || loading }" @tap="runWorkflow">
|
||
{{ loading ? '执行中' : '生成' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel-card result-card">
|
||
<view class="result-head">
|
||
<view class="panel-title">工作流结果</view>
|
||
<view class="result-status" :class="loading ? 'result-status--loading' : hasAnyResult ? 'result-status--done' : ''">
|
||
{{ loading ? '执行中' : hasAnyResult ? '已完成' : '待执行' }}
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading" class="loading-panel">
|
||
<view class="loading-kicker">WORKFLOW RUNNING</view>
|
||
<view class="loading-title">{{ loadingTitle }}</view>
|
||
<view class="loading-desc">{{ loadingDescription }}</view>
|
||
<view class="loading-steps">
|
||
<view
|
||
v-for="(item, index) in loadingSteps"
|
||
:key="item.title"
|
||
class="loading-step"
|
||
:class="{ 'is-active': index === 1 }"
|
||
>
|
||
<view class="loading-step-index">0{{ index + 1 }}</view>
|
||
<view class="loading-step-main">
|
||
<view class="loading-step-title">{{ item.title }}</view>
|
||
<view class="loading-step-desc">{{ item.desc }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else-if="!hasAnyResult" class="result-empty">
|
||
填写文本指令并上传附件后执行工作流
|
||
</view>
|
||
|
||
<view v-else class="result-body">
|
||
<view v-if="statusMessage" class="status-alert">
|
||
{{ statusMessage }}
|
||
</view>
|
||
|
||
<view v-if="videoFiles.length" class="result-section">
|
||
<view class="section-title">视频生成结果</view>
|
||
<view class="video-list">
|
||
<view
|
||
v-for="(item, index) in videoFiles"
|
||
:key="`${resolveFileUrl(item)}-${index}`"
|
||
class="video-card"
|
||
>
|
||
<video
|
||
v-if="isVideoFile(item)"
|
||
class="video-player"
|
||
controls
|
||
object-fit="cover"
|
||
:src="resolveFileUrl(item)"
|
||
></video>
|
||
<view class="result-meta">
|
||
<view class="result-name">{{ item.name || getFileName(item) }}</view>
|
||
<view class="result-link" @tap="copyFileLink(item)">复制链接</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="scriptDisplayContent" class="result-section">
|
||
<view class="section-title">脚本创作结果</view>
|
||
<zero-markdown-view
|
||
class="script-shell script-markdown"
|
||
:markdown="scriptDisplayContent"
|
||
/>
|
||
</view>
|
||
|
||
<view v-if="photoFiles.length" class="result-section">
|
||
<view class="section-title">图像 / 排版结果</view>
|
||
<view class="photo-grid">
|
||
<view
|
||
v-for="(item, index) in photoFiles"
|
||
:key="`${resolveFileUrl(item)}-${index}`"
|
||
class="photo-card"
|
||
>
|
||
<image
|
||
v-if="isImageFile(item)"
|
||
class="photo-image"
|
||
mode="aspectFill"
|
||
:src="resolveFileUrl(item)"
|
||
@tap="previewImage(item)"
|
||
/>
|
||
<view v-else class="photo-fallback">
|
||
<view class="file-badge">{{ getFileTypeLabel(item) }}</view>
|
||
<view class="result-link" @tap="copyFileLink(item)">复制结果链接</view>
|
||
</view>
|
||
<view class="result-meta">
|
||
<view class="result-name">{{ item.name || getFileName(item) }}</view>
|
||
<view class="result-link" @tap="copyFileLink(item)">复制链接</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="result-actions">
|
||
<view class="result-btn" @tap="copyScript">复制脚本</view>
|
||
<view class="result-btn" @tap="clearResult">清空结果</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { creativeAssistant, uploadAiFile } from './service'
|
||
|
||
const DOCUMENT_FILE_EXTS = new Set([
|
||
'TXT',
|
||
'MD',
|
||
'MARKDOWN',
|
||
'PDF',
|
||
'HTML',
|
||
'XLSX',
|
||
'XLS',
|
||
'DOCX',
|
||
'CSV',
|
||
'EML',
|
||
'MSG',
|
||
'PPTX',
|
||
'PPT',
|
||
'XML',
|
||
'EPUB'
|
||
])
|
||
const AUDIO_FILE_EXTS = new Set(['MP3', 'M4A', 'WAV', 'WEBM', 'AMR', 'AAC', 'OGG'])
|
||
const VIDEO_FILE_EXTS = new Set(['MP4', 'MOV', 'MPEG', 'MPGA', 'WEBM', 'AVI', 'M4V'])
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
uploading: false,
|
||
content: '',
|
||
uploadedFiles: [],
|
||
workflowResponse: null,
|
||
workflowOutputs: null,
|
||
loadingSteps: [
|
||
{
|
||
title: '输入理解',
|
||
desc: '解析文本指令、识别目标输出和执行路径'
|
||
},
|
||
{
|
||
title: '内容生成',
|
||
desc: '调用工作流处理视频、脚本或图像排版任务'
|
||
},
|
||
{
|
||
title: '结果整理',
|
||
desc: '回收文件链接并整理结构化输出'
|
||
}
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
canSubmit() {
|
||
return !!String(this.content || '').trim() || this.uploadedFiles.length > 0
|
||
},
|
||
videoFiles() {
|
||
return (this.workflowOutputs && this.workflowOutputs.video_result) || []
|
||
},
|
||
scriptMarkdown() {
|
||
return String((this.workflowOutputs && this.workflowOutputs.Script_result) || '')
|
||
},
|
||
scriptDisplayContent() {
|
||
console.log(this.scriptMarkdown
|
||
.replace(/<think>[\s\S]*?<\/think>/g, '')
|
||
.trim())
|
||
return this.scriptMarkdown
|
||
.replace(/<think>[\s\S]*?<\/think>/g, '')
|
||
.trim()
|
||
},
|
||
photoFiles() {
|
||
return (this.workflowOutputs && this.workflowOutputs.photo_files) || []
|
||
},
|
||
statusMessage() {
|
||
return (
|
||
(this.workflowOutputs && this.workflowOutputs.video_message) ||
|
||
(this.workflowOutputs && this.workflowOutputs.photo_message) ||
|
||
''
|
||
)
|
||
},
|
||
hasAnyResult() {
|
||
return !!(
|
||
this.videoFiles.length ||
|
||
this.scriptMarkdown.trim() ||
|
||
this.photoFiles.length
|
||
)
|
||
},
|
||
loadingTitle() {
|
||
if (this.uploadedFiles.length && String(this.content || '').trim()) {
|
||
return '正在分析指令并处理附件'
|
||
}
|
||
if (this.uploadedFiles.length) {
|
||
return '正在解析附件并生成结果'
|
||
}
|
||
return '正在根据文本指令生成内容'
|
||
},
|
||
loadingDescription() {
|
||
return '该工作流可能持续数分钟,请保持页面开启。系统正在后台完成素材理解、内容生成与结果整理。'
|
||
}
|
||
},
|
||
methods: {
|
||
showToast(title) {
|
||
uni.showToast({
|
||
title,
|
||
icon: 'none'
|
||
})
|
||
},
|
||
normalizeOutputPayload(payload) {
|
||
if (!payload) {
|
||
return {}
|
||
}
|
||
if (payload.outputs && typeof payload.outputs === 'object') {
|
||
return payload.outputs
|
||
}
|
||
if (payload.data && payload.data.outputs && typeof payload.data.outputs === 'object') {
|
||
return payload.data.outputs
|
||
}
|
||
return payload
|
||
},
|
||
resolveFileUrl(file) {
|
||
if (!file) {
|
||
return ''
|
||
}
|
||
return String(file.url || file.downloadUrl || file.thumbnail || file.path || '')
|
||
},
|
||
getFileName(file) {
|
||
const value = this.resolveFileUrl(file)
|
||
if (!value) {
|
||
return '未命名文件'
|
||
}
|
||
try {
|
||
return decodeURIComponent(value.split('?')[0].split('/').pop() || value)
|
||
} catch (error) {
|
||
return value.split('?')[0].split('/').pop() || value
|
||
}
|
||
},
|
||
getFileExt(file) {
|
||
const name = String((file && (file.name || this.getFileName(file))) || '').toLowerCase()
|
||
return name.split('.').pop() || ''
|
||
},
|
||
isImageFile(file) {
|
||
const contentType = String((file && file.contentType) || '').toLowerCase()
|
||
if (contentType.startsWith('image/')) {
|
||
return true
|
||
}
|
||
return ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(this.getFileExt(file))
|
||
},
|
||
isVideoFile(file) {
|
||
const contentType = String((file && file.contentType) || '').toLowerCase()
|
||
if (contentType.startsWith('video/')) {
|
||
return true
|
||
}
|
||
return ['mp4', 'mov', 'm4v', 'webm', 'avi'].includes(this.getFileExt(file))
|
||
},
|
||
isAudioFile(file) {
|
||
const contentType = String((file && file.contentType) || '').toLowerCase()
|
||
if (contentType.startsWith('audio/')) {
|
||
return true
|
||
}
|
||
return ['mp3', 'wav', 'aac', 'm4a', 'ogg', 'amr'].includes(this.getFileExt(file))
|
||
},
|
||
getUploadFileType(file) {
|
||
const contentType = String((file && file.contentType) || '').toLowerCase()
|
||
const ext = this.getFileExt(file).toUpperCase()
|
||
if (contentType.startsWith('image/')) {
|
||
return 'image'
|
||
}
|
||
if (AUDIO_FILE_EXTS.has(ext)) {
|
||
return 'audio'
|
||
}
|
||
if (VIDEO_FILE_EXTS.has(ext)) {
|
||
return 'video'
|
||
}
|
||
if (DOCUMENT_FILE_EXTS.has(ext)) {
|
||
return 'document'
|
||
}
|
||
return 'document'
|
||
},
|
||
getFileTypeLabel(file) {
|
||
if (this.isImageFile(file)) {
|
||
return '图片'
|
||
}
|
||
if (this.isVideoFile(file)) {
|
||
return '视频'
|
||
}
|
||
if (this.isAudioFile(file)) {
|
||
return '音频'
|
||
}
|
||
return '文档'
|
||
},
|
||
clearResult() {
|
||
this.workflowResponse = null
|
||
this.workflowOutputs = null
|
||
},
|
||
resetAll() {
|
||
this.content = ''
|
||
this.uploadedFiles = []
|
||
this.clearResult()
|
||
},
|
||
removeUploadedFile(index) {
|
||
this.uploadedFiles = this.uploadedFiles.filter((item, current) => current !== index)
|
||
},
|
||
async chooseFiles() {
|
||
if (this.uploading) {
|
||
return
|
||
}
|
||
try {
|
||
const result = await new Promise((resolve, reject) => {
|
||
uni.chooseMessageFile({
|
||
count: 9,
|
||
type: 'all',
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
const tempFiles = (result && result.tempFiles) || []
|
||
if (!tempFiles.length) {
|
||
return
|
||
}
|
||
this.uploading = true
|
||
uni.showLoading({
|
||
title: '上传中',
|
||
mask: true
|
||
})
|
||
for (let index = 0; index < tempFiles.length; index += 1) {
|
||
const file = tempFiles[index]
|
||
if (file.size && file.size / 1024 / 1024 > 100) {
|
||
this.showToast('单个附件不能超过 100MB')
|
||
continue
|
||
}
|
||
const uploaded = await uploadAiFile(file)
|
||
this.uploadedFiles = [
|
||
...this.uploadedFiles,
|
||
{
|
||
id: uploaded.id,
|
||
name: uploaded.name || file.name,
|
||
path: uploaded.path,
|
||
url: uploaded.url,
|
||
downloadUrl: uploaded.downloadUrl,
|
||
contentType: uploaded.contentType
|
||
}
|
||
]
|
||
}
|
||
this.showToast('附件上传成功')
|
||
} catch (error) {
|
||
if (error && error.errMsg && String(error.errMsg).includes('cancel')) {
|
||
return
|
||
}
|
||
this.showToast((error && error.message) || '上传失败')
|
||
} finally {
|
||
this.uploading = false
|
||
uni.hideLoading()
|
||
}
|
||
},
|
||
buildRequestPayload() {
|
||
const payload = {
|
||
user_content: String(this.content || '').trim(),
|
||
user_contentString: String(this.content || '').trim()
|
||
}
|
||
if (this.uploadedFiles.length) {
|
||
payload.user_files = this.uploadedFiles.map((item) => ({
|
||
transfer_method: 'local_file',
|
||
type: this.getUploadFileType(item),
|
||
upload_file_id: item.id,
|
||
url: ''
|
||
}))
|
||
}
|
||
return payload
|
||
},
|
||
async runWorkflow() {
|
||
if (!this.canSubmit) {
|
||
this.showToast('请先输入文本指令或上传附件')
|
||
return
|
||
}
|
||
if (this.loading) {
|
||
return
|
||
}
|
||
this.loading = true
|
||
try {
|
||
const result = await creativeAssistant(this.buildRequestPayload())
|
||
this.workflowResponse = result || null
|
||
this.workflowOutputs = this.normalizeOutputPayload(result || null)
|
||
if (!this.hasAnyResult) {
|
||
this.showToast('接口已返回,但未识别到文档约定的输出字段')
|
||
} else {
|
||
this.showToast('工作流执行完成')
|
||
}
|
||
} catch (error) {
|
||
this.showToast((error && error.message) || '工作流执行失败')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
copyScript() {
|
||
if (!this.scriptDisplayContent) {
|
||
this.showToast('暂无脚本结果')
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: this.scriptDisplayContent,
|
||
success: () => {
|
||
this.showToast('脚本已复制')
|
||
}
|
||
})
|
||
},
|
||
copyFileLink(file) {
|
||
const url = this.resolveFileUrl(file)
|
||
if (!url) {
|
||
this.showToast('当前文件缺少链接')
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: url,
|
||
success: () => {
|
||
this.showToast('链接已复制')
|
||
}
|
||
})
|
||
},
|
||
previewImage(file) {
|
||
const url = this.resolveFileUrl(file)
|
||
if (!url) {
|
||
this.showToast('当前图片缺少地址')
|
||
return
|
||
}
|
||
const urls = this.photoFiles.map((item) => this.resolveFileUrl(item)).filter(Boolean)
|
||
uni.previewImage({
|
||
current: url,
|
||
urls
|
||
})
|
||
},
|
||
fillExample(type) {
|
||
if (type === 'video') {
|
||
this.content = '请根据我上传的活动照片和通知文档,生成一条 45 秒校园活动宣传短视频,画面节奏明快,配一段适合配音的解说文案,并输出视频结果。'
|
||
return
|
||
}
|
||
if (type === 'script') {
|
||
this.content = '围绕“青年志愿者进社区”主题创作一份短视频分镜脚本,要求包含开场钩子、3 个主要场景和结尾号召,输出为 Markdown 脚本。'
|
||
return
|
||
}
|
||
this.content = '请根据上传的活动照片、LOGO 和文案素材,生成一套公众号头图与活动海报排版,风格青春、明亮,输出图片文件。'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background:
|
||
radial-gradient(circle at top right, rgba(20, 150, 242, 0.14), transparent 24%),
|
||
linear-gradient(180deg, #fff8f3 0%, #f7fbff 100%);
|
||
}
|
||
|
||
.page-scroll {
|
||
height: 100vh;
|
||
padding: 28rpx 24rpx 40rpx;
|
||
}
|
||
|
||
.hero-card {
|
||
padding: 34rpx 30rpx;
|
||
border-radius: 32rpx;
|
||
background: linear-gradient(135deg, #0c5f9f 0%, #1496f2 48%, #67c7ff 100%);
|
||
box-shadow: 0 18rpx 40rpx rgba(143, 31, 45, 0.2);
|
||
color: #333;
|
||
}
|
||
|
||
.hero-kicker {
|
||
font-size: 22rpx;
|
||
letter-spacing: 4rpx;
|
||
color: rgba(255, 235, 237, 0.72);
|
||
}
|
||
|
||
.hero-title {
|
||
margin-top: 18rpx;
|
||
font-size: 42rpx;
|
||
line-height: 1.35;
|
||
font-weight: 700;
|
||
color: #fff8f8;
|
||
}
|
||
|
||
.hero-desc {
|
||
margin-top: 16rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.8;
|
||
color: rgba(255, 239, 241, 0.88);
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
margin-top: 26rpx;
|
||
}
|
||
|
||
.hero-action {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
font-size: 22rpx;
|
||
color: #fff6f6;
|
||
}
|
||
|
||
.panel-card {
|
||
margin-top: 24rpx;
|
||
padding: 26rpx;
|
||
border-radius: 30rpx;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||
box-shadow: 0 14rpx 34rpx rgba(87, 54, 30, 0.08);
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.field-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 22rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.field-label {
|
||
margin-bottom: 12rpx;
|
||
font-size: 25rpx;
|
||
font-weight: 600;
|
||
color: #2b3037;
|
||
}
|
||
|
||
.field-textarea {
|
||
width: 100%;
|
||
min-height: 220rpx;
|
||
padding: 22rpx 24rpx;
|
||
border-radius: 22rpx;
|
||
background: #fcf6f2;
|
||
border: 1rpx solid #d6eafb;
|
||
font-size: 24rpx;
|
||
line-height: 1.8;
|
||
color: #2a2e35;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.field-textarea--large {
|
||
min-height: 360rpx;
|
||
}
|
||
|
||
:deep(.field-input.uv-input) {
|
||
width: 100%;
|
||
min-height: 88rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 12rpx;
|
||
border: 1px solid #dcdfe6;
|
||
background: #ffffff;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||
height: 88rpx;
|
||
min-height: 88rpx;
|
||
font-size: 28rpx;
|
||
line-height: 88rpx;
|
||
color: #303133;
|
||
}
|
||
|
||
.upload-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.upload-btn {
|
||
align-self: flex-start;
|
||
padding: 14rpx 24rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(20, 150, 242, 0.1);
|
||
color: #1496f2;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.upload-tip {
|
||
font-size: 22rpx;
|
||
line-height: 1.7;
|
||
color: #88715f;
|
||
}
|
||
|
||
.file-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.file-item-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
padding: 18rpx 20rpx;
|
||
border-radius: 22rpx;
|
||
background: #f4faff;
|
||
border: 1rpx solid #d6eafb;
|
||
}
|
||
|
||
.file-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-badge {
|
||
flex-shrink: 0;
|
||
min-width: 72rpx;
|
||
padding: 8rpx 14rpx;
|
||
border-radius: 999rpx;
|
||
background: #eaf5ff;
|
||
color: #1496f2;
|
||
text-align: center;
|
||
font-size: 20rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.file-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-name,
|
||
.file-url,
|
||
.result-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-name {
|
||
color: #3b2c20;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.file-url {
|
||
margin-top: 8rpx;
|
||
color: #8c7462;
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.file-remove {
|
||
flex-shrink: 0;
|
||
color: #ff4d4f;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.submit-bar,
|
||
.result-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 18rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.ghost-btn,
|
||
.primary-btn,
|
||
.result-btn {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
border-radius: 22rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.ghost-btn {
|
||
background: #f6f7fa;
|
||
color: #6f5a4b;
|
||
border: 1rpx solid #ead7c8;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%);
|
||
color: #fff;
|
||
}
|
||
|
||
.primary-btn--disabled {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.result-btn {
|
||
background: #edf6ff;
|
||
color: #0f7dd1;
|
||
}
|
||
|
||
.result-card {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.result-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.result-status {
|
||
padding: 10rpx 16rpx;
|
||
border-radius: 999rpx;
|
||
background: #f3f0ed;
|
||
color: #8b827d;
|
||
font-size: 20rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.result-status--loading {
|
||
background: #edf5ff;
|
||
color: #1554ad;
|
||
}
|
||
|
||
.result-status--done {
|
||
background: #eef9f1;
|
||
color: #1f8f49;
|
||
}
|
||
|
||
.result-empty {
|
||
margin-top: 22rpx;
|
||
padding: 32rpx 24rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fafc;
|
||
font-size: 24rpx;
|
||
line-height: 1.75;
|
||
color: #8a94a0;
|
||
text-align: center;
|
||
}
|
||
|
||
.result-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18rpx;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.status-alert {
|
||
padding: 20rpx 22rpx;
|
||
border-radius: 22rpx;
|
||
background: #f8fbff;
|
||
color: #5f6b7a;
|
||
font-size: 22rpx;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.loading-panel {
|
||
display: grid;
|
||
gap: 18rpx;
|
||
margin-top: 22rpx;
|
||
padding: 22rpx;
|
||
border-radius: 20rpx;
|
||
background:
|
||
radial-gradient(circle at top right, rgba(255, 182, 122, 0.24), transparent 30%),
|
||
linear-gradient(135deg, #fff7ef 0%, #fffdfb 100%);
|
||
border: 1rpx solid #d6eafb;
|
||
}
|
||
|
||
.loading-kicker {
|
||
font-size: 20rpx;
|
||
letter-spacing: 3rpx;
|
||
color: #1496f2;
|
||
}
|
||
|
||
.loading-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #0f7dd1;
|
||
}
|
||
|
||
.loading-desc {
|
||
font-size: 22rpx;
|
||
line-height: 1.75;
|
||
color: #8b735e;
|
||
}
|
||
|
||
.loading-steps {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.loading-step {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16rpx;
|
||
padding: 16rpx 18rpx;
|
||
border-radius: 18rpx;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
}
|
||
|
||
.loading-step.is-active {
|
||
border: 1rpx solid rgba(214, 117, 44, 0.18);
|
||
}
|
||
|
||
.loading-step-index {
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
color: #1496f2;
|
||
}
|
||
|
||
.loading-step-title {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #0f7dd1;
|
||
}
|
||
|
||
.loading-step-desc {
|
||
margin-top: 6rpx;
|
||
font-size: 20rpx;
|
||
line-height: 1.6;
|
||
color: #8b735e;
|
||
}
|
||
|
||
.result-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: #2d3238;
|
||
}
|
||
|
||
.video-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.video-card,
|
||
.photo-card {
|
||
padding: 18rpx;
|
||
border-radius: 22rpx;
|
||
background: #f4faff;
|
||
border: 1rpx solid #d6eafb;
|
||
}
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 360rpx;
|
||
border-radius: 18rpx;
|
||
background: #000;
|
||
}
|
||
|
||
.script-shell {
|
||
padding: 22rpx 24rpx;
|
||
border-radius: 22rpx;
|
||
background: #f8fafc;
|
||
border: 1rpx solid #ebeff5;
|
||
}
|
||
|
||
.script-markdown {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
line-height: 1.85;
|
||
color: #2b3037;
|
||
word-break: break-all;
|
||
|
||
p { margin: 0 0 16rpx; }
|
||
p:last-child { margin-bottom: 0; }
|
||
|
||
h1, h2, h3, h4, h5, h6 {
|
||
font-weight: 700;
|
||
margin: 20rpx 0 10rpx;
|
||
color: #1a1f27;
|
||
}
|
||
h1 { font-size: 36rpx; }
|
||
h2 { font-size: 32rpx; }
|
||
h3 { font-size: 28rpx; }
|
||
|
||
ul, ol {
|
||
padding-left: 36rpx;
|
||
margin: 10rpx 0;
|
||
}
|
||
li { margin-bottom: 8rpx; }
|
||
|
||
code {
|
||
display: inline;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 8rpx;
|
||
background: rgba(20, 150, 242, 0.08);
|
||
color: #1496f2;
|
||
font-size: 22rpx;
|
||
font-family: monospace;
|
||
}
|
||
|
||
pre {
|
||
padding: 20rpx;
|
||
border-radius: 14rpx;
|
||
background: #f1f3f7;
|
||
margin: 14rpx 0;
|
||
overflow-x: auto;
|
||
}
|
||
pre code {
|
||
padding: 0;
|
||
background: transparent;
|
||
color: #2d3a47;
|
||
font-size: 22rpx;
|
||
white-space: pre;
|
||
}
|
||
|
||
blockquote {
|
||
margin: 12rpx 0;
|
||
padding: 10rpx 20rpx;
|
||
border-left: 6rpx solid rgba(20, 150, 242, 0.3);
|
||
color: #6b5f59;
|
||
background: rgba(20, 150, 242, 0.04);
|
||
border-radius: 0 12rpx 12rpx 0;
|
||
}
|
||
|
||
hr {
|
||
margin: 18rpx 0;
|
||
border: none;
|
||
border-top: 1rpx solid #e0e6ed;
|
||
}
|
||
|
||
strong { font-weight: 700; }
|
||
em { font-style: italic; }
|
||
}
|
||
|
||
.photo-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.photo-image {
|
||
width: 100%;
|
||
height: 240rpx;
|
||
border-radius: 18rpx;
|
||
background: #f2f4f8;
|
||
}
|
||
|
||
.photo-fallback {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
height: 240rpx;
|
||
border-radius: 18rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.result-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.result-name {
|
||
flex: 1;
|
||
font-size: 22rpx;
|
||
color: #3b2c20;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.result-link {
|
||
flex-shrink: 0;
|
||
font-size: 22rpx;
|
||
color: #c45f17;
|
||
}
|
||
</style>
|