mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-21 10:34:26 +08:00
优化移动端即梦页面
This commit is contained in:
706
web/src/views/mobile/JimengCreate.vue
Normal file
706
web/src/views/mobile/JimengCreate.vue
Normal file
@@ -0,0 +1,706 @@
|
||||
<template>
|
||||
<div class="jimeng-create">
|
||||
<!-- 页面头部 -->
|
||||
<div class="jimeng-create__header">
|
||||
<div class="jimeng-create__header-content">
|
||||
<button @click="goBack" class="jimeng-create__header-back-btn">
|
||||
<i class="iconfont icon-back"></i>
|
||||
</button>
|
||||
<h1 class="jimeng-create__header-title">即梦AI</h1>
|
||||
<div class="jimeng-create__header-spacer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能分类选择 -->
|
||||
<div class="jimeng-create__content">
|
||||
<div class="jimeng-create__category-section">
|
||||
<CustomTabs
|
||||
v-model="jimengStore.activeCategory"
|
||||
@update:modelValue="jimengStore.switchCategory"
|
||||
>
|
||||
<CustomTabPane
|
||||
v-for="category in jimengStore.categories"
|
||||
:key="category.key"
|
||||
:label="category.name"
|
||||
:name="category.key"
|
||||
>
|
||||
<template #label>
|
||||
<span>{{ category.name }}</span>
|
||||
</template>
|
||||
</CustomTabPane>
|
||||
</CustomTabs>
|
||||
</div>
|
||||
<!-- 生成模式切换 -->
|
||||
<div
|
||||
v-if="
|
||||
jimengStore.activeCategory === 'image_generation' ||
|
||||
jimengStore.activeCategory === 'video_generation'
|
||||
"
|
||||
class="jimeng-create__mode-section"
|
||||
>
|
||||
<div class="jimeng-create__mode-section-content">
|
||||
<div>
|
||||
<span class="jimeng-create__mode-section-title">生成模式</span>
|
||||
<p class="jimeng-create__mode-section-description">
|
||||
{{
|
||||
jimengStore.activeCategory === 'image_generation' ? '图生图人像写真' : '图生视频'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="jimengStore.useImageInput"
|
||||
@change="jimengStore.switchInputMode"
|
||||
size="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文生图 -->
|
||||
<div v-if="jimengStore.activeFunction === 'text_to_image'" class="space-y-6">
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="请输入图片描述,越详细越好"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.textToImageParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="图片尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
|
||||
<!-- 创意度 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__slider-section">
|
||||
<div class="jimeng-create__slider-section-header">
|
||||
<label>创意度</label>
|
||||
<el-tooltip content="创意度越高,影响文本描述的程度越高" placement="top">
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-slider
|
||||
v-model="jimengStore.textToImageParams.scale"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能优化提示词 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__switch-section">
|
||||
<span>智能优化提示词</span>
|
||||
<el-switch v-model="jimengStore.textToImageParams.use_pre_llm" size="default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生图 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_to_image'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageToImageInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageToImageInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageToImageParams.image_input.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageToImageParams.image_input.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageToImageParams.image_input[0]?.url ||
|
||||
jimengStore.imageToImageParams.image_input[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageToImageParams.image_input = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的图片效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageToImageParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="图片尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图像编辑 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_edit'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageEditInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageEditInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageEditParams.image_urls.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageEditParams.image_urls.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageEditParams.image_urls[0]?.url ||
|
||||
jimengStore.imageEditParams.image_urls[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageEditParams.image_urls = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑提示词 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">编辑提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的编辑效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑强度 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__slider-section">
|
||||
<label>编辑强度</label>
|
||||
<el-slider v-model="jimengStore.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像特效 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_effects'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageEffectsInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageEffectsInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageEffectsParams.image_input1.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageEffectsParams.image_input1.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageEffectsParams.image_input1[0]?.url ||
|
||||
jimengStore.imageEffectsParams.image_input1[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageEffectsParams.image_input1 = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 特效模板 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageEffectsParams.template_id"
|
||||
:options="
|
||||
jimengStore.imageEffectsTemplateOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="特效模板"
|
||||
title="选择特效模板"
|
||||
/>
|
||||
|
||||
<!-- 输出尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageEffectsParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="输出尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文生视频 -->
|
||||
<div v-if="jimengStore.activeFunction === 'text_to_video'" class="space-y-6">
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的视频内容"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频比例 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.textToVideoParams.aspect_ratio"
|
||||
:options="
|
||||
jimengStore.videoAspectRatioOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="视频比例"
|
||||
title="选择比例"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图生视频 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_to_video'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片(最多2张)</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageToVideoInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
multiple
|
||||
@change="(e) => jimengStore.handleMultipleImageUpload(e)"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageToVideoInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageToVideoParams.image_urls.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageToVideoParams.image_urls.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-multiple">
|
||||
<div
|
||||
v-for="(image, index) in jimengStore.imageToVideoParams.image_urls"
|
||||
:key="index"
|
||||
class="jimeng-create__upload-multiple-item"
|
||||
>
|
||||
<el-image
|
||||
:src="image?.url || image?.content"
|
||||
fit="cover"
|
||||
class="w-24 h-24 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.removeImage(index)"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="jimengStore.imageToVideoParams.image_urls.length < 2"
|
||||
@click.stop="$refs.imageToVideoInput?.click()"
|
||||
class="jimeng-create__upload-multiple-add"
|
||||
>
|
||||
<i class="iconfont icon-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的视频效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频比例 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageToVideoParams.aspect_ratio"
|
||||
:options="
|
||||
jimengStore.videoAspectRatioOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="视频比例"
|
||||
title="选择比例"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="jimeng-create__submit-btn">
|
||||
<button @click="jimengStore.submitTask" :disabled="jimengStore.submitting">
|
||||
<i v-if="jimengStore.submitting" class="iconfont icon-loading animate-spin"></i>
|
||||
<span>{{
|
||||
jimengStore.submitting ? '创作中...' : `立即生成 (${jimengStore.currentPowerCost}算力)`
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作品列表 -->
|
||||
<div class="jimeng-create__works">
|
||||
<h2 class="jimeng-create__works-title">我的作品</h2>
|
||||
<div class="jimeng-create__works-list space-y-4">
|
||||
<div
|
||||
v-for="item in jimengStore.currentList"
|
||||
:key="item.id"
|
||||
class="jimeng-create__works-item"
|
||||
>
|
||||
<div class="jimeng-create__works-item-content">
|
||||
<div class="jimeng-create__works-item-thumb">
|
||||
<div class="jimeng-create__works-item-thumb-container">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
fit="cover"
|
||||
class="w-full h-full"
|
||||
:preview-disabled="true"
|
||||
>
|
||||
<template #error>
|
||||
<div class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i class="iconfont icon-image"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image
|
||||
v-else-if="item.video_url"
|
||||
:src="item.video_url"
|
||||
fit="cover"
|
||||
class="w-full h-full"
|
||||
:preview-disabled="true"
|
||||
>
|
||||
<template #error>
|
||||
<div class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i class="iconfont icon-video"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i
|
||||
:class="
|
||||
item.type.includes('video') ? 'iconfont icon-video' : 'iconfont icon-image'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
<!-- 播放/查看按钮 -->
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.playMedia(item)"
|
||||
class="jimeng-create__works-item-thumb-overlay"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'
|
||||
"
|
||||
></i>
|
||||
</button>
|
||||
<!-- 进度动画 -->
|
||||
<div
|
||||
v-if="item.status === 'in_queue' || item.status === 'generating'"
|
||||
class="jimeng-create__works-item-thumb-status jimeng-create__works-item-thumb-status--loading"
|
||||
>
|
||||
<i class="iconfont icon-loading animate-spin"></i>
|
||||
</div>
|
||||
<!-- 失败状态 -->
|
||||
<div
|
||||
v-if="item.status === 'failed'"
|
||||
class="jimeng-create__works-item-thumb-status jimeng-create__works-item-thumb-status--failed"
|
||||
>
|
||||
<i class="iconfont icon-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jimeng-create__works-item-info">
|
||||
<div class="jimeng-create__works-item-info-header">
|
||||
<div class="flex-1">
|
||||
<h3 class="jimeng-create__works-item-info-title">
|
||||
{{ jimengStore.getFunctionName(item.type) }}
|
||||
</h3>
|
||||
<p class="jimeng-create__works-item-info-prompt line-clamp-2">
|
||||
{{ item.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 任务状态 -->
|
||||
<div
|
||||
v-if="item.status !== 'completed'"
|
||||
class="jimeng-create__works-item-info-status"
|
||||
>
|
||||
<div
|
||||
v-if="item.status === 'failed'"
|
||||
class="jimeng-create__works-item-info-status--failed"
|
||||
>
|
||||
<i class="iconfont icon-warning"></i>
|
||||
<span>失败</span>
|
||||
</div>
|
||||
<div v-else class="jimeng-create__works-item-info-status--processing">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>生成中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签 -->
|
||||
<div class="jimeng-create__works-item-info-tags">
|
||||
<span
|
||||
:class="[
|
||||
'jimeng-create__works-item-info-tags-item',
|
||||
jimengStore.getTaskType(item.type) === 'warning'
|
||||
? 'jimeng-create__works-item-info-tags-item--warning'
|
||||
: 'jimeng-create__works-item-info-tags-item--primary',
|
||||
]"
|
||||
>
|
||||
{{ jimengStore.getFunctionName(item.type) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.power"
|
||||
class="jimeng-create__works-item-info-tags-item jimeng-create__works-item-info-tags-item--power"
|
||||
>
|
||||
{{ item.power }}算力
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="jimeng-create__works-item-actions">
|
||||
<div class="jimeng-create__works-item-actions-left">
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.playMedia(item)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--primary"
|
||||
>
|
||||
<i
|
||||
:class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'"
|
||||
></i>
|
||||
<span>{{ item.type.includes('video') ? '播放' : '查看' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.downloadFile(item)"
|
||||
:disabled="item.downloading"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--success"
|
||||
>
|
||||
<i v-if="item.downloading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-download"></i>
|
||||
<span>{{ item.downloading ? '下载中...' : '下载' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status === 'failed'"
|
||||
@click="jimengStore.retryTask(item.id)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--warning"
|
||||
>
|
||||
<i class="iconfont icon-refresh"></i>
|
||||
<span>重试</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="jimengStore.removeJob(item)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--danger"
|
||||
>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="jimengStore.listLoading" class="jimeng-create__works-loading">
|
||||
<i class="iconfont icon-loading animate-spin"></i>
|
||||
</div>
|
||||
|
||||
<!-- 没有更多了 -->
|
||||
<div
|
||||
v-if="jimengStore.listFinished && !jimengStore.listLoading"
|
||||
class="jimeng-create__works-finished"
|
||||
>
|
||||
没有更多了
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 媒体预览弹窗 -->
|
||||
<div
|
||||
v-if="jimengStore.showMediaDialog"
|
||||
class="jimeng-create__media-dialog"
|
||||
@click="jimengStore.closeMediaDialog"
|
||||
>
|
||||
<div @click.stop class="jimeng-create__media-dialog-content animate-scale-up">
|
||||
<div class="jimeng-create__media-dialog-header">
|
||||
<h3>媒体预览</h3>
|
||||
<button @click="jimengStore.closeMediaDialog">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="jimeng-create__media-dialog-body">
|
||||
<img
|
||||
v-if="jimengStore.currentMediaUrl && !jimengStore.currentMediaUrl.includes('video')"
|
||||
:src="jimengStore.currentMediaUrl"
|
||||
class="w-full max-h-[60vh] object-contain rounded-lg"
|
||||
/>
|
||||
<video
|
||||
v-else-if="jimengStore.currentMediaUrl"
|
||||
:src="jimengStore.currentMediaUrl"
|
||||
controls
|
||||
autoplay
|
||||
class="w-full max-h-[60vh] rounded-lg"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useJimengStore } from '@/store/mobile/jimeng'
|
||||
import CustomSelect from '@/components/mobile/CustomSelect.vue'
|
||||
import CustomTabs from '@/components/ui/CustomTabs.vue'
|
||||
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
|
||||
const router = useRouter()
|
||||
const jimengStore = useJimengStore()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
jimengStore.fetchData(1)
|
||||
jimengStore.startTaskPolling()
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
jimengStore.stopTaskPolling()
|
||||
})
|
||||
|
||||
// 工具方法
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/css/mobile/jimeng.scss';
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<van-cell class="app-cell">
|
||||
<div class="app-card">
|
||||
<div class="app-info">
|
||||
<div class="app-image">
|
||||
<van-image :src="app.icon" round />
|
||||
</div>
|
||||
<div class="app-detail">
|
||||
<div class="app-title">{{ app.name }}</div>
|
||||
<div class="app-desc">{{ app.hello_msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="$emit('use-role', app.id)"
|
||||
>开始对话</van-button
|
||||
>
|
||||
<van-button
|
||||
size="small"
|
||||
:type="hasRole ? 'danger' : 'success'"
|
||||
class="action-btn"
|
||||
@click="$emit('update-role', app, hasRole ? 'remove' : 'add')"
|
||||
>
|
||||
{{ hasRole ? '移出工作台' : '添加到工作台' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
app: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasRole: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['use-role', 'update-role'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-cell {
|
||||
padding: 0;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.app-card {
|
||||
background: var(--van-cell-background);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.app-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.app-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 15px;
|
||||
|
||||
:deep(.van-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.app-detail {
|
||||
flex: 1;
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 13px;
|
||||
color: var(--van-gray-6);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
border-radius: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="custom-select">
|
||||
<label v-if="label" class="block text-gray-700 font-medium mb-2">{{ label }}</label>
|
||||
<button
|
||||
@click="showPicker = true"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<span class="text-gray-900">{{ selectedLabel || placeholder || '请选择' }}</span>
|
||||
<i class="iconfont icon-arrow-down text-gray-400"></i>
|
||||
</button>
|
||||
|
||||
<!-- 选择器弹窗 -->
|
||||
<div
|
||||
v-if="showPicker"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-50"
|
||||
@click="showPicker = false"
|
||||
>
|
||||
<div @click.stop class="bg-white rounded-t-2xl w-full max-w-md animate-slide-up">
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ title || '请选择' }}</h3>
|
||||
<button @click="showPicker = false" class="p-2 hover:bg-gray-100 rounded-full">
|
||||
<i class="iconfont icon-close text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
<CustomSelectOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:option="option"
|
||||
:selected="modelValue === option.value"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<slot name="option" v-bind="slotProps">
|
||||
<div>
|
||||
<span class="text-gray-900 font-medium">{{ slotProps.option.label }}</span>
|
||||
<p v-if="slotProps.option.desc" class="text-sm text-gray-500 mt-1">
|
||||
{{ slotProps.option.desc }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="slotProps.selected" class="text-blue-600">
|
||||
<i class="iconfont icon-success"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</CustomSelectOption>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import CustomSelectOption from './CustomSelectOption.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
},
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
// Data
|
||||
const showPicker = ref(false)
|
||||
|
||||
// Computed
|
||||
const selectedLabel = computed(() => {
|
||||
const selected = props.options.find((option) => option.value === props.modelValue)
|
||||
return selected ? selected.label : ''
|
||||
})
|
||||
|
||||
// Methods
|
||||
const onSelect = (option) => {
|
||||
emit('update:modelValue', option.value)
|
||||
emit('change', option)
|
||||
showPicker.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<!--
|
||||
CustomSelectOption 组件
|
||||
Props:
|
||||
- option: 选项对象,必需,包含 label/desc/value 等属性
|
||||
- selected: 是否为当前选中项
|
||||
Emits:
|
||||
- select(option): 选中该项时触发
|
||||
Slots:
|
||||
- 默认插槽(default):用于自定义 option 内容,slotProps: { option, selected }
|
||||
示例:
|
||||
<template #option="{ option, selected }">
|
||||
<div>{{ option.label }}</div>
|
||||
<div v-if="selected">✔</div>
|
||||
</template>
|
||||
-->
|
||||
<div
|
||||
class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors border-b last:border-b-0"
|
||||
@click="$emit('select', option)"
|
||||
>
|
||||
<slot :option="option" :selected="selected">
|
||||
<div>
|
||||
<span class="text-gray-900 font-medium">{{ option.label }}</span>
|
||||
<p v-if="option.desc" class="text-sm text-gray-500 mt-1">{{ option.desc }}</p>
|
||||
</div>
|
||||
<div v-if="selected" class="text-blue-600">
|
||||
<i class="iconfont icon-success"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<van-empty :image="getImage()" :description="description" :image-size="imageSize">
|
||||
<template #bottom>
|
||||
<slot name="action">
|
||||
<van-button
|
||||
v-if="showAction"
|
||||
round
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ actionText }}
|
||||
</van-button>
|
||||
</slot>
|
||||
</template>
|
||||
</van-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'search', // search, error, network, default
|
||||
validator: (value) => ['search', 'error', 'network', 'default'].includes(value),
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
imageSize: {
|
||||
type: [String, Number],
|
||||
default: 120,
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '刷新',
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['action'])
|
||||
|
||||
// 根据类型获取对应的图标
|
||||
const getImage = () => {
|
||||
const imageMap = {
|
||||
search: 'search',
|
||||
error: 'error',
|
||||
network: 'network',
|
||||
default: 'default',
|
||||
}
|
||||
return imageMap[props.type] || 'search'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
|
||||
.action-btn {
|
||||
margin-top: 16px;
|
||||
min-width: 120px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user