1613 lines
41 KiB
Vue
1613 lines
41 KiB
Vue
<template>
|
||
<view class="page-container" :class="{ 'page-container--laser': isLaserCardCraft }">
|
||
|
||
<!-- 背景图片 -->
|
||
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
|
||
<view v-if="isLaserCardCraft" class="laser-bg-veil" aria-hidden="true" />
|
||
|
||
<!-- 镭射卡:左上角关闭 -->
|
||
<view v-if="isLaserCardCraft" class="laser-close-hit" @click="handleBack">
|
||
<text class="laser-close-x">×</text>
|
||
</view>
|
||
|
||
<!-- 主内容区域 -->
|
||
<scroll-view
|
||
class="content-wrapper"
|
||
:class="{ 'content-wrapper--laser': isLaserCardCraft }"
|
||
scroll-y="true"
|
||
:show-scrollbar="false"
|
||
:enhanced="scrollEnhanced"
|
||
>
|
||
<!-- 页面标题 -->
|
||
<!-- <view v-if="pageName" class="page-title">
|
||
<text class="title-text">创建{{ pageName }}</text>
|
||
</view> -->
|
||
|
||
<!-- 光栅卡:必须分别上传背景 + 主体 -->
|
||
<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,单张不超过 5MB</view>
|
||
</view>
|
||
|
||
<!-- 其他工艺:单图上传 -->
|
||
<view v-else class="upload-section" :class="{ 'upload-section--laser': isLaserCardCraft }">
|
||
<view class="upload-box-wrap">
|
||
<view
|
||
:class="isLaserCardCraft ? 'castlove-laser-upload-root' : 'upload-box'"
|
||
@click="onSingleUploadTap"
|
||
>
|
||
<image v-if="uploadedImage" class="uploaded-image" :src="uploadedImage" mode="aspectFill"></image>
|
||
<view v-else-if="isLaserCardCraft" class="upload-laser-stack">
|
||
<image
|
||
class="upload-laser-photo-bg"
|
||
src="/static/castlove/leisheka.png"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="upload-laser-watermark" aria-hidden="true" />
|
||
<view class="upload-laser-inset">
|
||
<view class="upload-laser-dashed-inner">
|
||
<text class="upload-plus upload-plus--laser">+</text>
|
||
<text class="upload-text upload-text--laser">点击上传图片</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="upload-placeholder">
|
||
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit" />
|
||
<text class="upload-text">点击上传图片</text>
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-if="isLaserCardCraft && uploadedImage"
|
||
class="upload-clear upload-clear--laser"
|
||
@click.stop="clearSingleUploadedImage"
|
||
>
|
||
<text class="upload-clear-x upload-clear-x--laser">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="upload-hint">{{ isLaserCardCraft ? '支持JPG.PNG格式,大小不超过5M' : '支持JPG、PNG格式,大小不超过5MB' }}</view>
|
||
</view>
|
||
|
||
<!-- 表单区域 -->
|
||
<view class="form-section" :class="{ 'form-section--laser': isLaserCardCraft }">
|
||
<!-- 素材类型选择器 -->
|
||
<view class="form-item form-item-picker">
|
||
<text class="form-label" :class="{ 'form-label--laser': isLaserCardCraft }">素材类型</text>
|
||
<view class="custom-picker">
|
||
<view class="picker-display" :class="{ 'picker-display--laser': isLaserCardCraft }" @click="toggleMaterialTypePicker">
|
||
<text class="picker-text">{{ materialTypes[materialTypeIndex] }}</text>
|
||
<text
|
||
class="picker-arrow"
|
||
:class="{
|
||
'picker-arrow-up': showMaterialTypePicker,
|
||
'picker-arrow--laser': isLaserCardCraft,
|
||
}"
|
||
>{{ isLaserCardCraft ? '▼' : '›' }}</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" :class="{ 'form-label--laser': isLaserCardCraft }">藏品信息</text>
|
||
<textarea
|
||
class="form-textarea"
|
||
:class="{ 'form-textarea--laser': isLaserCardCraft }"
|
||
v-model="nftInfo"
|
||
placeholder="请输入藏品相关信息"
|
||
placeholder-class="textarea-placeholder"
|
||
maxlength="500"
|
||
auto-height
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI描述区域 -->
|
||
<view class="ai-section" :class="{ 'ai-section--laser': isLaserCardCraft }">
|
||
<text class="section-title" :class="{ 'section-title--laser': isLaserCardCraft }">{{ isLaserCardCraft ? 'AI生成描述' : 'AI 生成描述' }}</text>
|
||
|
||
<!-- AI描述输入框 -->
|
||
<view class="ai-input-wrapper" :class="{ 'ai-input-wrapper--laser': isLaserCardCraft }">
|
||
<textarea
|
||
class="ai-input"
|
||
v-model="aiDescription"
|
||
placeholder="背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?"
|
||
placeholder-class="ai-placeholder"
|
||
auto-height
|
||
:show-confirm-bar="false"
|
||
maxlength="500"
|
||
/>
|
||
<view v-if="!isLaserCardCraft" 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" :class="{ 'button-section--laser': isLaserCardCraft }">
|
||
<button class="btn-secondary" :class="{ 'btn-secondary--laser': isLaserCardCraft }" @click="handleBack">返回</button>
|
||
<button v-if="isLenticularCraft" class="btn-skip" @click="handleLenticularGenerate">生成</button>
|
||
<button
|
||
v-else
|
||
:class="isLaserCardCraft ? 'btn-confirm-laser' : 'btn-skip'"
|
||
@click="handleSingleCraftLaserEntry"
|
||
>
|
||
{{ isLaserCardCraft ? '确认' : '生成' }}
|
||
</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>
|
||
export default {
|
||
inheritAttrs: false,
|
||
};
|
||
</script>
|
||
|
||
<script setup>
|
||
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 './create-laser-upload.css';
|
||
import {
|
||
buildCastloveFormSnapshot,
|
||
CRAFT_LENTICULAR_CN,
|
||
CRAFT_LASER_CARD_CN,
|
||
CASTLOVE_LASER_ENTRY_KEY,
|
||
LENTICULAR_STUDIO_STORAGE_KEY,
|
||
} from '@/utils/castloveMintForm.js';
|
||
|
||
// 获取页面参数(H5 必须用 onLoad 读 query;仅靠 onMounted + getCurrentPages().options 常为 undefined)
|
||
const pageType = ref('');
|
||
const pageName = ref('');
|
||
// #ifdef MP-WEIXIN
|
||
const scrollEnhanced = true;
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
const scrollEnhanced = false;
|
||
// #endif
|
||
|
||
const isLenticularCraft = computed(() => (pageName.value || '').trim() === CRAFT_LENTICULAR_CN);
|
||
const isLaserCardCraft = computed(() => (pageName.value || '').trim() === CRAFT_LASER_CARD_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('[CreatePage] 工艺名称:', 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 chooseImage = () => {
|
||
openImagePicker('');
|
||
};
|
||
|
||
const onSingleUploadTap = () => {
|
||
chooseImage();
|
||
};
|
||
|
||
const clearSingleUploadedImage = () => {
|
||
uploadedImage.value = '';
|
||
uploadedImageBase64.value = '';
|
||
originalFileName.value = '';
|
||
};
|
||
|
||
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;
|
||
if (fileInfo.size > maxSize) {
|
||
pendingLenticularSlot.value = '';
|
||
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) {
|
||
pendingLenticularSlot.value = '';
|
||
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) => {
|
||
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('[CreatePage] Base64转换成功 (H5)');
|
||
console.log('[CreatePage] Base64长度:', (e.target.result && e.target.result.length) || 0);
|
||
};
|
||
reader.onerror = (error) => {
|
||
pendingLenticularSlot.value = '';
|
||
console.error('[CreatePage] Base64转换失败 (H5):', error);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||
isUploading.value = false;
|
||
};
|
||
reader.readAsDataURL(blob);
|
||
})
|
||
.catch(error => {
|
||
pendingLenticularSlot.value = '';
|
||
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';
|
||
const dataUrl = `data:${mimeType};base64,${res.data}`;
|
||
applyUploadResult(filePath, dataUrl);
|
||
console.log('[CreatePage] Base64转换成功 (小程序)');
|
||
console.log('[CreatePage] Base64长度:', dataUrl.length);
|
||
},
|
||
fail: (error) => {
|
||
pendingLenticularSlot.value = '';
|
||
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) => {
|
||
applyUploadResult(filePath, e.target.result);
|
||
console.log('[CreatePage] Base64转换成功 (App)');
|
||
console.log('[CreatePage] Base64长度:', (e.target.result && e.target.result.length) || 0);
|
||
};
|
||
reader.onerror = (error) => {
|
||
pendingLenticularSlot.value = '';
|
||
console.error('[CreatePage] Base64转换失败 (App):', error);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||
isUploading.value = false;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}, (error) => {
|
||
pendingLenticularSlot.value = '';
|
||
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:仍走原有 OSS PostObject;本地开发经 manifest devServer /dev-oss-proxy 转发,避免 CORS
|
||
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
|
||
// App环境 - 将base64转为临时文件后上传
|
||
console.log('[CreatePage] App环境上传');
|
||
const base64ContentApp = 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 () => {
|
||
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();
|
||
}
|
||
};
|
||
|
||
/** 光栅卡:双图校验后进入工作室预览 */
|
||
const handleLenticularGenerate = () => {
|
||
if (!uploadedBg.value || !uploadedSubject.value) {
|
||
uni.showToast({ title: '请上传背景图与主体图', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!uploadedBgBase64.value || !uploadedSubjectBase64.value) {
|
||
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!nftInfo.value.trim()) {
|
||
uni.showToast({ title: '请输入藏品信息', icon: 'none' });
|
||
return;
|
||
}
|
||
try {
|
||
uni.setStorageSync(
|
||
LENTICULAR_STUDIO_STORAGE_KEY,
|
||
JSON.stringify({
|
||
bgPath: uploadedBg.value,
|
||
subjectPath: uploadedSubject.value,
|
||
bgBase64: uploadedBgBase64.value,
|
||
subjectBase64: uploadedSubjectBase64.value,
|
||
nftInfo: nftInfo.value,
|
||
materialTypeIndex: materialTypeIndex.value,
|
||
aiDescription: aiDescription.value,
|
||
})
|
||
);
|
||
} catch (e) {
|
||
console.error('[CreatePage] lenticular studio payload', e);
|
||
uni.showToast({ title: '保存失败,请重试', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: '/pages/castlove/lenticular-studio',
|
||
});
|
||
};
|
||
|
||
/** 单图工艺:进入镭射工坊(Laser-Card 配置页),保存后再上传并创建订单 */
|
||
const handleSingleCraftLaserEntry = () => {
|
||
if (isLenticularCraft.value) {
|
||
uni.showToast({ title: '光栅卡请使用「生成」进入工作室', icon: 'none' });
|
||
return;
|
||
}
|
||
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.setStorageSync(
|
||
CASTLOVE_LASER_ENTRY_KEY,
|
||
JSON.stringify({
|
||
nftInfo: nftInfo.value,
|
||
materialTypes: [...materialTypes],
|
||
materialTypeIndex: materialTypeIndex.value,
|
||
pageName: pageName.value,
|
||
uploadedImage: uploadedImage.value,
|
||
uploadedImageBase64: uploadedImageBase64.value,
|
||
})
|
||
);
|
||
} catch (e) {
|
||
console.error('[CreatePage] laser entry payload', e);
|
||
uni.showToast({ title: '保存失败,请重试', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: '/pages/castlove/laser-card-studio',
|
||
});
|
||
};
|
||
|
||
// 底部导航切换
|
||
const handleTabChange = (newTab) => {
|
||
const routes = [
|
||
'/pages/square/square',
|
||
'/pages/starbook/index',
|
||
'/pages/castlove/mall',
|
||
'/pages/starcity/index',
|
||
'/pages/ai-dazi/index'
|
||
];
|
||
|
||
if (newTab >= 0 && newTab < routes.length) {
|
||
uni.navigateTo({ url: routes[newTab] });
|
||
}
|
||
};
|
||
|
||
// 开始生成
|
||
const handleGenerate = async () => {
|
||
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;
|
||
}
|
||
|
||
// 组合输入文字(如果有页面名称,作为隐藏前缀)
|
||
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 snap = isLenticularCraft.value
|
||
? buildCastloveFormSnapshot({
|
||
nftInfo: nftInfo.value,
|
||
materialTypes,
|
||
materialTypeIndex: materialTypeIndex.value,
|
||
pageName: pageName.value,
|
||
uploadedImage: uploadedSubject.value,
|
||
uploadedImageBase64: uploadedSubjectBase64.value,
|
||
})
|
||
: buildCastloveFormSnapshot({
|
||
nftInfo: nftInfo.value,
|
||
materialTypes,
|
||
materialTypeIndex: materialTypeIndex.value,
|
||
pageName: pageName.value,
|
||
uploadedImage: uploadedImage.value,
|
||
uploadedImageBase64: uploadedImageBase64.value,
|
||
});
|
||
|
||
const formData = {
|
||
...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,
|
||
...(isLenticularCraft.value
|
||
? {
|
||
lenticularBgImage: uploadedBg.value,
|
||
lenticularBgBase64: uploadedBgBase64.value,
|
||
lenticularSubjectImage: uploadedSubject.value,
|
||
lenticularSubjectBase64: uploadedSubjectBase64.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 = () => {
|
||
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;
|
||
}
|
||
|
||
.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: 'yt', 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;
|
||
}
|
||
|
||
.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-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: '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;
|
||
}
|
||
|
||
.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: '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;
|
||
}
|
||
|
||
/* ========== 镭射卡设计稿专用 ========== */
|
||
.page-container--laser {
|
||
position: relative;
|
||
}
|
||
|
||
.laser-bg-veil {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
background:
|
||
radial-gradient(2rpx 2rpx at 10% 18%, rgba(255, 255, 255, 0.55), transparent 55%),
|
||
radial-gradient(1.5rpx 1.5rpx at 78% 12%, rgba(255, 255, 255, 0.45), transparent 55%),
|
||
radial-gradient(2rpx 2rpx at 42% 8%, rgba(255, 255, 255, 0.4), transparent 50%),
|
||
radial-gradient(1.5rpx 1.5rpx at 88% 46%, rgba(255, 255, 255, 0.5), transparent 50%),
|
||
radial-gradient(2rpx 2rpx at 18% 62%, rgba(255, 255, 255, 0.35), transparent 50%),
|
||
radial-gradient(1.5rpx 1.5rpx at 64% 72%, rgba(255, 255, 255, 0.45), transparent 50%),
|
||
radial-gradient(2rpx 2rpx at 36% 88%, rgba(255, 255, 255, 0.4), transparent 50%),
|
||
radial-gradient(1.5rpx 1.5rpx at 92% 84%, rgba(255, 255, 255, 0.35), transparent 50%),
|
||
linear-gradient(
|
||
165deg,
|
||
rgba(255, 182, 220, 0.16) 0%,
|
||
rgba(140, 100, 210, 0.22) 42%,
|
||
rgba(20, 40, 80, 0.2) 100%
|
||
);
|
||
}
|
||
|
||
.laser-close-hit {
|
||
position: fixed;
|
||
top: calc(16rpx + constant(safe-area-inset-top));
|
||
top: calc(16rpx + env(safe-area-inset-top));
|
||
left: 24rpx;
|
||
z-index: 20;
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
background: linear-gradient(
|
||
135deg,
|
||
#ffb7e8 0%,
|
||
#c9a6ff 28%,
|
||
#8fd4ff 55%,
|
||
#fff3b0 78%,
|
||
#ffa8d8 100%
|
||
);
|
||
background-size: 180% 180%;
|
||
border: 2rpx solid rgba(255, 255, 255, 0.55);
|
||
box-shadow: 0 6rpx 22rpx rgba(120, 60, 160, 0.35);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.laser-close-x {
|
||
color: #ffffff;
|
||
font-size: 44rpx;
|
||
line-height: 1;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.content-wrapper--laser {
|
||
padding-top: calc(112rpx + constant(safe-area-inset-top));
|
||
padding-top: calc(112rpx + env(safe-area-inset-top));
|
||
}
|
||
|
||
.upload-section--laser .upload-box-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
max-width: 640rpx;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.upload-section--laser .upload-hint {
|
||
color: rgba(255, 255, 255, 0.88);
|
||
font-size: 22rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.upload-section--laser .upload-clear {
|
||
position: absolute;
|
||
top: 14rpx;
|
||
right: 14rpx;
|
||
z-index: 6;
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.upload-clear--laser {
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 6rpx 18rpx rgba(60, 30, 100, 0.22);
|
||
}
|
||
|
||
.upload-clear-x--laser {
|
||
color: #3d6fd8;
|
||
font-size: 34rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.form-section--laser {
|
||
gap: 36rpx;
|
||
}
|
||
|
||
.form-label--laser,
|
||
.section-title--laser {
|
||
font-size: 28rpx;
|
||
padding: 10rpx 22rpx;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(95deg, #ff7ec8 0%, #ff9f7a 48%, #ffb35c 100%);
|
||
color: #ffffff;
|
||
text-shadow: 0 2rpx 6rpx rgba(160, 40, 80, 0.35);
|
||
box-shadow: 0 4rpx 14rpx rgba(255, 100, 140, 0.28);
|
||
}
|
||
|
||
.ai-section--laser {
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.picker-display--laser {
|
||
background: rgba(255, 255, 255, 0.32);
|
||
backdrop-filter: blur(22rpx);
|
||
-webkit-backdrop-filter: blur(22rpx);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||
box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.12);
|
||
}
|
||
|
||
.form-textarea--laser {
|
||
background: rgba(255, 255, 255, 0.32);
|
||
backdrop-filter: blur(22rpx);
|
||
-webkit-backdrop-filter: blur(22rpx);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.42);
|
||
box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.1);
|
||
}
|
||
|
||
.picker-arrow.picker-arrow--laser {
|
||
transform: rotate(0deg);
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
color: rgba(255, 255, 255, 0.88);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.picker-arrow.picker-arrow--laser.picker-arrow-up {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.ai-input-wrapper--laser {
|
||
flex-direction: column;
|
||
min-height: 280rpx;
|
||
padding: 24rpx 28rpx 28rpx;
|
||
background: rgba(255, 255, 255, 0.32);
|
||
backdrop-filter: blur(22rpx);
|
||
-webkit-backdrop-filter: blur(22rpx);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||
box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.1);
|
||
}
|
||
|
||
.ai-input-wrapper--laser .ai-input {
|
||
min-height: 220rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.button-section--laser {
|
||
gap: 24rpx;
|
||
margin-top: 48rpx;
|
||
}
|
||
|
||
.btn-secondary--laser {
|
||
background: rgba(255, 255, 255, 0.18);
|
||
border-color: rgba(255, 255, 255, 0.35);
|
||
}
|
||
|
||
.btn-confirm-laser {
|
||
flex: 1;
|
||
height: 96rpx;
|
||
line-height: 96rpx;
|
||
border-radius: 48rpx;
|
||
font-size: 36rpx;
|
||
font-family: 'yt', sans-serif;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(95deg, #ff8ec5 0%, #ff5a8c 38%, #ff2d6b 72%, #ff4d64 100%);
|
||
box-shadow: 0 10rpx 32rpx rgba(255, 50, 110, 0.42);
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.btn-confirm-laser::after {
|
||
border: none;
|
||
}
|
||
|
||
.btn-confirm-laser:active {
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.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>
|