Files
msh-system/msh_single_uniapp/pages/ai-generate/design.vue

1574 lines
41 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="design-container">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="iconfont icon-fanhui"></text>
</view>
<view class="nav-title">
<text>一键设计</text>
</view>
<view class="nav-right" @click="goToHistory">
<text class="iconfont icon-lishi"></text>
</view>
</view>
<scroll-view scroll-y class="main-content" :style="{ height: scrollViewHeight }">
<!-- 上传空间图片 -->
<view class="section upload-section">
<view class="section-header">
<view class="header-left">
<text class="section-title">上传空间图片</text>
</view>
</view>
<view class="upload-area" @click="chooseImage" v-if="!uploadedImage">
<view class="upload-box">
<view class="camera-icon-box">
<text class="iconfont icon-xiangji"></text>
</view>
<text class="upload-hint">点击上传或拖拽图片到此处</text>
<text class="upload-sub-hint">支持 JPGPNG 格式</text>
</view>
</view>
<view class="preview-area" v-else>
<image :src="uploadedImage" mode="aspectFill" class="preview-image"></image>
<view class="re-upload-btn" @click="chooseImage">
<text class="iconfont icon-huan"></text>
<text>重新上传</text>
</view>
</view>
</view>
<!-- 装修需求描述 -->
<view class="section prompt-section">
<view class="section-header">
<view class="header-left">
<text class="section-title">一句话描述设计需求</text>
</view>
</view>
<view class="input-box">
<textarea
v-model="promptText"
class="prompt-input"
placeholder="请描述希望生成的画面,例如:卧室,现代,双人床..."
placeholder-class="placeholder-text"
:maxlength="200"
:auto-height="true"
></textarea>
<view class="voice-btn" @click="isRecording ? stopVoiceInput() : startVoiceInput()" :class="{ recording: isRecording }">
<text class="iconfont icon-laba" v-if="!isRecording"></text>
<view class="recording-content" v-else>
<view class="recording-wave">
<view class="bar bar1"></view>
<view class="bar bar2"></view>
<view class="bar bar3"></view>
</view>
<text class="recording-time">{{ recordDuration }}s</text>
</view>
</view>
</view>
</view>
<!-- 风格参考图 -->
<view class="section style-section">
<view class="section-header">
<view class="header-left">
<view class="green-bar"></view>
<text class="section-title">风格参考</text>
</view>
<view class="header-right" @click="viewMoreStyles">
<text class="more-text">更多案例库</text>
</view>
</view>
<scroll-view scroll-x class="style-scroll" :show-scrollbar="false">
<view class="style-list">
<!-- 不使用选项 -->
<view
class="style-item"
:class="{ active: selectedStyle === 'none' }"
@click="selectStyle('none')"
>
<view class="style-img-box none-style">
<view class="check-circle" v-if="selectedStyle === 'none'">
<text class="iconfont icon-gou"></text>
</view>
</view>
<text class="style-name">不使用</text>
</view>
<!-- 预设风格 -->
<view
class="style-item"
v-for="(style, index) in styles"
:key="index"
:class="{ active: selectedStyle === style.id }"
@click="selectStyle(style.id)"
>
<view class="style-img-box">
<image :src="style.image" mode="aspectFill" class="style-img"></image>
<view class="check-circle" v-if="selectedStyle === style.id">
<text class="iconfont icon-gou"></text>
</view>
</view>
<text class="style-name">{{ style.name }}</text>
</view>
<!-- 自定义上传 -->
<view class="style-item" @click="uploadCustomStyle">
<view class="style-img-box custom-style">
<text class="iconfont icon-shangchuan"></text>
</view>
<text class="style-name">自定义上传</text>
</view>
</view>
</scroll-view>
</view>
<!-- 生成模式 -->
<view class="section mode-section">
<view class="section-header">
<view class="header-left">
<view class="green-bar"></view>
<text class="section-title">生成模式</text>
</view>
</view>
<view class="mode-options">
<view
class="mode-btn"
:class="{ active: generateMode === 'try' }"
@click="selectMode('try')"
>
<view class="check-icon" v-if="generateMode === 'try'">
<text class="iconfont icon-gou"></text>
</view>
<view class="radio-circle" v-else></view>
<text class="mode-text">标准</text>
</view>
<view
class="mode-btn"
:class="{ active: generateMode === 'inspiration' }"
@click="selectMode('inspiration')"
>
<view class="check-icon" v-if="generateMode === 'inspiration'">
<text class="iconfont icon-gou"></text>
</view>
<view class="radio-circle" v-else></view>
<text class="mode-text">创意</text>
</view>
</view>
</view>
<view class="bottom-placeholder" style="height: 120px;"></view>
</scroll-view>
<!-- 底部按钮 -->
<view class="footer-bar">
<button class="submit-btn" @click="handleGenerate">
<text class="btn-icon"></text>
<text class="btn-text">开始生成</text>
</button>
</view>
<!-- 自定义加载提示 -->
<view class="custom-loading-mask" v-if="showCustomLoading">
<view class="custom-loading-box">
<view class="loading-spinner">
<view class="spinner-circle"></view>
</view>
<text class="loading-text">{{ loadingText }}</text>
<text class="countdown-text" v-if="countdownSeconds > 0">预计还需 {{ countdownSeconds }} </text>
</view>
</view>
</view>
</template>
<script>
import api from '@/api/models-api.js'
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['userInfo','uid'])
},
data() {
return {
statusBarHeight: 0,
scrollViewHeight: '100vh',
uploadedImage: '',
promptText: '',
isRecording: false,
selectedStyle: 'none',
styles: [
{ id: 'cream', name: '奶油风', image: 'https://uthink2025.oss-cn-shanghai.aliyuncs.com//crmebimage/public/content/2025/07/29/368cb3967d2542479bfeaae31d663093zahdxpvd4z.jpg', prompt: '奶油风格装修,温馨舒适的居住空间,柔和的米白色调为主,搭配浅色木质家具,简约优雅的设计,温暖的光线氛围' },
{ id: 'nordic', name: '北欧风', image: 'https://uthink2025.oss-cn-shanghai.aliyuncs.com//crmebimage/public/content/2025/07/29/368cb3967d2542479bfeaae31d663093zahdxpvd4z.jpg', prompt: '北欧风格装修,简约自然的设计理念,浅色木质地板和家具,白色墙面,清新明亮的空间,绿植装饰,舒适宜居' },
{ id: 'modern', name: '现代简约', image: 'https://uthink2025.oss-cn-shanghai.aliyuncs.com//crmebimage/public/content/2025/07/29/368cb3967d2542479bfeaae31d663093zahdxpvd4z.jpg', prompt: '现代简约风格装修,时尚前卫的设计,黑白灰色调为主,线条流畅简洁,金属元素点缀,宽敞明亮的空间布局' },
],
generateMode: 'try', // 'try' or 'inspiration'
// 录音相关
recorderManager: null,
recordTimer: null,
recordDuration: 0,
// 生成动态提示相关
loadingTimer: null,
loadingTextIndex: 0,
showCustomLoading: false,
loadingText: '正在分析图片...',
countdownTimer: null,
countdownSeconds: 20,
};
},
onLoad(options) {
// 检查用户是否登录
if (!this.uid) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/users/wechat_login/index'
});
}, 1500);
return;
}
this.initPage();
// 接收参数(支持从其他页面跳转过来时预填数据)
if (options && options.beforeImage) {
this.uploadedImage = decodeURIComponent(options.beforeImage);
}
if (options && options.promptText) {
this.promptText = decodeURIComponent(options.promptText);
}
// 初始化录音管理器
// #ifdef MP-WEIXIN || APP-PLUS
this.initRecorder();
// #endif
},
onShow() {
// 检查用户是否登录
if (!this.uid) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/users/wechat_login/index'
});
}, 1500);
return;
}
},
onUnload() {
// 清理录音相关资源
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
// 清理生成动态提示定时器
if (this.loadingTimer) {
clearInterval(this.loadingTimer);
this.loadingTimer = null;
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
// #ifdef MP-WEIXIN || APP-PLUS
if (this.recorderManager) {
this.recorderManager.stop();
}
// #endif
},
methods: {
initPage() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
const navHeight = 44;
const footerHeight = 100; // 预留底部按钮区域
this.scrollViewHeight = `calc(100vh - ${this.statusBarHeight + navHeight + footerHeight}px)`;
},
// 初始化录音管理器
initRecorder() {
// #ifdef MP-WEIXIN || APP-PLUS
this.recorderManager = uni.getRecorderManager();
// 录音开始事件
this.recorderManager.onStart(() => {
console.log('录音开始');
this.recordDuration = 0;
this.startRecordTimer();
});
// 录音结束事件
this.recorderManager.onStop((res) => {
console.log('录音结束', res);
this.isRecording = false; // 重置录音状态
this.stopRecordTimer();
if (res.duration < 1000) {
uni.showToast({
title: '录音时间太短',
icon: 'none'
});
return;
}
// 处理录音结果
this.handleRecordResult(res);
});
// 录音错误事件
this.recorderManager.onError((err) => {
console.error('录音错误:', err);
this.isRecording = false;
this.stopRecordTimer();
// 如果错误是因为已经在录音或暂停,先停止再提示
if (err.errMsg && (err.errMsg.includes('recording') || err.errMsg.includes('paused'))) {
console.log('检测到录音状态冲突,停止录音管理器');
try {
this.recorderManager.stop();
} catch (e) {
console.error('停止录音失败:', e);
}
uni.showToast({
title: '录音状态异常,已重置',
icon: 'none',
duration: 2000
});
return;
}
// 根据错误类型给出更详细的提示
let errorMsg = '录音失败,请重试';
if (err.errMsg) {
if (err.errMsg.includes('permission') || err.errMsg.includes('权限')) {
errorMsg = '录音权限被拒绝,请在设置中开启';
} else if (err.errMsg.includes('busy') || err.errMsg.includes('忙碌')) {
errorMsg = '录音功能正忙,请稍后再试';
} else if (err.errMsg.includes('system') || err.errMsg.includes('系统')) {
errorMsg = '系统录音功能异常';
}
}
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
});
// #endif
// #ifdef H5
// H5 环境提示
console.log('H5环境暂不支持录音功能');
// #endif
},
// 开始录音计时
startRecordTimer() {
this.recordTimer = setInterval(() => {
this.recordDuration++;
if (this.recordDuration >= 60) {
// 录音超过60秒自动停止
this.stopVoiceInput();
}
}, 1000);
},
// 停止录音计时
stopRecordTimer() {
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
this.recordDuration = 0;
},
goBack() {
uni.navigateBack();
},
goToHistory() {
uni.navigateTo({
url: '/pages/ai-generate/history'
});
},
async chooseImage() {
try {
const res = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: reject
});
});
// 上传图片到服务器
uni.showLoading({
title: '上传中...',
mask: true
});
const uploadResult = await api.uploadFile(res.tempFilePaths[0], {
model: 'user',
pid: '7'
});
uni.hideLoading();
if (uploadResult && uploadResult.code === 200) {
console.log('图片上传成功:', uploadResult.data);
this.uploadedImage = uploadResult.data.fullUrl;
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
});
} else {
throw new Error(uploadResult?.message || '上传失败');
}
} catch (error) {
uni.hideLoading();
console.error('图片选择或上传失败:', error);
if (error.errMsg && error.errMsg.includes('cancel')) {
// 用户取消选择,不显示错误提示
return;
}
uni.showToast({
title: error.message || '图片上传失败',
icon: 'none',
duration: 2000
});
}
},
// 开始语音输入
startVoiceInput() {
// #ifdef H5
uni.showModal({
title: '提示',
content: 'H5环境暂不支持语音输入请使用小程序或APP',
showCancel: false
});
return;
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
if (!this.recorderManager) {
console.error('录音管理器未初始化');
// 尝试重新初始化
this.initRecorder();
if (!this.recorderManager) {
uni.showToast({
title: '录音功能初始化失败',
icon: 'none',
duration: 2000
});
return;
}
}
// 先检查权限状态
uni.getSetting({
success: (res) => {
const recordAuth = res.authSetting['scope.record'];
if (recordAuth === true) {
// 已有权限,直接开始录音
console.log('已有录音权限,开始录音');
this.startRecording();
} else if (recordAuth === false) {
// 用户已拒绝过权限,需要引导到设置页面
uni.showModal({
title: '需要录音权限',
content: '请在设置中开启录音权限',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.record']) {
// 用户开启了权限,重新尝试录音
console.log('用户已开启录音权限,开始录音');
this.startRecording();
} else {
uni.showToast({
title: '需要开启录音权限',
icon: 'none'
});
}
}
});
}
}
});
} else {
// 未请求过权限,请求权限
uni.authorize({
scope: 'scope.record',
success: () => {
console.log('录音权限获取成功');
this.startRecording();
},
fail: (err) => {
console.error('录音权限获取失败:', err);
uni.showModal({
title: '需要录音权限',
content: '请在设置中开启录音权限',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.record']) {
console.log('用户已开启录音权限,开始录音');
this.startRecording();
}
}
});
}
}
});
}
});
}
},
fail: (err) => {
console.error('获取权限设置失败:', err);
// 降级方案:直接尝试请求权限
uni.authorize({
scope: 'scope.record',
success: () => {
console.log('录音权限获取成功');
this.startRecording();
},
fail: () => {
uni.showModal({
title: '需要录音权限',
content: '请在设置中开启录音权限',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting();
}
}
});
}
});
}
});
// #endif
},
// 开始录音
startRecording() {
if (!this.recorderManager) {
console.error('录音管理器不存在,无法开始录音');
uni.showToast({
title: '录音功能未初始化',
icon: 'none'
});
return;
}
// 如果已经在录音,先停止再重新开始
if (this.isRecording) {
console.log('检测到正在录音,先停止再重新开始');
this.stopVoiceInput();
// 等待停止完成后再开始
setTimeout(() => {
this.startRecording();
}, 300);
return;
}
this.isRecording = true;
uni.showToast({
title: '开始录音',
icon: 'none',
duration: 1000
});
// #ifdef MP-WEIXIN || APP-PLUS
try {
// 开始录音 - 根据平台使用不同参数
let recordOptions = {};
// #ifdef MP-WEIXIN
recordOptions = {
duration: 60000, // 最长录音60秒
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
};
// #endif
this.recorderManager.start(recordOptions);
console.log('录音开始命令已发送,参数:', recordOptions);
} catch (error) {
console.error('启动录音失败:', error);
this.isRecording = false;
// 如果错误是因为已经在录音,先停止再重试
if (error.errMsg && error.errMsg.includes('recording')) {
console.log('检测到录音状态冲突,先停止再重试');
this.recorderManager.stop();
setTimeout(() => {
this.startRecording();
}, 300);
} else {
uni.showToast({
title: '启动录音失败,请重试',
icon: 'none',
duration: 2000
});
}
}
// #endif
},
// 停止语音输入
stopVoiceInput() {
if (!this.isRecording) {
return;
}
this.isRecording = false;
this.stopRecordTimer(); // 停止计时器
// #ifdef MP-WEIXIN || APP-PLUS
if (this.recorderManager) {
this.recorderManager.stop();
}
// #endif
uni.showToast({
title: '正在识别...',
icon: 'loading',
duration: 2000
});
},
// 处理录音结果
async handleRecordResult(res) {
console.log('录音文件路径:', res.tempFilePath);
console.log('录音时长:', res.duration, 'ms');
try {
// 调用语音识别API
const recognizedText = await this.recognizeSpeech(res.tempFilePath);
if (recognizedText) {
// 将识别结果添加到描述中
this.promptText = recognizedText;
uni.showToast({
title: '识别成功',
icon: 'success',
duration: 1500
});
}
} catch (error) {
console.error('语音识别失败:', error);
uni.showToast({
title: '识别失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 语音识别(对接腾讯云语音识别服务)
async recognizeSpeech(audioPath) {
try {
// 1. 上传录音文件到服务器
console.log('开始上传录音文件:', audioPath);
const uploadRes = await api.uploadFile(audioPath, {
model: 'audio',
pid: '8'
});
if (!uploadRes || uploadRes.code !== 200) {
throw new Error('录音文件上传失败');
}
const audioUrl = uploadRes.data.fullUrl;
console.log('录音文件上传成功URL:', audioUrl);
// 2. 创建语音识别任务
console.log('创建语音识别任务...');
const createTaskRes = await api.createAsrTask({
url: audioUrl,
engineModelType: '16k_zh',
channelNum: 1,
resTextFormat: 0,
sourceType: 0
});
if (!createTaskRes || createTaskRes.code !== 200) {
throw new Error('创建识别任务失败');
}
const taskId = createTaskRes.data.taskId;
console.log('识别任务创建成功任务ID:', taskId);
// 3. 轮询查询识别结果
const result = await this.pollAsrResult(taskId);
// 4. 解析识别文本
const text = this.parseAsrResult(result);
if (!text) {
throw new Error('识别结果为空');
}
console.log('语音识别成功,结果:', text);
return text;
} catch (error) {
console.error('语音识别失败:', error);
throw error;
}
},
// 轮询查询ASR识别结果
async pollAsrResult(taskId, maxAttempts = 30, interval = 2000) {
let attempts = 0;
while (attempts < maxAttempts) {
attempts++;
console.log(`查询识别状态,第${attempts}次尝试...`);
try {
const statusRes = await api.queryAsrStatus(taskId);
if (!statusRes || statusRes.code !== 200) {
throw new Error('查询识别状态失败');
}
const status = statusRes.data.status;
const statusStr = statusRes.data.statusStr;
console.log(`任务状态: ${statusStr} (${status})`);
// status: 0-等待处理, 1-处理中, 2-识别成功, 3-识别失败
if (status === 2) {
// 识别成功
console.log('识别成功!');
return statusRes.data;
} else if (status === 3) {
// 识别失败
throw new Error(statusRes.data.errorMsg || '识别失败');
}
// 等待一段时间后继续查询
await new Promise(resolve => setTimeout(resolve, interval));
} catch (error) {
console.error('查询识别状态出错:', error);
// 如果不是最后一次尝试,继续重试
if (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, interval));
continue;
}
throw error;
}
}
throw new Error('识别超时,请重试');
},
// 解析ASR识别结果文本
parseAsrResult(data) {
if (!data || !data.result) {
return '';
}
let text = data.result;
// 去除时间戳标记,例如:[0:0.000,0:11.580]
text = text.replace(/\[\d+:\d+\.\d+,\d+:\d+\.\d+\]\s*/g, '');
// 去除多余的空格和换行
text = text.replace(/\n+/g, ' ').trim();
// 去除多余的空格
text = text.replace(/\s+/g, ' ');
return text;
},
viewMoreStyles() {
uni.navigateTo({
url: '/pages/ai-generate/inspiration'
});
},
selectStyle(id) {
this.selectedStyle = id;
// 选择风格时如果有对应的prompt则赋值给promptText
if (id !== 'none') {
const selectedStyleObj = this.styles.find(s => s.id === id);
if (selectedStyleObj && selectedStyleObj.prompt) {
// 如果当前promptText为空或很短则替换否则追加
if (!this.promptText || this.promptText.length < 10) {
this.promptText = selectedStyleObj.prompt;
} else {
this.promptText = selectedStyleObj.prompt;
}
}
}
},
uploadCustomStyle() {
uni.chooseImage({
count: 1,
success: (res) => {
uni.showToast({ title: '已选择自定义风格', icon: 'success' });
// Logic to add custom style to list or set as active
}
});
},
selectMode(mode) {
this.generateMode = mode;
},
async handleGenerate() {
// 验证
if (!this.uploadedImage) {
uni.showToast({
title: '请先上传空间图片',
icon: 'none'
});
return;
}
if (!this.promptText.trim()) {
uni.showToast({
title: '请输入装修需求描述',
icon: 'none'
});
return;
}
try {
// 启动动态提示
this.startDynamicLoading();
// 准备参考图数组
const referenceImages = [this.uploadedImage];
// 如果选择了风格参考图,添加到参考图数组
if (this.selectedStyle !== 'none') {
const selectedStyleObj = this.styles.find(s => s.id === this.selectedStyle);
if (selectedStyleObj && selectedStyleObj.image) {
referenceImages.push(selectedStyleObj.image);
}
}
// 获取用户信息
const userInfo = this.userInfo || {};
const avatar = userInfo.avatar || '';
const nickname = userInfo.nickname || '';
const uid = this.uid || '';
const resolution = '2K';
// 根据生成模式选择不同的API
let response;
if (this.generateMode === 'inspiration') {
// 专业模式:调用 createImageEditTaskPro
response = await api.createImageEditTaskPro({
prompt: this.promptText,
image_urls: referenceImages,
output_format: 'png',
aspect_ratio: '9:16',
resolution: resolution,
title: this.promptText,
avatar: avatar,
nickname: nickname,
uid: uid
});
} else {
// 普通模式:调用 createImageEditTask
response = await api.createImageEditTask({
prompt: this.promptText,
image_urls: referenceImages,
output_format: 'png',
image_size: '9:16',
// aspect_ratio: '9:16',
// resolution: resolution,
title: this.promptText,
mode: this.generateMode,
avatar: avatar,
nickname: nickname,
uid: uid
});
}
console.log('图片编辑任务创建成功:', response);
// 停止动态提示
this.stopDynamicLoading();
// 等待任务完成,轮询获取最新作品
if (response && response.code === 200) {
//
if (response.data) {
// 跳转到作品详情页
uni.navigateTo({
// url: `/pages/ai-generate/image?id=${response.data}`,
url: `/pages/ai-generate/history`,
success: () => {
uni.showToast({
title: '生成成功',
icon: 'success'
});
}
});
} else {
uni.showToast({
title: '任务已提交,请稍后查看结果',
icon: 'none',
duration: 2000
});
}
} else {
throw new Error(response?.message || '任务创建失败');
}
} catch (error) {
console.error('图片编辑任务创建失败:', error);
this.stopDynamicLoading();
uni.showToast({
title: error.message || '生成失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 启动动态加载提示
startDynamicLoading() {
const loadingTexts = [
'正在分析图片...',
'正在生成设计方案...',
'正在优化细节...',
'努力加油中...',
'即将完成...'
];
this.loadingTextIndex = 0;
this.loadingText = loadingTexts[0];
this.countdownSeconds = 20; // 初始化倒计时20秒
this.showCustomLoading = true;
// 定时更新提示文本
this.loadingTimer = setInterval(() => {
this.loadingTextIndex = (this.loadingTextIndex + 1) % loadingTexts.length;
this.loadingText = loadingTexts[this.loadingTextIndex];
}, 1500); // 每1.5秒切换一次提示
// 启动倒计时
this.countdownTimer = setInterval(() => {
if (this.countdownSeconds > 0) {
this.countdownSeconds--;
} else {
// 倒计时结束,但可能还在加载中,不自动关闭
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
}, 1000); // 每秒更新一次倒计时
},
// 停止动态加载提示
stopDynamicLoading() {
if (this.loadingTimer) {
clearInterval(this.loadingTimer);
this.loadingTimer = null;
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
this.loadingTextIndex = 0;
this.countdownSeconds = 20;
this.showCustomLoading = false;
this.loadingText = '正在分析图片...';
},
}
};
</script>
<style lang="scss" scoped>
.design-container {
width: 100%;
height: 100vh;
background: #ffffff;
color: #333333;
display: flex;
flex-direction: column;
}
.nav-bar {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 48px;
.nav-left, .nav-right {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 500;
gap: 6px;
.title-icon {
color: #42ca4d;
}
}
.iconfont {
color: #333333;
font-size: 20px;
}
}
.main-content {
flex: 1;
padding: 0 16px;
box-sizing: border-box;
}
.section {
margin-bottom: 24px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.header-left {
display: flex;
align-items: center;
gap: 8px;
.header-icon {
font-size: 16px;
}
.green-bar {
width: 4px;
height: 14px;
background: #42ca4d;
border-radius: 2px;
}
.section-title {
font-size: 15px;
font-weight: 500;
color: #333333;
}
}
.header-right {
.more-text {
font-size: 12px;
color: #42ca4d;
}
}
}
}
// 上传区域
.upload-section {
.upload-area {
width: 100%;
height: 200px;
background: #f5f5f5;
border: 1px dashed #ddd;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
.upload-box {
display: flex;
flex-direction: column;
align-items: center;
.camera-icon-box {
width: 64px;
height: 64px;
border: 2px solid #42ca4d;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
.icon-xiangji {
font-size: 32px;
color: #42ca4d;
}
}
.upload-hint {
font-size: 14px;
color: #666666;
margin-bottom: 8px;
}
.upload-sub-hint {
font-size: 12px;
color: #999999;
}
}
}
.preview-area {
width: 100%;
height: 240px;
border-radius: 16px;
overflow: hidden;
position: relative;
border: 1px solid #e0e0e0;
.preview-image {
width: 100%;
height: 100%;
}
.re-upload-btn {
position: absolute;
bottom: 16px;
right: 16px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 4px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text {
font-size: 12px;
color: #333;
}
}
}
}
// 描述区域
.prompt-section {
.input-box {
background: #f5f5f5;
border-radius: 16px;
padding: 16px;
position: relative;
min-height: 100px;
border: 1px solid #e0e0e0;
.prompt-input {
width: 100%;
font-size: 14px;
color: #333;
line-height: 1.5;
min-height: 60px;
}
.placeholder-text {
color: #999;
}
.voice-btn {
position: absolute;
bottom: 12px;
right: 12px;
width: 36px;
height: 36px;
background: rgba(66, 202, 77, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #42ca4d;
.icon-laba {
color: #42ca4d;
font-size: 18px;
}
&.recording {
background: rgba(255, 107, 107, 0.2);
border-color: #ff6b6b;
width: auto;
min-width: 36px;
padding: 0 8px;
.recording-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
.recording-wave {
display: flex;
align-items: center;
gap: 2px;
height: 12px;
.bar {
width: 3px;
background: #ff6b6b;
border-radius: 2px;
animation: wave 1s infinite;
&.bar1 { height: 60%; animation-delay: 0s; }
&.bar2 { height: 100%; animation-delay: 0.2s; }
&.bar3 { height: 60%; animation-delay: 0.4s; }
}
}
.recording-time {
font-size: 10px;
color: #ff6b6b;
font-weight: 500;
line-height: 1;
}
}
}
}
}
}
@keyframes wave {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(0.5); }
}
// 风格选择
.style-section {
.style-scroll {
width: 100%;
white-space: nowrap;
}
.style-list {
display: flex;
gap: 12px;
padding-right: 16px;
}
.style-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
.style-img-box {
width: 80px;
height: 80px;
border-radius: 12px;
overflow: hidden;
position: relative;
border: 2px solid transparent;
background: #f5f5f5;
&.none-style {
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
}
&.custom-style {
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
.icon-shangchuan {
font-size: 24px;
color: #999;
}
}
.style-img {
width: 100%;
height: 100%;
}
.check-circle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
background: #42ca4d;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.icon-gou {
font-size: 14px;
color: #fff;
}
}
}
&.active {
.style-img-box {
border-color: #42ca4d;
background: rgba(66, 202, 77, 0.1);
}
.style-name {
color: #42ca4d;
}
}
.style-name {
font-size: 12px;
color: #666;
}
}
}
// 生成模式
.mode-section {
.mode-options {
display: flex;
gap: 16px;
.mode-btn {
flex: 1;
height: 50px;
background: #f5f5f5;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid #e0e0e0;
.check-icon {
width: 18px;
height: 18px;
background: #42ca4d;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.icon-gou {
font-size: 10px;
color: #fff;
}
}
.radio-circle {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #999;
}
.mode-info {
display: flex;
flex-direction: column;
align-items: flex-start;
.mode-text {
font-size: 14px;
color: #666;
line-height: 1.2;
}
.mode-desc {
font-size: 10px;
color: #999;
margin-top: 2px;
}
}
&.active {
border-color: #42ca4d;
background: rgba(66, 202, 77, 0.1);
.mode-text {
color: #42ca4d;
}
.mode-desc {
color: rgba(66, 202, 77, 0.8);
}
}
}
}
}
.bottom-placeholder {
height: 40px;
}
// 底部操作栏
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px 30px;
background: #ffffff;
border-top: 1px solid #e0e0e0;
z-index: 10;
.submit-btn {
width: 100%;
height: 50px;
background: #42ca4d;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
.btn-icon {
font-size: 18px;
}
.btn-text {
font-size: 16px;
color: #fff;
font-weight: 600;
}
&:active {
opacity: 0.9;
}
}
}
// 自定义加载提示样式
.custom-loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.custom-loading-box {
background: #ffffff;
border-radius: 16px;
padding: 40px 50px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 280px;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.loading-spinner {
width: 50px;
height: 50px;
margin-bottom: 20px;
position: relative;
}
.spinner-circle {
width: 100%;
height: 100%;
border: 4px solid #f0f0f0;
border-top-color: #42ca4d;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.custom-loading-box .loading-text {
font-size: 16px;
color: #333333;
text-align: center;
line-height: 1.5;
margin-bottom: 8px;
}
.countdown-text {
font-size: 14px;
color: #666666;
text-align: center;
margin-top: 8px;
}
</style>