topfans/frontend/pages/castlove/lenticular/lenticular-create.vue
2026-05-17 19:03:34 +08:00

1090 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>
<!-- 主内容区域 -->
<scroll-view
class="content-wrapper"
scroll-y="true"
:show-scrollbar="false"
:enhanced="scrollEnhanced"
>
<!-- 光栅卡必须分别上传背景 + 主体 -->
<view v-if="isLenticularCraft" class="upload-section">
<view class="lenticular-upload-row">
<view class="upload-box upload-box--half" @click="chooseLenticularBg">
<image v-if="uploadedBg" class="uploaded-image" :src="uploadedBg" 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-box upload-box--half" @click="chooseLenticularSubject">
<image v-if="uploadedSubject" class="uploaded-image" :src="uploadedSubject" 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>
<view class="upload-hint">请各上传一张JPG / PNG单张不超过 2MB</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>
</view>
<!-- 底部按钮 -->
<view class="button-section">
<button class="btn-secondary" @click="handleBack">返回</button>
<button class="btn-skip" @click="handleLenticularGenerate">生成</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, computed, onMounted } from 'vue';
import { onLoad,onUnload } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import ConfirmModal from '@/components/ConfirmModal.vue';
import {
buildCastloveFormSnapshot,
CRAFT_LENTICULAR_CN,
} from '@/utils/castloveMintForm.js';
import {
startCraftGenerationFlow,
STUDIO_LENTICULAR,
} from '@/utils/castloveGenerationFlow.js';
// #ifdef MP-WEIXIN
const scrollEnhanced = true;
// #endif
// #ifndef MP-WEIXIN
const scrollEnhanced = false;
// #endif
const pageType = ref('');
const pageName = ref('');
const isLenticularCraft = computed(() => (pageName.value || '').trim() === CRAFT_LENTICULAR_CN);
onUnload(() => {
try {
uni.hideToast();
} catch (e) {
/* noop */
}
try {
uni.hideLoading();
} catch (e) {
/* noop */
}
});
function safeDecodeParam(v) {
if (v == null || v === '') return '';
const s = typeof v === 'string' ? v : String(v);
try {
return decodeURIComponent(s.replace(/\+/g, ' '));
} catch {
return s;
}
}
function applyCreateRouteOptions(options) {
if (!options || typeof options !== 'object') return;
if (options.type) pageType.value = safeDecodeParam(options.type);
if (options.name != null && String(options.name) !== '') {
pageName.value = safeDecodeParam(options.name);
console.log('[LenticularCreate] 工艺名称:', pageName.value);
}
}
// 通用确认弹窗状态
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 pendingLenticularSlot = ref('');
const uploadedBg = ref('');
const uploadedBgBase64 = ref('');
const uploadedSubject = ref('');
const uploadedSubjectBase64 = 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('');
function applyUploadResult(filePath, dataUrl) {
const slot = pendingLenticularSlot.value;
if (slot === 'bg') {
uploadedBg.value = filePath;
uploadedBgBase64.value = dataUrl;
} else if (slot === 'subject') {
uploadedSubject.value = filePath;
uploadedSubjectBase64.value = dataUrl;
} else {
uploadedImage.value = filePath;
uploadedImageBase64.value = dataUrl;
}
pendingLenticularSlot.value = '';
uni.hideLoading();
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
isUploading.value = false;
}
const chooseLenticularBg = () => {
openImagePicker('bg');
};
const chooseLenticularSubject = () => {
openImagePicker('subject');
};
const openImagePicker = (lenticularSlot) => {
if (isUploading.value) {
uni.showToast({ title: '图片上传中,请稍候', icon: 'none' });
return;
}
pendingLenticularSlot.value = lenticularSlot;
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;
const slotLabel = lenticularSlot === 'bg' ? '背景图' : '主体图';
if (fileInfo.size > maxSize) {
pendingLenticularSlot.value = '';
showConfirmModal({
title: '图片不符合要求',
content: `${slotLabel}大小不能超过5MB当前图片大小为${(fileInfo.size / 1024 / 1024).toFixed(2)}MB请重新选择`,
showCancel: false,
confirmText: '知道了'
});
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) {
pendingLenticularSlot.value = '';
showConfirmModal({
title: '图片格式不符合要求',
content: `${slotLabel}只支持JPG和PNG格式的图片请重新选择`,
showCancel: false,
confirmText: '知道了'
});
return;
}
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
originalFileName.value = rawName;
convertImageToBase64(filePath, originalFileName.value);
},
fail: (error) => {
pendingLenticularSlot.value = '';
console.error('获取文件信息失败:', error);
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
}
});
},
fail: (err) => {
pendingLenticularSlot.value = '';
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) => {
applyUploadResult(filePath, e.target.result);
console.log('[LenticularCreate] Base64转换成功 (H5)');
console.log('[LenticularCreate] Base64长度:', (e.target.result && e.target.result.length) || 0);
};
reader.onerror = (error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] Base64转换失败 (H5):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
reader.readAsDataURL(blob);
})
.catch(error => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] 获取图片失败 (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';
const dataUrl = `data:${mimeType};base64,${res.data}`;
applyUploadResult(filePath, dataUrl);
console.log('[LenticularCreate] Base64转换成功 (小程序)');
console.log('[LenticularCreate] Base64长度:', dataUrl.length);
},
fail: (error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] 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) => {
applyUploadResult(filePath, e.target.result);
console.log('[LenticularCreate] Base64转换成功 (App)');
console.log('[LenticularCreate] Base64长度:', (e.target.result && e.target.result.length) || 0);
};
reader.onerror = (error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] Base64转换失败 (App):', error);
uni.hideLoading();
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
isUploading.value = false;
};
reader.readAsDataURL(file);
});
}, (error) => {
pendingLenticularSlot.value = '';
console.error('[LenticularCreate] 读取文件失败 (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
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(resolveH5OssPostUrl(ossData.host), {
method: 'POST',
body: formData
});
})
.then((response) => {
if (response.ok || response.status === 204) {
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
console.log('[LenticularCreate] App环境上传');
const base64ContentApp = base64Data.split(',')[1];
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64ContentApp, () => {
const tempFilePath = `_doc/${fileName}`;
bitmap.save(tempFilePath, {}, () => {
console.log('[LenticularCreate] 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('[LenticularCreate] 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('[LenticularCreate] App上传失败:', error);
reject(error);
}
});
}, (error) => {
console.error('[LenticularCreate] App保存临时文件失败:', error);
bitmap.clear();
reject(new Error('保存临时文件失败'));
});
}, (error) => {
console.error('[LenticularCreate] App加载base64失败:', error);
bitmap.clear();
reject(new Error('加载图片数据失败'));
});
// #endif
});
};
// 返回按钮
const handleBack = async () => {
const hasLenticular =
isLenticularCraft.value &&
(uploadedBg.value || uploadedSubject.value || uploadedBgBase64.value || uploadedSubjectBase64.value);
const hasAny =
uploadedImage.value ||
hasLenticular ||
nftInfo.value ||
aiDescription.value;
if (hasAny) {
showConfirmModal({
title: '提示',
content: '确定要返回吗?未保存的数据将会丢失',
success: async (res) => {
if (res.confirm) {
resetForm();
try {
uni.hideToast();
} catch (e) {
/* noop */
}
uni.navigateBack();
}
}
});
} else {
try {
uni.hideToast();
} catch (e) {
/* noop */
}
uni.navigateBack();
}
};
/** 光栅:上传 → Loading → 选图 → 工作室 */
const buildCraftFormData = () => {
const snap = buildCastloveFormSnapshot({
nftInfo: nftInfo.value,
materialTypes,
materialTypeIndex: materialTypeIndex.value,
pageName: pageName.value,
uploadedImage: uploadedSubject.value,
uploadedImageBase64: uploadedSubjectBase64.value,
});
return {
...snap,
image: isLenticularCraft.value ? uploadedSubject.value : uploadedImage.value,
imageBase64: isLenticularCraft.value ? uploadedSubjectBase64.value : uploadedImageBase64.value,
type: pageType.value,
typeName: pageName.value,
materialType: snap.material_type,
materialTypeIndex: materialTypeIndex.value,
materialTypes: [...materialTypes],
aiDescription: aiDescription.value,
nftInfo: nftInfo.value,
...(isLenticularCraft.value
? {
lenticularBgImage: uploadedBg.value,
lenticularBgBase64: uploadedBgBase64.value,
lenticularSubjectImage: uploadedSubject.value,
lenticularSubjectBase64: uploadedSubjectBase64.value,
}
: {}),
};
};
const buildCraftPrompt = () => {
let fullText = aiDescription.value.trim() || nftInfo.value.trim();
if (pageName.value) {
fullText = `${pageName.value}${fullText}`;
}
return fullText;
};
const startCraftStudioPipeline = (studioKind) => {
if (isLenticularCraft.value) {
if (!uploadedBg.value || !uploadedSubject.value) {
uni.showToast({ title: '请上传背景图与主体图', icon: 'none' });
return;
}
if (!uploadedBgBase64.value || !uploadedSubjectBase64.value) {
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
return;
}
} else {
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 {
const formData = buildCraftFormData();
const result = startCraftGenerationFlow({ formData, studioKind });
if (result && !result.success && result.error) {
showConfirmModal({
title: result.error.title,
content: result.error.content,
showCancel: false,
confirmText: '知道了'
});
return;
}
} catch (e) {
console.error('[LenticularCreate] craft studio pipeline', e);
uni.showToast({ title: '启动失败,请重试', icon: 'none' });
}
};
const handleLenticularGenerate = () => {
startCraftStudioPipeline(STUDIO_LENTICULAR);
};
// 重置表单
const resetForm = () => {
pendingLenticularSlot.value = '';
uploadedBg.value = '';
uploadedBgBase64.value = '';
uploadedSubject.value = '';
uploadedSubjectBase64.value = '';
uploadedImage.value = '';
uploadedImageBase64.value = '';
originalFileName.value = '';
isUploading.value = false;
materialTypeIndex.value = 0;
nftInfo.value = '';
aiDescription.value = '';
};
onLoad((options) => {
applyCreateRouteOptions(options || {});
});
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = (currentPage && currentPage.options) || {};
if (options.name || options.type) {
applyCreateRouteOptions(options);
}
});
</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;
}
.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;
}
/* 图片上传区域 */
.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;
}
.lenticular-upload-row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: stretch;
gap: 20rpx;
padding: 0 4rpx;
box-sizing: border-box;
}
.upload-box--half {
flex: 1;
width: 0;
min-width: 0;
height: 280rpx;
}
.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: 'yt', 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-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: 'yt', 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;
}
/* 底部按钮 */
.button-section {
width: 100%;
display: flex;
gap: 20rpx;
margin-top: 40rpx;
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.btn-secondary,
.btn-skip {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 36rpx;
font-family: 'yt', 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;
}
</style>