topfans/frontend/pages/castlove/create.vue
2026-04-13 17:34:03 +08:00

1184 lines
28 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="page-container">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<!-- <view class="background-overlay"></view> -->
<!-- 主内容区域 -->
<scroll-view class="content-wrapper" scroll-y="true" :show-scrollbar="false" :enhanced="true">
<!-- 页面标题 -->
<!-- <view v-if="pageName" class="page-title">
<text class="title-text">创建{{ pageName }}</text>
</view> -->
<!-- 图片上传区域 -->
<view class="upload-section">
<view class="upload-box" @click="chooseImage">
<image v-if="uploadedImage" class="uploaded-image" :src="uploadedImage" mode="aspectFit"></image>
<view v-else class="upload-placeholder">
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
<text class="upload-text">点击上传图片</text>
</view>
</view>
<view class="upload-hint">支持JPG、PNG格式大小不超过5MB</view>
</view>
<!-- 表单区域 -->
<view class="form-section">
<!-- 素材类型选择器 -->
<view class="form-item form-item-picker">
<text class="form-label">素材类型</text>
<view class="custom-picker">
<view class="picker-display" @click="toggleMaterialTypePicker">
<text class="picker-text">{{ materialTypes[materialTypeIndex] }}</text>
<text class="picker-arrow" :class="{ 'picker-arrow-up': showMaterialTypePicker }"></text>
</view>
<view v-if="showMaterialTypePicker" class="picker-options">
<view v-for="(type, index) in materialTypes" :key="index" class="picker-option"
:class="{ 'picker-option-active': materialTypeIndex === index }"
@click.stop="selectMaterialType(index)">
<text class="picker-option-text">{{ type }}</text>
<text v-if="materialTypeIndex === index" class="picker-option-check">✓</text>
</view>
</view>
</view>
</view>
<!-- 藏品信息 -->
<view class="form-item">
<text class="form-label">藏品信息</text>
<textarea class="form-textarea" v-model="nftInfo" placeholder="请输入藏品相关信息"
placeholder-class="textarea-placeholder" maxlength="500" auto-height :show-confirm-bar="false" />
</view>
</view>
<!-- AI描述区域 -->
<view class="ai-section">
<text class="section-title">AI 生成描述</text>
<!-- AI描述输入框 -->
<view class="ai-input-wrapper">
<textarea
class="ai-input"
v-model="aiDescription"
placeholder="背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?"
placeholder-class="ai-placeholder"
auto-height
:show-confirm-bar="false"
maxlength="500"
/>
<view class="input-actions">
<!-- 发送按钮 -->
<view class="action-icon-btn send-btn" @click="handleGenerate">
<image class="action-icon" src="/static/icon/send.png" mode="aspectFit" />
<text class="action-text">发送</text>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="button-section">
<button class="btn-secondary" @click="handleBack">返回</button>
<button class="btn-skip" @click="handleSkip">跳过</button>
</view>
</scroll-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>
defineOptions({
inheritAttrs: false
})
import { ref, onMounted } from 'vue';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import ConfirmModal from '@/components/ConfirmModal.vue';
// 获取页面参数
const pageType = ref('');
const pageName = ref('');
// 通用确认弹窗状态
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 uploadedImage = ref('');
const uploadedImageBase64 = ref('');
const originalFileName = ref('');
const isUploading = ref(false);
const materialTypes = ['粉丝自制', '热爱痕迹', '其他'];
const materialTypeIndex = ref(0);
const showMaterialTypePicker = ref(false);
const nftInfo = ref(''); // 藏品信息(合并了名称和事件)
// AI描述相关
const aiDescription = ref('');
// 风格标签数据 - 已移除
// 选择图片
const chooseImage = () => {
if (isUploading.value) {
uni.showToast({ title: '图片上传中,请稍候', icon: 'none' });
return;
}
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
const filePath = res.tempFilePaths[0];
const tempFile = res.tempFiles && res.tempFiles[0];
uni.getFileInfo({
filePath: filePath,
success: (fileInfo) => {
const maxSize = 5 * 1024 * 1024;
if (fileInfo.size > maxSize) {
uni.showToast({ title: '图片大小不能超过5MB', icon: 'none', duration: 2000 });
return;
}
const mimeType = (tempFile && tempFile.type) ? tempFile.type.toLowerCase() : '';
const pathLower = filePath.toLowerCase();
const validByMime = mimeType === 'image/jpeg' || mimeType === 'image/png';
const validByExt = pathLower.endsWith('.jpg') || pathLower.endsWith('.jpeg') || pathLower.endsWith('.png');
if (!validByMime && !validByExt) {
uni.showToast({ title: '只支持JPG和PNG格式', icon: 'none', duration: 2000 });
return;
}
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
originalFileName.value = rawName;
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
console.error('获取文件信息失败:', error);
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
}
});
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
});
};
// 将图片转换为 base64
const convertImageToBase64 = (filePath, fileName) => {
isUploading.value = true;
uni.showLoading({ title: '处理中...', mask: true });
// #ifdef H5
convertImageToBase64H5(filePath, fileName);
// #endif
// #ifndef H5
convertImageToBase64Native(filePath, fileName);
// #endif
};
const convertImageToBase64H5 = (filePath, fileName) => {
fetch(filePath)
.then(res => res.blob())
.then(blob => {
const reader = new FileReader();
reader.onload = (e) => {
// e.target.result 已经是完整的 data:image/xxx;base64,xxx 格式
uploadedImageBase64.value = e.target.result;
uploadedImage.value = filePath;
console.log('[CreatePage] Base64转换成功 (H5)');
console.log('[CreatePage] Base64长度:', uploadedImageBase64.value.length);
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
};
reader.onerror = (error) => {
console.error('[CreatePage] Base64转换失败 (H5):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
reader.readAsDataURL(blob);
})
.catch(error => {
console.error('[CreatePage] 获取图片失败 (H5):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
});
};
const convertImageToBase64Native = (filePath, fileName) => {
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
const fs = uni.getFileSystemManager();
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
const ext = fileName.toLowerCase().split('.').pop();
let mimeType = 'image/jpeg';
if (ext === 'png') mimeType = 'image/png';
uploadedImageBase64.value = `data:${mimeType};base64,${res.data}`;
uploadedImage.value = filePath;
console.log('[CreatePage] Base64转换成功 (小程序)');
console.log('[CreatePage] Base64长度:', uploadedImageBase64.value.length);
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
},
fail: (error) => {
console.error('[CreatePage] Base64转换失败 (小程序):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
}
});
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
uploadedImageBase64.value = e.target.result;
uploadedImage.value = filePath;
console.log('[CreatePage] Base64转换成功 (App)');
console.log('[CreatePage] Base64长度:', uploadedImageBase64.value.length);
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
};
reader.onerror = (error) => {
console.error('[CreatePage] Base64转换失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
reader.readAsDataURL(file);
});
}, (error) => {
console.error('[CreatePage] 读取文件失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
});
// #endif
};
// 切换素材类型选择器
const toggleMaterialTypePicker = () => {
showMaterialTypePicker.value = !showMaterialTypePicker.value;
};
const selectMaterialType = (index) => {
materialTypeIndex.value = index;
showMaterialTypePicker.value = false;
};
// 上传图片到OSS的辅助函数
const uploadImageToOss = async (base64Data, ossData) => {
return new Promise((resolve, reject) => {
const fileName = `${Date.now()}.jpg`;
// #ifdef H5
// H5环境直接使用base64
fetch(base64Data)
.then(res => res.blob())
.then(blob => {
const formData = new FormData();
formData.append('key', ossData.dir + fileName);
formData.append('policy', ossData.policy);
formData.append('success_action_status', '200');
formData.append('x-oss-credential', ossData.x_oss_credential);
formData.append('x-oss-date', ossData.x_oss_date);
formData.append('x-oss-security-token', ossData.security_token);
formData.append('x-oss-signature', ossData.signature);
formData.append('x-oss-signature-version', ossData.x_oss_signature_version);
formData.append('file', blob, fileName);
return fetch(ossData.host, {
method: 'POST',
body: formData
});
})
.then(response => {
if (response.ok) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error('上传失败'));
}
})
.catch(error => {
reject(error);
});
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
// 小程序环境 - 使用文件系统管理器
const base64Content = base64Data.split(',')[1];
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
const fs = uni.getFileSystemManager();
fs.writeFile({
filePath: filePath,
data: base64Content,
encoding: 'base64',
success: () => {
uni.uploadFile({
url: ossData.host,
filePath: filePath,
name: 'file',
formData: {
key: ossData.dir + fileName,
policy: ossData.policy,
success_action_status: '200',
'x-oss-credential': ossData.x_oss_credential,
'x-oss-date': ossData.x_oss_date,
'x-oss-security-token': ossData.security_token,
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
reject(error);
}
});
},
fail: (error) => {
reject(error);
}
});
// #endif
// #ifdef APP-PLUS
// App环境 - 将base64转为临时文件后上传
console.log('[CreatePage] App环境上传');
const base64Content = base64Data.split(',')[1];
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Data, () => {
const tempFilePath = `_doc/${fileName}`;
bitmap.save(tempFilePath, {}, () => {
console.log('[CreatePage] App临时文件保存成功:', tempFilePath);
bitmap.clear();
uni.uploadFile({
url: ossData.host,
filePath: tempFilePath,
name: 'file',
formData: {
key: ossData.dir + fileName,
policy: ossData.policy,
success_action_status: '200',
'x-oss-credential': ossData.x_oss_credential,
'x-oss-date': ossData.x_oss_date,
'x-oss-security-token': ossData.security_token,
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
console.log('[CreatePage] App上传响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(imageUrl);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
console.error('[CreatePage] App上传失败:', error);
reject(error);
}
});
}, (error) => {
console.error('[CreatePage] App保存临时文件失败:', error);
bitmap.clear();
reject(new Error('保存临时文件失败'));
});
}, (error) => {
console.error('[CreatePage] App加载base64失败:', error);
bitmap.clear();
reject(new Error('加载图片数据失败'));
});
// #endif
});
};
// 返回按钮
const handleBack = async () => {
if (uploadedImage.value || nftInfo.value || aiDescription.value) {
showConfirmModal({
title: '提示',
content: '确定要返回吗?未保存的数据将会丢失',
success: async (res) => {
if (res.confirm) {
uni.redirectTo({
url: '/pages/castlove/mall'
});
}
}
});
} else {
uni.redirectTo({
url: '/pages/castlove/mall'
});
}
};
// 跳过按钮 - 直接创建订单跳过AI生成
const handleSkip = async () => {
// 表单验证
if (!uploadedImage.value) {
uni.showToast({ title: '请上传藏品图片', icon: 'none' });
return;
}
if (!uploadedImageBase64.value) {
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
return;
}
if (!nftInfo.value.trim()) {
uni.showToast({ title: '请输入藏品信息', icon: 'none' });
return;
}
try {
// 显示加载提示
uni.showLoading({ title: '上传图片中...', mask: true });
// 获取OSS签名
const signRes = await getOssSignatureApi('asset');
if (signRes.code !== 200) {
throw new Error(signRes.message || '获取签名失败');
}
const orderId = signRes.data.order_id || '';
// 上传原图到OSS
const imageUrl = await uploadImageToOss(uploadedImageBase64.value, signRes.data);
// 更新加载提示
uni.showLoading({ title: '创建订单中...', mask: true });
// 构建订单数据
const orderData = {
name: pageName.value || '藏品',
event: nftInfo.value.trim(),
info: nftInfo.value.trim(), // 添加必填的 info 字段
description: '',
material_type: materialTypes[materialTypeIndex.value],
material_url: imageUrl,
rarity: 0,
tags: [],
order_id: orderId
};
// 调用创建铸造订单API
const response = await createMintOrderApi(orderData);
uni.hideLoading();
if (response.code !== 200) {
throw new Error(response.message || '创建订单失败');
}
// 构建藏品数据存储到temp_nft_data
const nftData = {
image: imageUrl,
name: pageName.value || '藏品',
event: nftInfo.value.trim(),
info: nftInfo.value.trim(), // 添加 info 字段
description: '',
material_type: materialTypes[materialTypeIndex.value],
order_id: orderId
};
// 存储到storage
uni.setStorageSync('temp_nft_data', JSON.stringify(nftData));
// 清空表单
// resetForm();
// 跳转到成功页面
uni.redirectTo({
url: '/pages/castlove/success'
});
} catch (error) {
console.error('[CreatePage] 跳过失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败,请重试',
icon: 'none'
});
}
};
// 底部导航切换
const handleTabChange = (newTab) => {
const routes = [
'/pages/square/square',
'/pages/starbook/index',
'/pages/castlove/mall',
'/pages/starcity/index',
'/pages/friends/index'
];
if (newTab >= 0 && newTab < routes.length) {
uni.redirectTo({ url: routes[newTab] });
}
};
// 开始生成
const handleGenerate = async () => {
// 表单验证
if (!uploadedImage.value) {
uni.showToast({ title: '请上传藏品图片', icon: 'none' });
return;
}
if (!uploadedImageBase64.value) {
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
return;
}
if (!nftInfo.value.trim()) {
uni.showToast({ title: '请输入藏品信息', icon: 'none' });
return;
}
// 组合输入文字(如果有页面名称,作为隐藏前缀)
let fullText = aiDescription.value.trim();
if (pageName.value) {
fullText = `${pageName.value}${fullText}`;
}
if (!fullText || fullText === `${pageName.value}`) {
uni.showToast({ title: '请输入AI描述', icon: 'none' });
return;
}
// 保存表单数据
const formData = {
image: uploadedImage.value,
imageBase64: uploadedImageBase64.value,
info: nftInfo.value.trim(), // 藏品信息
materialType: materialTypes[materialTypeIndex.value],
type: pageType.value, // 添加类型信息
typeName: pageName.value // 添加类型名称
};
try {
uni.setStorageSync('castlove_form_data', JSON.stringify(formData));
} catch (e) {
console.error('保存表单数据失败:', e);
return;
}
// 发送到后端生成
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));
console.log('[CreatePage] 生成请求数据已保存');
// base64图片已经在castlove_form_data中不需要重复存储
// 清空表单
// resetForm();
// 跳转到加载页面
uni.navigateTo({
url: '/pages/discover/generation-loading'
});
};
// 重置表单
const resetForm = () => {
uploadedImage.value = '';
uploadedImageBase64.value = '';
originalFileName.value = '';
isUploading.value = false;
materialTypeIndex.value = 0;
nftInfo.value = '';
aiDescription.value = '';
};
onMounted(() => {
// 获取页面参数
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options || {};
if (options.type) {
pageType.value = options.type;
console.log('[CreatePage] 页面类型:', pageType.value);
}
if (options.name) {
pageName.value = decodeURIComponent(options.name);
console.log('[CreatePage] 页面名称:', pageName.value);
// 页面名称将作为隐藏前缀,不显示在输入框中
}
});
</script>
<style scoped>
.page-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.background-image {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
object-fit: cover;
}
.background-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
background: rgba(0, 0, 0, 0.3);
}
.content-wrapper {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
padding: 100rpx 32rpx 40rpx 32rpx;
box-sizing: border-box;
}
/* 隐藏滚动条 */
.content-wrapper::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* 页面标题 */
.page-title {
width: 100%;
text-align: center;
margin-bottom: 40rpx;
}
.title-text {
font-size: 48rpx;
color: #ffffff;
font-weight: 700;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.5);
background-clip: text;
}
/* 图片上传区域 */
.upload-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.upload-box {
width: 300rpx;
height: 300rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
border: 4rpx dashed rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
}
.upload-hint {
margin-top: 20rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
text-align: center;
}
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.upload-icon {
width: 80rpx;
height: 80rpx;
opacity: 0.8;
}
.upload-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
/* 表单区域 */
.form-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 30rpx;
margin-bottom: 40rpx;
}
.form-item {
width: 100%;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.form-item-picker {
position: relative;
z-index: 10;
}
.form-label {
font-size: 36rpx;
color: #e6e6e6;
font-weight: 500;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
padding: 18rpx 18rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 44rpx;
display: inline-block;
align-self: flex-start;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
}
/* 自定义选择器 */
.custom-picker {
position: relative;
width: 100%;
z-index: 10;
}
.picker-display {
width: 100%;
height: 88rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.picker-display:active {
background: rgba(255, 255, 255, 0.2);
}
.picker-text {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.picker-arrow {
font-size: 48rpx;
color: rgba(255, 255, 255, 0.7);
font-weight: bold;
transform: rotate(90deg);
transition: transform 0.3s ease;
}
.picker-arrow-up {
transform: rotate(-90deg);
}
.picker-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
width: 100%;
margin-top: 10rpx;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(15rpx);
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
z-index: 10;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.picker-option {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
transition: background 0.2s ease;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
.picker-option:last-child {
border-bottom: none;
}
.picker-option:active {
background: rgba(255, 255, 255, 0.15);
}
.picker-option-active {
background: rgba(255, 255, 255, 0.1);
}
.picker-option-text {
font-size: 32rpx;
color: #e6e6e6;
}
.picker-option-check {
font-size: 36rpx;
color: #e6e6e6;
font-weight: bold;
}
.form-input {
width: 100%;
height: 88rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
padding: 0 30rpx;
font-size: 32rpx;
color: #e6e6e6;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.input-placeholder {
color: rgba(255, 255, 255, 0.85);
font-size: 32rpx;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.form-textarea {
width: 100%;
min-height: 200rpx;
max-height: 400rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
padding: 20rpx 30rpx;
font-size: 32rpx;
color: #e6e6e6;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
line-height: 1.5;
}
.textarea-placeholder {
color: rgba(255, 255, 255, 0.85);
font-size: 32rpx;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
/* AI描述区域 */
.ai-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 40rpx;
}
.section-title {
font-size: 36rpx;
color: #e6e6e6;
font-weight: 500;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
padding: 18rpx 18rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 44rpx;
display: inline-block;
align-self: flex-start;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
}
/* AI输入框 */
.ai-input-wrapper {
width: 100%;
min-height: 200rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
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;
align-items: flex-start;
padding: 20rpx 24rpx;
gap: 16rpx;
box-sizing: border-box;
}
.ai-input {
flex: 1;
min-height: 192rpx;
max-height: 400rpx;
font-size: 32rpx;
color: rgba(255, 255, 255, 0.95);
padding: 0;
background: transparent;
border: none;
line-height: 1.5;
box-sizing: border-box;
}
.ai-placeholder {
color: rgba(255, 255, 255, 0.6);
font-size: 32rpx;
}
.input-actions {
display: flex;
align-items: flex-end;
padding-top: 120rpx;
}
.action-icon-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
padding: 8rpx 12rpx;
transition: all 0.3s ease;
}
.action-icon {
width: 40rpx;
height: 40rpx;
}
.action-text {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
}
.send-btn .action-text {
color: rgba(255, 255, 255, 0.95);
font-weight: 600;
}
.send-btn:active {
opacity: 0.8;
}
/* 底部按钮 */
.button-section {
width: 100%;
display: flex;
gap: 20rpx;
margin-top: 40rpx;
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom)); /* iOS 11.0 */
padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
}
.btn-secondary,
.btn-skip {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 36rpx;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10rpx);
color: #e6e6e6;
border: 2rpx solid rgba(255, 255, 255, 0.4);
}
.btn-secondary::after {
border: none;
}
.btn-skip {
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
color: #e6e6e6;
}
.btn-skip::after {
border: none;
}
.btn-secondary:active {
background: rgba(255, 255, 255, 0.3);
}
.btn-skip:active {
opacity: 0.9;
}
.nav-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>