1113 lines
23 KiB
Vue
1113 lines
23 KiB
Vue
<template>
|
||
<view class="create-feed">
|
||
<!-- 梦幻渐变背景 -->
|
||
<view class="gradient-background"></view>
|
||
|
||
<!-- 散布的元素 -->
|
||
<view v-for="item in scatteredItems" :key="item.id" class="scattered-item" :style="getItemStyle(item)"
|
||
@click="handleItemClick(item)">
|
||
<!-- 图片卡片 -->
|
||
<view class="item-card">
|
||
<image class="card-image" :src="item.image" mode="aspectFill" />
|
||
</view>
|
||
<!-- 标签文字在图片下方 -->
|
||
<text class="card-label">{{ item.label }}</text>
|
||
</view>
|
||
|
||
<!-- 底部输入框 -->
|
||
<view class="bottom-hint">
|
||
<view class="input-wrapper">
|
||
<!-- 已选标签 -->
|
||
<view v-if="selectedTags.length > 0" class="selected-tags">
|
||
<view v-for="(tag, index) in selectedTags" :key="index" class="selected-tag" @click.stop="removeTag(index)">
|
||
<text class="selected-tag-text">{{ tag }}</text>
|
||
<text class="selected-tag-close">×</text>
|
||
</view>
|
||
</view>
|
||
<!-- 输入框和按钮 -->
|
||
<view class="input-row">
|
||
<input
|
||
class="hint-input"
|
||
v-model="dreamInput"
|
||
placeholder="点击描述你的愿景"
|
||
placeholder-class="hint-placeholder"
|
||
@focus="handleInputFocus"
|
||
@blur="handleInputBlur"
|
||
@confirm="handleInputConfirm"
|
||
/>
|
||
<view class="input-actions">
|
||
<!-- 语音输入按钮 - 支持长按 -->
|
||
<view
|
||
class="action-icon-btn voice-btn"
|
||
:class="{ 'recording': isRecording }"
|
||
@touchstart.stop.prevent="handleTouchStart"
|
||
@touchend.stop.prevent="handleTouchEnd"
|
||
@touchcancel.stop.prevent="handleTouchCancel"
|
||
>
|
||
<!-- 录音状态显示 -->
|
||
<view v-if="!isRecording" class="voice-normal" style="pointer-events: none;">
|
||
<!-- <image class="action-icon" src="/static/icon/voice.png" mode="aspectFit" /> -->
|
||
<!-- <text class="action-text">长按说话</text> -->
|
||
</view>
|
||
<!-- 录音中显示波形 -->
|
||
<view v-else class="voice-recording" style="pointer-events: none;">
|
||
<view class="waveform">
|
||
<view
|
||
v-for="(height, index) in waveformBars"
|
||
:key="index"
|
||
class="waveform-bar"
|
||
:style="{ height: (height * 100) + '%' }"
|
||
></view>
|
||
</view>
|
||
<text class="action-text recording-text">{{ recordingDuration }}s 松开识别</text>
|
||
</view>
|
||
</view>
|
||
<view class="action-icon-btn send-btn" @click="handleInputConfirm">
|
||
<image class="action-icon" src="/static/icon/send.png" mode="aspectFit" />
|
||
<text class="action-text">发送</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 通用确认弹窗 -->
|
||
<ConfirmModal
|
||
:visible="confirmModal.visible"
|
||
:title="confirmModal.title"
|
||
:content="confirmModal.content"
|
||
:confirmText="confirmModal.confirmText"
|
||
:cancelText="confirmModal.cancelText"
|
||
:showCancel="confirmModal.showCancel"
|
||
@confirm="onConfirmModal"
|
||
@cancel="onCancelModal"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, nextTick } from 'vue';
|
||
import ConfirmModal from '@/components/ConfirmModal.vue';
|
||
|
||
// 梦境输入
|
||
const dreamInput = ref('');
|
||
// 已选标签
|
||
const selectedTags = ref([]);
|
||
|
||
// 从 CastloveContent 获取的表单数据
|
||
const castloveFormData = ref(null);
|
||
|
||
// 语音识别相关
|
||
const recorderManager = ref(null);
|
||
const isRecording = ref(false);
|
||
const recordingDuration = ref(0);
|
||
let recordingTimer = null;
|
||
let recognizedText = ''; // 保存识别结果
|
||
let isRecognizing = false; // 标记是否正在识别
|
||
|
||
// 波形动画相关
|
||
const waveformBars = ref([1, 1, 1, 1, 1]); // 5个波形条的高度
|
||
let waveformTimer = null;
|
||
|
||
// 散布的元素数据 - 动态生成,图片路径后续会随机生成
|
||
// 3列2行布局,统一尺寸,高低错落
|
||
const scatteredItems = ref([
|
||
{
|
||
id: 1,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '自律小卡',
|
||
x: 6,
|
||
y: 18, // 第一行第一张,最低
|
||
width: 220,
|
||
height: 400
|
||
},
|
||
{
|
||
id: 2,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '未来艺术',
|
||
x: 38,
|
||
y: 10, // 第一行中间,最高
|
||
width: 220,
|
||
height: 400
|
||
},
|
||
{
|
||
id: 3,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '真人表情包',
|
||
x: 70,
|
||
y: 14, // 第一行第三张,中等
|
||
width: 220,
|
||
height: 400
|
||
},
|
||
{
|
||
id: 4,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '海报风',
|
||
x: 6,
|
||
y: 48, // 第二行第一张,最低
|
||
width: 220,
|
||
height: 400
|
||
},
|
||
{
|
||
id: 5,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '艺术插画',
|
||
x: 38,
|
||
y: 40, // 第二行中间,最高
|
||
width: 220,
|
||
height: 400
|
||
},
|
||
{
|
||
id: 6,
|
||
type: 'image',
|
||
image: '', // 后续随机生成
|
||
label: '二次元',
|
||
x: 70,
|
||
y: 44, // 第二行第三张,中等
|
||
width: 220,
|
||
height: 400
|
||
}
|
||
]);
|
||
|
||
// 随机生成图片路径的函数
|
||
const generateRandomImages = () => {
|
||
const totalImages = 16; // static/sucai 中有 16 张图片
|
||
const usedIndexes = new Set();
|
||
|
||
scatteredItems.value.forEach(item => {
|
||
let randomIndex;
|
||
do {
|
||
randomIndex = Math.floor(Math.random() * totalImages) + 1;
|
||
} while (usedIndexes.has(randomIndex));
|
||
|
||
usedIndexes.add(randomIndex);
|
||
const imageNumber = String(randomIndex).padStart(2, '0');
|
||
item.image = `/static/sucai/image-${imageNumber}.png`;
|
||
});
|
||
};
|
||
|
||
// 获取元素样式
|
||
const getItemStyle = (item) => {
|
||
if (item.type === 'image') {
|
||
return {
|
||
left: `${item.x}%`,
|
||
top: `${item.y}%`,
|
||
width: `${item.width}rpx`,
|
||
height: `${item.height}rpx`
|
||
};
|
||
}
|
||
return {
|
||
left: `${item.x}%`,
|
||
top: `${item.y}%`,
|
||
width: `${item.size}rpx`,
|
||
height: `${item.size}rpx`
|
||
};
|
||
};
|
||
|
||
// 处理元素点击
|
||
const handleItemClick = (item) => {
|
||
// 添加标签到已选列表
|
||
const index = selectedTags.value.indexOf(item.label);
|
||
if (index > -1) {
|
||
// 已存在,则移除
|
||
selectedTags.value.splice(index, 1);
|
||
} else {
|
||
// 不存在,则添加
|
||
selectedTags.value.push(item.label);
|
||
}
|
||
};
|
||
|
||
// 初始化录音管理器
|
||
const initRecorderManager = () => {
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
recorderManager.value = uni.getRecorderManager();
|
||
|
||
// 录音开始事件
|
||
recorderManager.value.onStart(() => {
|
||
isRecording.value = true;
|
||
recordingDuration.value = 0;
|
||
|
||
// 开始计时
|
||
recordingTimer = setInterval(() => {
|
||
recordingDuration.value += 1;
|
||
if (recordingDuration.value >= 60) {
|
||
// 最长录音60秒
|
||
stopRecording();
|
||
}
|
||
}, 1000);
|
||
|
||
// 开始波形动画
|
||
startWaveformAnimation();
|
||
});
|
||
|
||
// 录音结束事件
|
||
recorderManager.value.onStop((res) => {
|
||
// 使用保存的录音时长(因为 recordingDuration 已经被重置为 0)
|
||
const duration = savedDuration;
|
||
console.log('[CreateFeed] onStop 回调,使用保存的时长:', duration);
|
||
|
||
// 重置保存的时长
|
||
savedDuration = 0;
|
||
|
||
// 立即停止定时器
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
|
||
// 停止波形动画
|
||
stopWaveformAnimation();
|
||
|
||
// 使用 setTimeout 确保状态更新能触发界面刷新
|
||
setTimeout(() => {
|
||
isRecording.value = false;
|
||
recordingDuration.value = 0;
|
||
|
||
// 检查录音时长
|
||
if (duration < 1) {
|
||
uni.showToast({
|
||
title: '录音时间太短',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 显示录音成功提示
|
||
uni.showToast({
|
||
title: `✅ 录音已停止 (${duration}秒)`,
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
}, 0);
|
||
});
|
||
|
||
// 录音错误事件
|
||
recorderManager.value.onError((err) => {
|
||
console.error('[CreateFeed] 录音错误:', err);
|
||
isRecording.value = false;
|
||
recordingDuration.value = 0; // 重置录音时长
|
||
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
|
||
// 根据错误类型显示不同提示
|
||
let errorMsg = '录音失败,请重试';
|
||
if (err.errMsg && err.errMsg.includes('permission')) {
|
||
errorMsg = '请授予录音权限';
|
||
// 显示权限设置提示
|
||
showConfirmModal({
|
||
title: '需要录音权限',
|
||
content: '请在设置中开启录音权限后重试',
|
||
confirmText: '去设置',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
plus.runtime.openURL('app-settings:');
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
|
||
// 停止波形动画
|
||
stopWaveformAnimation();
|
||
});
|
||
|
||
console.log('[CreateFeed] 录音管理器初始化成功');
|
||
} catch (error) {
|
||
console.error('[CreateFeed] 录音管理器初始化失败:', error);
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
// 语音识别(支持两种模式)
|
||
// 将识别的文字追加到输入框
|
||
const appendTextToInput = (text) => {
|
||
if (!text) return;
|
||
|
||
if (dreamInput.value) {
|
||
dreamInput.value += ' ' + text;
|
||
} else {
|
||
dreamInput.value = text;
|
||
}
|
||
};
|
||
|
||
// 开始录音
|
||
const startRecording = () => {
|
||
// #ifdef APP-PLUS
|
||
|
||
// 防止重复调用
|
||
if (isRecording.value) {
|
||
return;
|
||
}
|
||
|
||
// 清空之前的识别结果
|
||
recognizedText = '';
|
||
|
||
// 启动实时语音识别
|
||
startSpeechRecognition();
|
||
|
||
// 同时启动录音(用于显示波形和时长)
|
||
if (!recorderManager.value) {
|
||
initRecorderManager();
|
||
}
|
||
|
||
try {
|
||
recorderManager.value.start({
|
||
duration: 60000,
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 96000,
|
||
format: 'mp3'
|
||
});
|
||
} catch (error) {
|
||
console.error('[CreateFeed] 开始录音失败:', error);
|
||
stopSpeechRecognition();
|
||
showConfirmModal({
|
||
title: '录音失败',
|
||
content: '请确保已授予录音权限',
|
||
confirmText: '去设置',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// #ifdef APP-PLUS
|
||
plus.runtime.openURL('app-settings:');
|
||
// #endif
|
||
}
|
||
}
|
||
});
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
// 启动实时语音识别
|
||
const startSpeechRecognition = () => {
|
||
// #ifdef APP-PLUS
|
||
if (!plus.speech) {
|
||
console.error('[CreateFeed] 语音识别模块不可用');
|
||
uni.showToast({
|
||
title: '语音识别模块不可用',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果正在识别,先停止
|
||
if (isRecognizing) {
|
||
console.log('[CreateFeed] 检测到正在识别,先停止');
|
||
try {
|
||
plus.speech.stopRecognize();
|
||
} catch (e) {
|
||
console.error('[CreateFeed] 停止识别失败:', e);
|
||
}
|
||
isRecognizing = false;
|
||
// 等待一下再启动
|
||
setTimeout(() => {
|
||
startSpeechRecognition();
|
||
}, 300);
|
||
return;
|
||
}
|
||
|
||
// 清空之前的识别结果
|
||
recognizedText = '';
|
||
isRecognizing = true;
|
||
|
||
console.log('[CreateFeed] 开始语音识别');
|
||
|
||
// 使用官方 demo 的简化配置
|
||
const options = {
|
||
engine: 'baidu'
|
||
};
|
||
|
||
plus.speech.startRecognize(options, function(result) {
|
||
console.log('[CreateFeed] 识别结果:', result);
|
||
// 累积识别结果
|
||
recognizedText += result;
|
||
}, function(error) {
|
||
isRecognizing = false;
|
||
console.error('[CreateFeed] 语音识别失败:', JSON.stringify(error));
|
||
|
||
// 根据错误代码显示提示
|
||
let errorMsg = '识别失败';
|
||
if (error.code === 2225220) {
|
||
errorMsg = '认证失败,请检查配置';
|
||
} else if (error.code === 2625535) {
|
||
errorMsg = '引擎忙,请稍后重试';
|
||
} else if (error.code === 10001) {
|
||
errorMsg = '网络连接失败';
|
||
}
|
||
|
||
// 只在非引擎忙的情况下显示提示
|
||
if (error.code !== 2625535) {
|
||
uni.showToast({
|
||
title: errorMsg,
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
// 停止实时语音识别
|
||
const stopSpeechRecognition = () => {
|
||
// #ifdef APP-PLUS
|
||
if (plus.speech && isRecognizing) {
|
||
console.log('[CreateFeed] 停止实时语音识别');
|
||
try {
|
||
plus.speech.stopRecognize();
|
||
isRecognizing = false;
|
||
} catch (e) {
|
||
console.error('[CreateFeed] 停止识别异常:', e);
|
||
isRecognizing = false;
|
||
}
|
||
|
||
// 如果有识别结果,添加到输入框
|
||
if (recognizedText) {
|
||
console.log('[CreateFeed] 最终识别结果:', recognizedText);
|
||
appendTextToInput(recognizedText);
|
||
uni.showToast({
|
||
title: '识别成功',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
} else {
|
||
console.log('[CreateFeed] 未识别到内容');
|
||
}
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
// 停止录音
|
||
const stopRecording = () => {
|
||
// #ifdef APP-PLUS
|
||
if (recorderManager.value && isRecording.value) {
|
||
try {
|
||
recorderManager.value.stop();
|
||
// 停止语音识别
|
||
stopSpeechRecognition();
|
||
} catch (error) {
|
||
console.error('[CreateFeed] 停止录音异常:', error);
|
||
// 如果停止失败,手动重置状态
|
||
isRecording.value = false;
|
||
recordingDuration.value = 0;
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
stopWaveformAnimation();
|
||
stopSpeechRecognition();
|
||
}
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
// 开始波形动画
|
||
const startWaveformAnimation = () => {
|
||
if (waveformTimer) {
|
||
clearInterval(waveformTimer);
|
||
}
|
||
|
||
waveformTimer = setInterval(() => {
|
||
// 随机生成波形高度,模拟音频波动
|
||
waveformBars.value = waveformBars.value.map(() => {
|
||
return Math.random() * 0.7 + 0.3; // 0.3 到 1.0 之间
|
||
});
|
||
}, 100); // 每100ms更新一次
|
||
};
|
||
|
||
// 停止波形动画
|
||
const stopWaveformAnimation = () => {
|
||
if (waveformTimer) {
|
||
clearInterval(waveformTimer);
|
||
waveformTimer = null;
|
||
}
|
||
// 重置波形高度
|
||
waveformBars.value = [1, 1, 1, 1, 1];
|
||
};
|
||
|
||
// 长按开始录音(使用 longpress 事件更可靠)
|
||
const handleLongPress = () => {
|
||
// #ifdef APP-PLUS
|
||
startRecording();
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
showConfirmModal({
|
||
title: '提示',
|
||
content: '语音输入功能仅支持 App 端使用',
|
||
showCancel: false
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
// 触摸开始(直接开始录音,不需要长按)
|
||
const handleTouchStart = () => {
|
||
console.log('[CreateFeed] touchstart 触发');
|
||
// #ifdef APP-PLUS
|
||
startRecording();
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
showConfirmModal({
|
||
title: '提示',
|
||
content: '语音输入功能仅支持 App 端使用',
|
||
showCancel: false
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
// 防止重复触发的标志
|
||
let isStopping = false;
|
||
// 保存录音时长用于 onStop 回调
|
||
let savedDuration = 0;
|
||
|
||
// 通用确认弹窗状态
|
||
const confirmModal = ref({
|
||
visible: false,
|
||
title: '',
|
||
content: '',
|
||
confirmText: '确认',
|
||
cancelText: '取消',
|
||
showCancel: true,
|
||
confirmCallback: null
|
||
});
|
||
|
||
// 弹窗确认回调
|
||
const onConfirmModal = () => {
|
||
if (confirmModal.value.confirmCallback) {
|
||
confirmModal.value.confirmCallback({ confirm: true });
|
||
}
|
||
confirmModal.value.visible = false;
|
||
};
|
||
|
||
// 弹窗取消回调
|
||
const onCancelModal = () => {
|
||
if (confirmModal.value.confirmCallback) {
|
||
confirmModal.value.confirmCallback({ confirm: false });
|
||
}
|
||
confirmModal.value.visible = false;
|
||
};
|
||
|
||
// 显示通用确认弹窗
|
||
const showConfirmModal = (options) => {
|
||
confirmModal.value = {
|
||
visible: true,
|
||
title: options.title || '',
|
||
content: options.content || '',
|
||
confirmText: options.confirmText || '确认',
|
||
cancelText: options.cancelText || '取消',
|
||
showCancel: options.showCancel !== false,
|
||
confirmCallback: options.success || null
|
||
};
|
||
};
|
||
|
||
// 松开停止录音
|
||
const handleTouchEnd = () => {
|
||
console.log('[CreateFeed] ========== touchend 触发 ==========');
|
||
console.log('[CreateFeed] touchend 触发, isRecording:', isRecording.value, 'isStopping:', isStopping);
|
||
|
||
// #ifdef APP-PLUS
|
||
// 防止重复调用
|
||
if (isStopping) {
|
||
console.log('[CreateFeed] 正在停止中,忽略重复调用');
|
||
return;
|
||
}
|
||
|
||
// 立即重置界面状态(不等待 onStop 回调)
|
||
const wasRecording = isRecording.value;
|
||
|
||
if (wasRecording) {
|
||
isStopping = true;
|
||
// 保存当前录音时长,供 onStop 回调使用
|
||
savedDuration = recordingDuration.value;
|
||
console.log('[CreateFeed] 保存录音时长:', savedDuration);
|
||
|
||
isRecording.value = false;
|
||
recordingDuration.value = 0;
|
||
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
|
||
stopWaveformAnimation();
|
||
// 停止语音识别
|
||
stopSpeechRecognition();
|
||
}
|
||
|
||
// 立即调用停止录音
|
||
if (wasRecording && recorderManager.value) {
|
||
try {
|
||
recorderManager.value.stop();
|
||
} catch (error) {
|
||
console.error('[CreateFeed] 停止录音失败:', error);
|
||
}
|
||
|
||
// 延迟重置标志,防止过快的重复调用
|
||
setTimeout(() => {
|
||
isStopping = false;
|
||
}, 500);
|
||
}
|
||
|
||
console.log('[CreateFeed] ========== touchend 处理完成 ==========');
|
||
// #endif
|
||
};
|
||
|
||
// 取消录音(手指移出按钮区域)
|
||
const handleTouchCancel = () => {
|
||
console.log('[CreateFeed] touchcancel 触发');
|
||
// #ifdef APP-PLUS
|
||
if (isRecording.value) {
|
||
// 取消录音,不进行识别
|
||
if (recorderManager.value) {
|
||
recorderManager.value.stop();
|
||
}
|
||
// 停止语音识别
|
||
stopSpeechRecognition();
|
||
|
||
isRecording.value = false;
|
||
recordingDuration.value = 0;
|
||
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
|
||
stopWaveformAnimation();
|
||
|
||
uni.showToast({
|
||
title: '已取消录音',
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
// 语音输入(保留点击方式作为备用)
|
||
const handleVoiceInput = () => {
|
||
// #ifdef APP-PLUS
|
||
if (isRecording.value) {
|
||
// 正在录音,点击停止
|
||
stopRecording();
|
||
} else {
|
||
// 未录音,点击开始
|
||
startRecording();
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
showConfirmModal({
|
||
title: '提示',
|
||
content: '语音输入功能仅支持 App 端使用',
|
||
showCancel: false
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
// 移除标签
|
||
const removeTag = (index) => {
|
||
const tag = selectedTags.value[index];
|
||
selectedTags.value.splice(index, 1);
|
||
uni.showToast({
|
||
title: `已移除"${tag}"`,
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
};
|
||
|
||
// 输入框聚焦
|
||
const handleInputFocus = () => {
|
||
// 输入框聚焦处理
|
||
};
|
||
|
||
// 输入框失焦
|
||
const handleInputBlur = () => {
|
||
// 输入框失焦处理
|
||
};
|
||
|
||
// 输入确认
|
||
const handleInputConfirm = () => {
|
||
// 检查是否有铸造表单数据
|
||
if (!castloveFormData.value) {
|
||
uni.showToast({
|
||
title: '未找到铸造数据',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 组合标签和输入文字
|
||
const tagsText = selectedTags.value.join(',');
|
||
const fullText = tagsText ? `${tagsText},${dreamInput.value.trim()}` : dreamInput.value.trim();
|
||
|
||
if (!fullText) {
|
||
uni.showToast({
|
||
title: '请选择标签或输入描述',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 显示确认信息
|
||
showConfirmModal({
|
||
title: '确认发送',
|
||
content: `藏品名称: ${castloveFormData.value.name}\nAI描述: ${fullText}`,
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 这里调用后端接口
|
||
sendToBackend(fullText);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 发送数据到后端
|
||
const sendToBackend = (aiDescription) => {
|
||
// 构建后端需要的数据格式(不包含base64图片)
|
||
const requestData = {
|
||
prompt: aiDescription, // AI描述文本
|
||
model: "image-01",
|
||
aspect_ratio: "16:9",
|
||
subject_reference: [
|
||
{
|
||
type: "character",
|
||
image_file: "" // 占位符,实际使用时从castlove_form_data读取
|
||
}
|
||
],
|
||
"n": 4
|
||
};
|
||
|
||
// 将轻量数据存储到本地
|
||
uni.setStorageSync('generation_request_data', JSON.stringify(requestData));
|
||
|
||
// base64图片已经在castlove_form_data中,不需要重复存储
|
||
|
||
// 跳转到加载页面
|
||
uni.navigateTo({
|
||
url: '/pages/discover/generation-loading'
|
||
});
|
||
};
|
||
|
||
onMounted(() => {
|
||
// 随机生成图片
|
||
generateRandomImages();
|
||
|
||
// 初始化录音管理器
|
||
initRecorderManager();
|
||
|
||
// 尝试从存储中读取表单数据
|
||
try {
|
||
const storedData = uni.getStorageSync('castlove_form_data');
|
||
if (storedData) {
|
||
castloveFormData.value = JSON.parse(storedData);
|
||
}
|
||
} catch (e) {
|
||
console.error('[CreateFeed] 读取表单数据失败:', e);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.create-feed {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 背景层(移除,使用父组件背景) */
|
||
.gradient-background {
|
||
display: none;
|
||
}
|
||
|
||
/* 顶部用户 */
|
||
.top-user {
|
||
position: absolute;
|
||
top: 120rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 10;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 50%;
|
||
border: 6rpx solid rgba(255, 255, 255, 0.8);
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.user-name-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.verified-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
/* 散布的元素 */
|
||
.scattered-item {
|
||
position: absolute;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 5;
|
||
animation: float 3s ease-in-out infinite;
|
||
overflow: visible;
|
||
}
|
||
|
||
.scattered-item:nth-child(odd) {
|
||
animation-delay: 0.5s;
|
||
}
|
||
|
||
.scattered-item:nth-child(even) {
|
||
animation-delay: 1s;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%,
|
||
100% {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
50% {
|
||
transform: translateY(-10rpx);
|
||
}
|
||
}
|
||
|
||
/* 图片卡片 */
|
||
.item-card {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.card-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.card-label {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
|
||
text-align: center;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 底部输入框 */
|
||
.bottom-hint {
|
||
position: absolute;
|
||
bottom: 200rpx;
|
||
left: 48rpx;
|
||
right: 48rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
}
|
||
|
||
.input-wrapper {
|
||
width: 100%;
|
||
min-height: 88rpx;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 44rpx;
|
||
backdrop-filter: blur(10rpx);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 16rpx 24rpx;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.selected-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.selected-tag {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 8rpx 20rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 32rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.selected-tag-text {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.selected-tag-close {
|
||
font-size: 32rpx;
|
||
color: #999;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
}
|
||
|
||
.input-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.hint-input {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.95);
|
||
padding: 0 20rpx;
|
||
background: transparent;
|
||
border: none;
|
||
text-align: left;
|
||
}
|
||
|
||
.hint-placeholder {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.input-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.action-icon-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
padding: 8rpx 12rpx;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.voice-btn {
|
||
min-width: 120rpx;
|
||
}
|
||
|
||
.voice-normal,
|
||
.voice-recording {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.action-icon-btn.recording {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
border-radius: 12rpx;
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.9;
|
||
transform: scale(1.05);
|
||
}
|
||
}
|
||
|
||
/* 波形动画 */
|
||
.waveform {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4rpx;
|
||
height: 40rpx;
|
||
width: 80rpx;
|
||
}
|
||
|
||
.waveform-bar {
|
||
width: 6rpx;
|
||
background: linear-gradient(to top, #ff4444, #ff6666);
|
||
border-radius: 3rpx;
|
||
transition: height 0.1s ease;
|
||
min-height: 8rpx;
|
||
}
|
||
|
||
.waveform-bar:nth-child(1) {
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.waveform-bar:nth-child(2) {
|
||
animation-delay: 0.1s;
|
||
}
|
||
|
||
.waveform-bar:nth-child(3) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.waveform-bar:nth-child(4) {
|
||
animation-delay: 0.1s;
|
||
}
|
||
|
||
.waveform-bar:nth-child(5) {
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.action-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 20rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.recording-text {
|
||
color: #ff4444;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.send-btn .action-text {
|
||
color: rgba(255, 255, 255, 0.95);
|
||
font-weight: 600;
|
||
}
|
||
</style>
|