优化移动端即梦页面

This commit is contained in:
RockYang
2025-08-07 22:27:09 +08:00
parent e456210944
commit 4e237c9560
14 changed files with 1906 additions and 1239 deletions

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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