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

1486 lines
39 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="oneclick-container">
<view class="top-controls">
<view class="left-controls">
<view class="back-btn" @click="onBack">
<text class="icon-back"></text>
</view>
</view>
<view class="center-title">
<text class="title-text">视频生成</text>
</view>
</view>
<!-- 参考图片模块 -->
<view class="section-title-row">
<text class="section-title">参考图片一张图</text>
</view>
<view class="tips">
<text class="warning">特别提示图片不可含人物否则视频生成会失败</text>
</view>
<view class="upload-card" v-if="!refImage" @click="chooseImage">
<text class="upload-plus">+</text>
<text class="upload-text">添加参考</text>
</view>
<view class="upload-preview" v-else>
<view class="image-container">
<view class="image-loading" v-if="imageLoading">
<view class="loading-spinner"></view>
<text class="loading-text">图片加载中...</text>
</view>
<image
:src="refImage"
mode="aspectFit"
class="preview-image"
@load="onImageLoad"
@error="onImageError"
:style="{ display: imageLoading ? 'none' : 'block' }"
></image>
<view class="image-error" v-if="imageError" @click="retryLoadImage">
<text class="error-text">图片加载中</text>
<text class="error-retry">点击重试</text>
</view>
</view>
<view class="preview-actions">
<button class="btn-secondary" size="mini" @click="removeImage">移除</button>
<button class="btn-secondary" size="mini" @click="chooseImage">更换</button>
</view>
</view>
<!-- 文案模块 -->
<view class="section-title-outer">
<text class="section-title">创意描述一句话</text>
<view class="desc-row">
<view class="desc-input-wrapper">
<textarea class="desc-input" :value="description" placeholder="请用一句话描述你要生成的视频内容" maxlength="800" @input="onDescInput"></textarea>
<!-- 语音输入按钮 -->
<view class="voice-btn" @click="startVoiceInput" v-if="!isRecording">
<text class="iconfont icon-laba"></text>
</view>
<view class="voice-btn recording" @click="stopVoiceInput" v-else>
<view class="recording-animation"></view>
<text class="recording-time">{{ recordDuration }}s</text>
</view>
</view>
<!-- 录音提示 -->
<view class="recording-hint" v-if="isRecording">
<text class="hint-icon">🎤</text>
<text class="hint-text">正在录音点击右下角完成</text>
</view>
<view class="desc-actions">
<button class="btn-secondary" size="mini" @click="shuffleIdeas">换一组</button>
<button class="btn-secondary" size="mini" @click="clearDescription">清空</button>
</view>
</view>
</view>
<!-- 时长选择 -->
<view class="section">
<text class="section-title">时长选择</text>
<view class="options-row">
<view :class="['option-chip', selectedDuration === 10 ? 'active' : '']" @click="selectDuration" :data-value="10">10</view>
<view :class="['option-chip', selectedDuration === 15 ? 'active' : '']" @click="selectDuration" :data-value="15">15</view>
<view :class="['option-chip', selectedDuration === 25 ? 'active' : '']" @click="selectDuration" :data-value="25">25</view>
</view>
</view>
<!-- 生成比例 -->
<view class="section">
<text class="section-title">生成比例</text>
<view class="options-row">
<view :class="['option-chip', selectedRatio === '9:16' ? 'active' : '']" @click="selectRatio" data-value="9:16">9:16竖屏</view>
<view :class="['option-chip', selectedRatio === '16:9' ? 'active' : '']" @click="selectRatio" data-value="16:9">16:9横屏</view>
</view>
</view>
<!-- AI生成内容标识 -->
<view class="ai-notice">
<text class="ai-notice-text">内容由AI生成结果仅供参考</text>
</view>
<!-- 底部操作 -->
<view class="footer">
<button class="btn-outline" @click="viewHistory">历史创作</button>
<button class="btn-primary" @click="onGenerate">立即生成会员免费</button>
</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,
refImage: '',
imageLoading: false,
imageError: false,
description: '描述你想要生成的内容,尽量说清楚人物、场景、动作等,比如:一个女孩坐在新装修的客厅沙发上',
selectedDuration: 15,
selectedRatio: '9:16',
suggestions: [
'女孩自然的姿势贴地快递飞行,飞向湖面',
'黄昏城市街头,骑行穿梭灯影之间'
],
// 语音输入
isRecording: false,
recorderManager: null,
recordDuration: 0,
recordTimer: null,
sourceArticleId: ''
};
},
onLoad(options) {
console.log('一键创作页面加载', options);
// 检查用户是否登录
if (!this.uid) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/users/wechat_login/index'
});
}, 1500);
return;
}
this.computeStatusBarHeight();
if (options && options.description && options.from === 'video') {
const description = decodeURIComponent(options.description);
this.description = description;
console.log('从视频页面接收到描述内容:', description);
}
if (options && options.description && options.from === 'image') {
const description = decodeURIComponent(options.description);
this.description = description;
}
if (options && options.articleId) {
this.sourceArticleId = options.articleId;
}
// 初始化录音管理器
this.initRecorder();
},
onShow() {
// 检查用户是否登录
if (!this.uid) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/users/wechat_login/index'
});
}, 1500);
return;
}
if (!this.statusBarHeight || this.statusBarHeight <= 0) {
this.computeStatusBarHeight();
}
},
onUnload() {
// 清理录音计时器
this.stopRecordTimer();
// 如果正在录音,停止录音
if (this.isRecording && this.recorderManager) {
this.recorderManager.stop();
}
},
methods: {
computeStatusBarHeight() {
const sys = uni.getSystemInfoSync() || {};
const sbh = sys.statusBarHeight;
const safeTop = sys.safeArea && typeof sys.safeArea.top === 'number' ? sys.safeArea.top : 0;
const isIOS = (sys.platform === 'ios') || /iphone|ipad|ipod/i.test(String(sys.model || ''));
const fallback = isIOS ? 44 : 24;
this.statusBarHeight = (sbh && sbh > 0) ? sbh : (safeTop && safeTop > 0 ? safeTop : fallback);
this.statusBarHeight += 30;
},
onBack() {
uni.navigateBack();
},
// 初始化录音管理器
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.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();
uni.showToast({
title: '录音失败,请重试',
icon: 'none'
});
});
// #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;
},
// 开始语音输入
startVoiceInput() {
// #ifdef H5
uni.showModal({
title: '提示',
content: 'H5环境暂不支持语音输入请使用小程序或APP',
showCancel: false
});
return;
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
if (!this.recorderManager) {
uni.showToast({
title: '录音功能初始化失败',
icon: 'none'
});
return;
}
// 检查录音权限
uni.authorize({
scope: 'scope.record',
success: () => {
this.startRecording();
},
fail: () => {
uni.showModal({
title: '需要录音权限',
content: '请在设置中开启录音权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
});
}
});
// #endif
},
// 开始录音
startRecording() {
this.isRecording = true;
uni.showToast({
title: '开始录音',
icon: 'none',
duration: 1000
});
// 开始录音
this.recorderManager.start({
duration: 60000, // 最长录音60秒
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
});
},
// 停止语音输入
stopVoiceInput() {
if (!this.isRecording) {
return;
}
this.isRecording = false;
// #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) {
// 将识别结果添加到描述中
if (this.description) {
this.description = recognizedText;
} else {
this.description = 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;
},
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) {
this.imageLoading = true;
this.imageError = false;
this.refImage = uploadResult.data.fullUrl;
uni.showToast({
title: '图片上传成功',
icon: 'success',
duration: 2000
});
console.log('图片上传成功:', uploadResult.data);
} else {
this.imageError = true;
throw new Error(uploadResult?.message || '上传失败');
}
} catch (error) {
uni.hideLoading();
console.error('图片选择或上传失败:', error);
let errorMsg = '图片上传失败';
if (error.errMsg && error.errMsg.includes('cancel')) {
return;
} else if (error.message) {
errorMsg = error.message;
}
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
},
removeImage() {
this.refImage = '';
this.imageLoading = false;
this.imageError = false;
},
onImageLoad() {
this.imageLoading = false;
this.imageError = false;
},
onImageError() {
this.imageLoading = false;
this.imageError = true;
},
retryLoadImage() {
if (this.refImage) {
this.imageLoading = true;
this.imageError = false;
// 强制重新加载图片
const originalSrc = this.refImage;
this.refImage = '';
this.$nextTick(() => {
this.refImage = originalSrc + '?t=' + Date.now();
});
}
},
onDescInput(e) {
this.description = e.detail.value;
},
applySuggestion(e) {
const text = e.currentTarget.dataset.text;
this.description = text;
},
shuffleIdeas() {
const pools = [
['现代简约风格客厅,白色墙面,灰色沙发,落地窗,极简设计', '北欧风格卧室,浅色木质家具,温馨舒适,自然光线'],
['中式风格书房,红木家具,传统装饰,典雅大气', '欧式风格餐厅,华丽吊灯,精致餐具,浪漫氛围'],
['日式风格茶室,榻榻米,简约禅意,自然材质', '工业风格loft裸露砖墙金属元素现代感强'],
['美式风格客厅,暖色调,舒适沙发,壁炉装饰', '地中海风格阳台,蓝白色调,拱形门窗,清新自然'],
['新中式风格卧室,现代与传统结合,水墨画装饰', '轻奢风格客厅,金色点缀,大理石地面,高端质感']
];
const idx = Math.floor(Math.random() * pools.length);
const newSuggestions = pools[idx];
const randomSuggestion = newSuggestions[Math.floor(Math.random() * newSuggestions.length)];
this.suggestions = newSuggestions;
this.description = randomSuggestion;
},
clearDescription() {
this.description = '';
},
selectDuration(e) {
const value = Number(e.currentTarget.dataset.value);
this.selectedDuration = value;
},
selectRatio(e) {
const value = e.currentTarget.dataset.value;
this.selectedRatio = value;
},
viewHistory() {
uni.navigateTo({
url: '/pages/ai-generate/history'
});
},
onShowExample() {
uni.showToast({ title: '展示示例', icon: 'none' });
},
async onGenerate() {
uni.showLoading({ title: '正在创建视频任务...', mask: true });
try {
// 获取用户信息
const userInfo = this.userInfo || {};
const avatar = userInfo.avatar || '';
const nickname = userInfo.nickname || '';
const uid = this.uid || '';
let response;
if (this.refImage) {
const imageToVideoParams = {
imageUrl: this.refImage,
prompt: this.description,
aspect_ratio: this.selectedRatio === '9:16' ? 'portrait' : '16:9',
n_frames: Math.ceil(this.selectedDuration / 3),
size: 'standard',
remove_watermark: true,
avatar: avatar,
nickname: nickname,
uid:this.uid,
};
console.log('调用图生视频任务接口,参数:', imageToVideoParams);
response = await api.createImageToVideoTask(imageToVideoParams);
} else {
const params = {
prompt: this.description,
aspect_ratio: this.selectedRatio === '9:16' ? 'portrait' : '16:9',
n_frames: Math.ceil(this.selectedDuration / 3),
size: 'standard',
remove_watermark: true,
avatar: avatar,
nickname: nickname,
uid:this.uid,
};
console.log('调用文生视频任务接口,参数:', params);
response = await api.createTextToVideoTask(params);
}
console.log('创建任务响应:', response);
uni.hideLoading();
if (response && response.code === 200) {
uni.showToast({
title: '任务创建成功',
icon: 'success',
duration: 2000
});
const taskType = this.refImage ? 'image-to-video' : 'text-to-video';
const q = `type=${taskType}&desc=${encodeURIComponent(this.description)}&duration=${this.selectedDuration}&ratio=${encodeURIComponent(this.selectedRatio)}&taskId=${response.data?.task_id || ''}&refImage=${encodeURIComponent(this.refImage || '')}`;
setTimeout(() => {
uni.navigateTo({ url: `/pages/ai-generate/history` });
}, 1000);
} else {
const errorMsg = response?.message || '任务创建失败,请重试';
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
} catch (error) {
uni.hideLoading();
console.error('创建视频任务失败:', error);
let errorMsg = '网络连接失败,请检查网络后重试';
if (error.message && error.message.includes('请求失败')) {
errorMsg = '服务器响应异常,请稍后重试';
} else if (error.message) {
errorMsg = error.message;
}
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
},
onShareAppMessage() {
return {
title: '一键创作 - 人人都是大导演',
path: '/pages/ai-generate/oneclick'
};
},
onShareTimeline() {
return {
title: '一键创作 - 人人都是大导演'
};
},
setData(data) {
let that = this;
Object.keys(data).forEach(key => {
that[key] = data[key];
});
}
}
};
</script>
<style>
/* 一键创作页面 - 科技感设计 */
.oneclick-container {
background-color: black;
min-height: 100vh;
position: relative;
padding: 190rpx 24rpx 120rpx 24rpx; /* 增加底部padding为固定按钮留出空间 */
box-sizing: border-box;
overflow: hidden;
}
.top-controls { position: fixed; margin-top: 48px; top: 0; left: 0; right: 0; height: 48px; display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px 16px; padding-top: env(safe-area-inset-top); background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%); z-index: 10; }
.left-controls { display: flex; align-items: center; }
.back-btn { height: 25px; display: flex; align-items: center; justify-content: center; margin-right: 12px; }
.icon-back { font-size: 34px; color: #ffffff; font-weight: normal; }
.center-title { flex: 1; display: flex; align-items: flex-end; justify-content: center; height: 48px; pointer-events: none; }
.title-text { font-size: 16px; margin-right: 60px; font-weight: 600; color: #ffffff; }
/* 科技感背景粒子效果 */
.oneclick-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 30%, rgba(66, 202, 77, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(42, 165, 58, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(66, 202, 77, 0.08) 0%, transparent 50%);
animation: techFloat 8s ease-in-out infinite;
pointer-events: none;
}
@keyframes techFloat {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(1deg); }
66% { transform: translateY(5px) rotate(-1deg); }
}
/* AI生成内容标识 */
.ai-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 16rpx 32rpx;
margin: 0rpx 1rpx 70rpx 1rpx;
background: rgba(66, 202, 77, 0.1);
border: 2rpx solid rgba(66, 202, 77, 0.3);
border-radius: 24rpx;
backdrop-filter: blur(20px);
position: relative;
z-index: 2;
box-shadow: 0 4rpx 12rpx rgba(66, 202, 77, 0.15);
}
.ai-notice-icon {
font-size: 32rpx;
}
.ai-notice-text {
font-size: 24rpx;
color: #42ca4d;
line-height: 1.4;
text-shadow: 0 0 8rpx rgba(66, 202, 77, 0.3);
}
.page-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
position: relative;
z-index: 2;
}
.page-title {
color: #ffffff;
font-size: 40rpx;
font-weight: 700;
text-shadow: 0 0 20rpx rgba(66, 202, 77, 0.5);
letter-spacing: 2rpx;
}
/* 科技感玻璃态卡片 */
.section {
background: rgba(0, 0, 0, 0.552);
backdrop-filter: blur(20px);
border: 1rpx solid rgba(66, 202, 77, 0.2);
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow:
0 8rpx 32rpx rgba(0, 0, 0, 0.3),
inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
/* 卡片内发光边框效果 */
.section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 32rpx;
padding: 2rpx;
background: linear-gradient(135deg, rgba(66, 202, 77, 0.3), transparent, rgba(42, 165, 58, 0.2));
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}
.section-title-outer {
margin: 36rpx 0;
}
.section-title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16rpx;
}
.section-title {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
text-shadow: 0 0 10rpx rgba(66, 202, 77, 0.3);
}
.section-link {
color: #42ca4d;
font-size: 24rpx;
opacity: 0.9;
text-decoration: none;
}
.tips {
margin-top: 12rpx;
color: rgba(255, 255, 255, 0.7);
font-size: 24rpx;
line-height: 1.5;
}
.tips .warning {
display: block;
margin-top: 8rpx;
color: #ff6b6b;
text-shadow: 0 0 8rpx rgba(255, 107, 107, 0.3);
}
/* 科技感上传区域 */
.upload-card {
margin-top: 20rpx;
border: 2rpx dashed rgba(66, 202, 77, 0.6);
border-radius: 24rpx;
height: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.85);
background: linear-gradient(160deg, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.2));
backdrop-filter: blur(10px);
position: relative;
transition: all 0.3s ease;
box-shadow: 0 0 28rpx rgba(66, 202, 77, 0.22), inset 0 0 10rpx rgba(66, 202, 77, 0.1);
overflow: hidden;
animation: pulseGlow 3s ease-in-out infinite;
}
.upload-card:active {
transform: scale(0.98);
border-color: rgba(66, 202, 77, 0.8);
background: rgba(66, 202, 77, 0.2);
box-shadow: 0 0 30rpx rgba(66, 202, 77, 0.38);
}
/* 光晕与扫光效果 */
.upload-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 24rpx;
pointer-events: none;
background:
radial-gradient(60% 80% at 15% 15%, rgba(66, 202, 77, 0.12), transparent 60%),
radial-gradient(60% 80% at 85% 85%, rgba(42, 165, 58, 0.08), transparent 60%);
}
.upload-card::after {
content: '';
position: absolute;
top: 0;
left: -120%;
width: 120%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
transform: skewX(12deg);
animation: sweep 4s linear infinite;
}
@keyframes pulseGlow {
0%, 100% { box-shadow: 0 0 24rpx rgba(66, 202, 77, 0.18), inset 0 0 8rpx rgba(66, 202, 77, 0.08); }
50% { box-shadow: 0 0 32rpx rgba(66, 202, 77, 0.28), inset 0 0 12rpx rgba(66, 202, 77, 0.12); }
}
@keyframes sweep {
0% { left: -120%; }
100% { left: 120%; }
}
.upload-plus {
font-size: 72rpx;
line-height: 1;
color: #42ca4d;
text-shadow: 0 0 26rpx rgba(66, 202, 77, 0.55);
letter-spacing: 2rpx;
}
.upload-text {
margin-top: 12rpx;
font-size: 28rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
opacity: 0.95;
}
.upload-preview {
margin-top: 20rpx;
position: relative;
background: rgba(0, 0, 0, 0.25);
border: 1rpx solid rgba(66, 202, 77, 0.2);
border-radius: 24rpx;
padding: 12rpx;
box-shadow: 0 6rpx 18rpx rgba(66, 202, 77, 0.2);
}
/* 图片容器 - 确保等比例缩放和居中显示 */
.image-container {
width: 100%;
aspect-ratio: 16/9; /* 使用aspect-ratio确保固定宽高比 */
min-height: 200rpx;
max-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.1);
border-radius: 18rpx;
border: 2rpx solid rgba(66, 202, 77, 0.35);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.32), 0 0 18rpx rgba(66, 202, 77, 0.22);
overflow: hidden;
position: relative;
}
/* 图片预加载和性能优化 */
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
border-radius: 16rpx;
transition: all 0.3s ease;
/* 确保图片在容器中居中 */
display: block;
margin: auto;
/* 性能优化 */
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
/* 防止图片模糊 */
image-rendering: -webkit-optimize-contrast;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
/* GPU加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* 图片预加载优化 */
loading: lazy;
/* 防止图片拖拽 */
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
/* 防止图片选择 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* 图片缓存优化 */
image-orientation: from-image;
}
/* 图片加载状态指示器 */
.image-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid rgba(66, 202, 77, 0.3);
border-top: 4rpx solid #42ca4d;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16rpx;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #42ca4d;
font-size: 24rpx;
text-align: center;
}
/* 图片加载错误状态 */
.image-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
cursor: pointer;
padding: 20rpx;
border-radius: 12rpx;
background: rgba(0, 0, 0, 0.8);
transition: all 0.3s ease;
}
.image-error:hover {
background: rgba(0, 0, 0, 0.9);
transform: translate(-50%, -50%) scale(1.05);
}
.error-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.error-text {
color: #ff6b6b;
font-size: 24rpx;
text-align: center;
margin-bottom: 8rpx;
}
.error-retry {
color: #42ca4d;
font-size: 20rpx;
text-align: center;
opacity: 0.8;
}
/* 响应式设计优化 */
@media screen and (max-width: 750rpx) {
.image-container {
aspect-ratio: 4/3;
min-height: 180rpx;
max-height: 320rpx;
}
}
@media screen and (max-width: 600rpx) {
.image-container {
aspect-ratio: 1/1;
min-height: 160rpx;
max-height: 280rpx;
}
}
@media screen and (max-width: 480rpx) {
.image-container {
aspect-ratio: 1/1;
min-height: 140rpx;
max-height: 240rpx;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
border-width: 3rpx;
}
.loading-text {
font-size: 20rpx;
}
.error-icon {
font-size: 40rpx;
}
.error-text {
font-size: 20rpx;
}
.error-retry {
font-size: 18rpx;
}
}
/* 高分辨率屏幕优化 */
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.preview-image {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
}
/* 横屏适配 */
@media screen and (orientation: landscape) {
.image-container {
aspect-ratio: 16/9;
min-height: 160rpx;
max-height: 280rpx;
}
}
.preview-actions {
margin-top: 16rpx;
display: flex;
gap: 16rpx;
}
.desc-row {
margin-top: 16rpx;
}
/* 输入框包装器 */
.desc-input-wrapper {
position: relative;
background: rgba(0, 0, 0, 0.7);
border-radius: 24rpx;
border: 2rpx solid rgba(66, 202, 77, 0.6);
box-shadow: 0 0 25rpx rgba(66, 202, 77, 0.4);
backdrop-filter: blur(15px);
min-height: 240rpx;
}
/* 科技感输入框 */
.desc-input {
width: 100%;
min-height: 240rpx;
background: transparent;
color: #ffffff;
border-radius: 24rpx;
padding: 24rpx 88rpx 24rpx 24rpx; /* 右侧留出语音按钮空间 */
box-sizing: border-box;
border: none;
font-size: 30rpx;
line-height: 1.8;
resize: none;
}
.desc-input-wrapper:focus-within {
border-color: rgba(66, 202, 77, 0.8);
box-shadow: 0 0 30rpx rgba(66, 202, 77, 0.5);
}
/* 语音输入按钮 */
.voice-btn {
position: absolute;
bottom: 16rpx;
right: 16rpx;
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(66, 202, 77, 0.4);
transition: all 0.3s ease;
z-index: 10;
}
.voice-btn .iconfont {
font-size: 48rpx;
color: #ffffff;
}
.voice-btn.recording {
background: #ff6b6b;
width: 102rpx;
height: 102rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.5);
}
.voice-btn.recording .recording-animation {
width: 40rpx;
height: 40rpx;
background: #ffffff;
border-radius: 50%;
animation: pulse 1s infinite;
margin-bottom: 4rpx;
}
.voice-btn.recording .recording-time {
font-size: 20rpx;
color: #ffffff;
font-weight: 500;
}
/* 录音提示 */
.recording-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-top: 24rpx;
padding: 16rpx 32rpx;
background: rgba(255, 107, 107, 0.1);
border-radius: 40rpx;
border: 1px solid rgba(255, 107, 107, 0.3);
animation: blink 1.5s infinite;
}
.recording-hint .hint-icon {
font-size: 32rpx;
}
.recording-hint .hint-text {
font-size: 26rpx;
color: #ff6b6b;
}
/* 动画 */
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.8);
opacity: 0.6;
}
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.desc-actions {
margin-top: 20rpx; /* 增加间距 */
display: flex;
gap: 20rpx; /* 增加按钮间距 */
justify-content: flex-start; /* 左对齐 */
}
.suggest-list {
margin-top: 24rpx; /* 增加顶部间距 */
display: flex;
flex-wrap: wrap;
gap: 16rpx; /* 增加间距 */
}
/* 科技感建议标签 */
.suggest-item {
background: rgba(66, 202, 77, 0.15); /* 增加背景透明度 */
color: rgba(255, 255, 255, 0.95); /* 增加文字透明度 */
border: 2rpx solid rgba(66, 202, 77, 0.4); /* 加粗边框并增加透明度 */
border-radius: 999rpx;
padding: 18rpx 28rpx; /* 增加内边距 */
font-size: 26rpx; /* 增大字体 */
transition: all 0.3s ease;
backdrop-filter: blur(12px);
box-shadow: 0 2rpx 12rpx rgba(66, 202, 77, 0.1); /* 添加阴影 */
}
.suggest-item:active {
background: rgba(66, 202, 77, 0.25); /* 增加激活状态背景 */
transform: scale(0.96); /* 稍微调整缩放 */
box-shadow: 0 0 20rpx rgba(66, 202, 77, 0.4); /* 增强阴影效果 */
border-color: rgba(66, 202, 77, 0.6); /* 增强边框颜色 */
}
.options-row {
margin-top: 20rpx;
display: flex;
gap: 16rpx;
}
/* 科技感选项芯片 */
.option-chip {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(66, 202, 77, 0.2);
border-radius: 999rpx;
padding: 16rpx 28rpx;
font-size: 26rpx;
font-weight: 500;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
position: relative;
}
.option-chip.active {
background: linear-gradient(135deg, #42ca4d 0%, #2aa53a 100%);
color: white;
border-color: transparent;
box-shadow:
0 0 20rpx rgba(66, 202, 77, 0.4),
0 4rpx 15rpx rgba(42, 165, 58, 0.3);
transform: translateY(-2rpx);
}
/* 科技感底部操作区 */
.footer {
position: fixed; /* 改为fixed固定定位 */
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.8); /* 添加半透明背景 */
backdrop-filter: blur(20px);
padding: 24rpx;
display: flex;
gap: 20rpx;
z-index: 1000; /* 确保在最上层 */
border-top: 1rpx solid rgba(66, 202, 77, 0.2); /* 添加顶部边框 */
}
/* 科技感主按钮 */
.btn-primary {
flex: 2; /* 设置主按钮占更多空间 */
background: linear-gradient(135deg, #42ca4d 0%, #2aa53a 100%);
color: #fff;
border: none;
border-radius: 39rpx;
padding: 24rpx 32rpx;
font-size: 30rpx;
font-weight: 600;
box-shadow:
0 8rpx 24rpx rgba(66, 202, 77, 0.4),
0 0 20rpx rgba(42, 165, 58, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-primary::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.btn-primary:active::before {
left: 100%;
}
.btn-primary:active {
transform: scale(0.98);
box-shadow:
0 4rpx 16rpx rgba(66, 202, 77, 0.5),
0 0 15rpx rgba(42, 165, 58, 0.4);
}
/* 科技感次要按钮 */
.btn-outline {
flex: 1;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
border: 2rpx solid rgba(66, 202, 77, 0.4);
border-radius: 39rpx;
padding: 24rpx 32rpx;
font-size: 30rpx;
font-weight: 500;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.btn-outline:active {
background: rgba(66, 202, 77, 0.1);
border-color: rgba(66, 202, 77, 0.6);
transform: scale(0.98);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(66, 202, 77, 0.2);
border-radius: 999rpx;
padding: 10rpx 24rpx;
font-size: 20rpx;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.btn-secondary:active {
background: rgba(66, 202, 77, 0.1);
transform: scale(0.95);
}
</style>