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

1382 lines
32 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="ai-generate-container" :style="{ paddingTop: statusBarHeight + 'px' }">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="iconfont icon-fanhui"></text>
</view>
<view class="nav-title">图片生成</view>
<view class="nav-right" @click="goToHistory">
<text class="iconfont icon-lishi"></text>
</view>
</view>
<scroll-view scroll-y class="main-content" :style="{ height: scrollViewHeight }">
<!-- 参考图模块 -->
<view class="section reference-section">
<view class="reference-images">
<!-- 已上传的参考图 -->
<view
v-for="(img, index) in referenceImages"
:key="index"
class="reference-item"
>
<image :src="img" mode="aspectFill" class="ref-image"></image>
<view class="ref-delete" @click="removeReference(index)">
<text class="iconfont icon-guanbi"></text>
</view>
<view class="ref-tag">智能参考</view>
</view>
<!-- 添加按钮 -->
<view
v-if="referenceImages.length < 3"
class="reference-add"
@click="addReference"
>
<text class="add-icon">+</text>
<text class="add-text">上传参考图</text>
</view>
</view>
</view>
<!-- 提示词输入模块 -->
<view class="section prompt-section">
<view class="prompt-input-wrapper">
<textarea
v-model="promptText"
class="prompt-input"
placeholder="输入想要生成的图片描述做一张618大促海报"
:maxlength="800"
:auto-height="true"
@input="onPromptInput"
></textarea>
<!-- 语音输入按钮 -->
<view class="voice-btn" @click="startVoiceInput" v-if="!isRecording">
<text class="iconfont icon-laba"></text>
</view>
<view class="voice-btn recording" @click="stopVoiceInput" v-else>
<view class="recording-animation"></view>
<text class="recording-time">{{ recordDuration }}s</text>
</view>
</view>
<!-- 录音提示 -->
<view class="recording-hint" v-if="isRecording">
<text class="hint-icon">🎤</text>
<text class="hint-text">正在录音点击右下角完成</text>
</view>
<!-- 提示词建议 -->
<view class="prompt-suggestions" v-if="showSuggestions">
<view
v-for="(item, index) in suggestions"
:key="index"
class="suggestion-item"
@click="applySuggestion(item)"
>
<text class="suggestion-text">{{ item }}</text>
</view>
</view>
</view>
<!-- 生成类型切换 -->
<view class="section type-section">
<view class="type-tabs">
<view class="type-tab active">
<text class="tab-text">图片生成</text>
</view>
<view class="type-tab" @click="goToVideoGenerate">
<text class="tab-text">视频生成</text>
</view>
</view>
</view>
<!-- 配置选项 -->
<view class="section config-section">
<!-- 生成比例选择 -->
<view class="ratio-section">
<view class="ratio-header" @click="showRatioList = !showRatioList">
<view class="ratio-title-wrapper">
<text class="ratio-title">生成比例</text>
<text class="ratio-current" v-if="!showRatioList">{{ selectedRatio }}</text>
</view>
<text class="iconfont" :class="showRatioList ? 'icon-xiangshang' : 'icon-xiangxia'"></text>
</view>
<view class="ratio-content" v-if="showRatioList">
<view class="ratio-list">
<view
v-for="(ratio, index) in ratios"
:key="index"
:class="['ratio-item', selectedRatio === ratio.value ? 'active' : '']"
@click="selectRatio(ratio.value)"
>
<view class="ratio-box" :style="{ aspectRatio: ratio.aspect }"></view>
<text class="ratio-text">{{ ratio.value }}</text>
</view>
</view>
</view>
</view>
<!-- 联想词助手开关 -->
<view class="config-item assistant-item">
<view class="assistant-info">
<text class="assistant-title">联想词助手</text>
<text class="assistant-desc">AI智能补充和优化提示词</text>
</view>
<switch
:checked="enableAssistant"
@change="toggleAssistant"
color="#42ca4d"
/>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
<!-- AI生成内容标识 -->
<view class="ai-notice">
<text class="ai-notice-text">内容由AI生成结果仅供参考</text>
</view>
<!-- 底部操作栏 -->
<view class="footer-bar">
<button class="generate-btn" @click="handleGenerate">
<text class="btn-text">一键生成</text>
<!-- <text class="btn-count">{{ generateCount }}/</text> -->
</button>
<button class="gallery-btn" @click="goToGallery">
<text class="iconfont icon-tupian"></text>
<text class="btn-text">我的创作</text>
</button>
</view>
</view>
</template>
<script>
import api from '@/api/models-api.js'
export default {
data() {
return {
statusBarHeight: 0,
scrollViewHeight: '100vh',
// 参考图
referenceImages: [],
// 提示词
promptText: '',
showSuggestions: false,
suggestions: [
'618大促海报',
'会员礼包banner',
'新品上市宣传图',
'优惠券设计'
],
// 语音输入
isRecording: false,
recorderManager: null,
recordDuration: 0,
recordTimer: null,
// 模型选择
selectedModel: '4.0',
showModelPicker: false,
models: [
{
name: '4.0',
value: '4.0',
icon: '/static/images/model-4.0.png',
desc: '最新模型,图片质量最高,速度适中',
badge: 'New'
},
{
name: '3.1',
value: '3.1',
icon: '/static/images/model-3.1.png',
desc: '平衡模型,质量与速度兼顾'
},
{
name: '3.0',
value: '3.0',
icon: '/static/images/model-3.0.png',
desc: '稳定版本,生成速度快'
},
{
name: '2.1',
value: '2.1',
icon: '/static/images/model-2.1.png',
desc: '轻量模型,快速生成'
},
{
name: '2.0 PRO',
value: '2.0',
icon: '/static/images/model-2.0.png',
desc: '专业版,细节丰富'
}
],
// 生成比例
selectedRatio: '9:16',
ratios: [
{ value: '9:16', aspect: '9/16', label: '竖屏' },
{ value: '3:4', aspect: '3/4', label: '竖屏' },
{ value: '2:3', aspect: '2/3', label: '竖屏' },
{ value: '1:1', aspect: '1/1', label: '方形' },
{ value: '3:2', aspect: '3/2', label: '横屏' }
],
showRatioList: false, // 比例列表展开状态
// 图片质量
selectedQuality: '2K',
qualities: [
{ value: '2K', label: '2K' },
{ value: '4K', label: '4K' }
],
// 联想词助手
enableAssistant: true,
// 生成类型
generateType: 'image',
generateCount: 4,
sourceArticleId: ''
};
},
onLoad(options) {
this.initPage();
if (options && options.description) {
this.promptText = decodeURIComponent(options.description);
}
if (options && options.articleId) {
this.sourceArticleId = options.articleId;
}
// 接收参考图片参数
if (options && options.referenceImage) {
const refImage = decodeURIComponent(options.referenceImage);
this.referenceImages = [refImage];
}
},
onUnload() {
// 清理录音计时器
this.stopRecordTimer();
// 如果正在录音,停止录音
if (this.isRecording && this.recorderManager) {
this.recorderManager.stop();
}
},
methods: {
initPage() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// 计算滚动区域高度
const navHeight = 44;
const footerHeight = 120;
this.scrollViewHeight = `calc(100vh - ${this.statusBarHeight + navHeight + footerHeight}px)`;
// 初始化录音管理器
this.initRecorder();
},
// 初始化录音管理器
initRecorder() {
// #ifdef MP-WEIXIN || APP-PLUS
this.recorderManager = uni.getRecorderManager();
// 录音开始事件
this.recorderManager.onStart(() => {
console.log('录音开始');
this.recordDuration = 0;
this.startRecordTimer();
});
// 录音结束事件
this.recorderManager.onStop((res) => {
console.log('录音结束', res);
this.stopRecordTimer();
if (res.duration < 1000) {
uni.showToast({
title: '录音时间太短',
icon: 'none'
});
return;
}
// 处理录音结果
this.handleRecordResult(res);
});
// 录音错误事件
this.recorderManager.onError((err) => {
console.error('录音错误:', err);
this.isRecording = false;
this.stopRecordTimer();
uni.showToast({
title: '录音失败,请重试',
icon: 'none'
});
});
// #endif
// #ifdef H5
// H5 环境提示
console.log('H5环境暂不支持录音功能');
// #endif
},
// 开始录音计时
startRecordTimer() {
this.recordTimer = setInterval(() => {
this.recordDuration++;
if (this.recordDuration >= 60) {
// 录音超过60秒自动停止
this.stopVoiceInput();
}
}, 1000);
},
// 停止录音计时
stopRecordTimer() {
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
this.recordDuration = 0;
},
// 返回
goBack() {
uni.navigateBack();
},
// 查看历史
goToHistory() {
uni.navigateTo({
url: '/pages/ai-generate/history'
});
},
// 跳转到我的图库
goToGallery() {
uni.navigateTo({
url: '/pages/ai-generate/history'
});
},
// 跳转到视频生成
goToVideoGenerate() {
uni.navigateTo({
url: '/pages/ai-generate/oneclick'
});
},
// 添加参考图
async addReference() {
if (this.referenceImages.length >= 3) {
uni.showToast({
title: '最多添加3张参考图',
icon: 'none'
});
return;
}
try {
// 选择图片
const res = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 3 - this.referenceImages.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: reject
});
});
// 批量上传图片
const uploadPromises = res.tempFilePaths.map(async (filePath, index) => {
try {
uni.showLoading({
title: `上传中 ${index + 1}/${res.tempFilePaths.length}...`,
mask: true
});
const uploadResult = await api.uploadFile(filePath, {
model: 'user',
pid: '7'
});
if (uploadResult && uploadResult.code === 200) {
console.log('图片上传成功:', uploadResult.data);
return uploadResult.data.fullUrl;
} else {
throw new Error(uploadResult?.message || '上传失败');
}
} catch (error) {
console.error('图片上传失败:', error);
throw error;
}
});
// 等待所有图片上传完成
const uploadedUrls = await Promise.all(uploadPromises);
uni.hideLoading();
// 添加到参考图列表
this.referenceImages.push(...uploadedUrls);
uni.showToast({
title: `成功添加${uploadedUrls.length}张图片`,
icon: 'success',
duration: 2000
});
} catch (error) {
uni.hideLoading();
console.error('图片选择或上传失败:', error);
let errorMsg = '图片上传失败';
if (error.errMsg && error.errMsg.includes('cancel')) {
// 用户取消选择,不显示错误提示
return;
} else if (error.message) {
errorMsg = error.message;
}
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
},
// 移除参考图
removeReference(index) {
this.referenceImages.splice(index, 1);
},
// 提示词输入
onPromptInput(e) {
this.promptText = e.detail.value;
// 显示建议(简化逻辑)
this.showSuggestions = this.promptText.length > 0 && this.promptText.length < 10;
},
// 应用建议
applySuggestion(text) {
this.promptText = text;
this.showSuggestions = false;
},
// 开始语音输入
startVoiceInput() {
// #ifdef H5
uni.showModal({
title: '提示',
content: 'H5环境暂不支持语音输入请使用小程序或APP',
showCancel: false
});
return;
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
if (!this.recorderManager) {
uni.showToast({
title: '录音功能初始化失败',
icon: 'none'
});
return;
}
// 检查录音权限
uni.authorize({
scope: 'scope.record',
success: () => {
this.startRecording();
},
fail: () => {
uni.showModal({
title: '需要录音权限',
content: '请在设置中开启录音权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
});
}
});
// #endif
},
// 开始录音
startRecording() {
this.isRecording = true;
uni.showToast({
title: '开始录音',
icon: 'none',
duration: 1000
});
// 开始录音
this.recorderManager.start({
duration: 60000, // 最长录音60秒
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
});
},
// 停止语音输入
stopVoiceInput() {
if (!this.isRecording) {
return;
}
this.isRecording = false;
// #ifdef MP-WEIXIN || APP-PLUS
if (this.recorderManager) {
this.recorderManager.stop();
}
// #endif
uni.showToast({
title: '正在识别...',
icon: 'loading',
duration: 2000
});
},
// 处理录音结果
async handleRecordResult(res) {
console.log('录音文件路径:', res.tempFilePath);
console.log('录音时长:', res.duration, 'ms');
try {
// 这里可以调用语音识别API
// 示例:将录音文件上传到服务器进行识别
const recognizedText = await this.recognizeSpeech(res.tempFilePath);
if (recognizedText) {
// 将识别结果添加到提示词中
if (this.promptText) {
this.promptText += ' ' + recognizedText;
} else {
this.promptText = recognizedText;
}
uni.showToast({
title: '识别成功',
icon: 'success',
duration: 1500
});
}
} catch (error) {
console.error('语音识别失败:', error);
uni.showToast({
title: '识别失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 语音识别(对接腾讯云语音识别服务)
async recognizeSpeech(audioPath) {
try {
// 1. 上传录音文件到服务器
console.log('开始上传录音文件:', audioPath);
const uploadRes = await api.uploadFile(audioPath, {
model: 'audio',
pid: '8'
});
if (!uploadRes || uploadRes.code !== 200) {
throw new Error('录音文件上传失败');
}
const audioUrl = uploadRes.data.fullUrl;
console.log('录音文件上传成功URL:', audioUrl);
// 2. 创建语音识别任务
console.log('创建语音识别任务...');
const createTaskRes = await api.createAsrTask({
url: audioUrl,
engineModelType: '16k_zh',
channelNum: 1,
resTextFormat: 0,
sourceType: 0
});
if (!createTaskRes || createTaskRes.code !== 200) {
throw new Error('创建识别任务失败');
}
const taskId = createTaskRes.data.taskId;
console.log('识别任务创建成功任务ID:', taskId);
// 3. 轮询查询识别结果
const result = await this.pollAsrResult(taskId);
// 4. 解析识别文本
const text = this.parseAsrResult(result);
if (!text) {
throw new Error('识别结果为空');
}
console.log('语音识别成功,结果:', text);
return text;
} catch (error) {
console.error('语音识别失败:', error);
throw error;
}
},
// 轮询查询ASR识别结果
async pollAsrResult(taskId, maxAttempts = 30, interval = 2000) {
let attempts = 0;
while (attempts < maxAttempts) {
attempts++;
console.log(`查询识别状态,第${attempts}次尝试...`);
try {
const statusRes = await api.queryAsrStatus(taskId);
if (!statusRes || statusRes.code !== 200) {
throw new Error('查询识别状态失败');
}
const status = statusRes.data.status;
const statusStr = statusRes.data.statusStr;
console.log(`任务状态: ${statusStr} (${status})`);
// status: 0-等待处理, 1-处理中, 2-识别成功, 3-识别失败
if (status === 2) {
// 识别成功
console.log('识别成功!');
return statusRes.data;
} else if (status === 3) {
// 识别失败
throw new Error(statusRes.data.errorMsg || '识别失败');
}
// 等待一段时间后继续查询
await new Promise(resolve => setTimeout(resolve, interval));
} catch (error) {
console.error('查询识别状态出错:', error);
// 如果不是最后一次尝试,继续重试
if (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, interval));
continue;
}
throw error;
}
}
throw new Error('识别超时,请重试');
},
// 解析ASR识别结果文本
parseAsrResult(data) {
if (!data || !data.result) {
return '';
}
let text = data.result;
// 去除时间戳标记,例如:[0:0.000,0:11.580]
text = text.replace(/\[\d+:\d+\.\d+,\d+:\d+\.\d+\]\s*/g, '');
// 去除多余的空格和换行
text = text.replace(/\n+/g, ' ').trim();
// 去除多余的空格
text = text.replace(/\s+/g, ' ');
return text;
},
// 选择模型
selectModel(value) {
this.selectedModel = value;
},
// 从选择器选择模型
selectModelFromPicker(value) {
this.selectedModel = value;
this.showModelPicker = false;
},
// 选择比例
selectRatio(value) {
this.selectedRatio = value;
},
// 选择质量
selectQuality(value) {
this.selectedQuality = value;
},
// 切换联想词助手
toggleAssistant(e) {
this.enableAssistant = e.detail.value;
},
// 生成图片
async handleGenerate() {
// 验证
if (!this.promptText.trim()) {
uni.showToast({
title: '请输入生成描述',
icon: 'none'
});
return;
}
// 验证是否有参考图
if (this.referenceImages.length === 0) {
uni.showToast({
title: '请至少添加一张参考图',
icon: 'none'
});
return;
}
try {
uni.showLoading({
title: '生成中...'
});
// 调用图片编辑API
const response = await api.createImageEditTask({
prompt: this.promptText,
image_urls: this.referenceImages,
output_format: 'png',
image_size: this.selectedRatio,
title: this.promptText
});
console.log('图片编辑任务创建成功:', response);
uni.hideLoading();
// 跳转到结果页,携带生成参数
const params = {
promptText: encodeURIComponent(this.promptText),
ratio: this.selectedRatio,
model: this.selectedModel,
referenceImages: encodeURIComponent(JSON.stringify(this.referenceImages)),
generate: 'true'
};
const queryString = Object.keys(params)
.map(key => `${key}=${params[key]}`)
.join('&');
uni.navigateTo({
url: `/pages/ai-generate/result?${queryString}`,
success: () => {
uni.showToast({
title: '任务已提交',
icon: 'success'
});
}
});
} catch (error) {
console.error('图片编辑任务创建失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '生成失败,请重试',
icon: 'none',
duration: 2000
});
}
}
}
};
</script>
<style lang="scss" scoped>
.ai-generate-container {
width: 100%;
height: 100vh;
background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%);
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
// 顶部导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
.nav-left,
.nav-right {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 20px;
color: #ffffff;
}
}
.nav-title {
font-size: 17px;
font-weight: 500;
color: #ffffff;
}
}
// AI生成内容标识
.ai-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
margin: -8px 16px 1px 16px;
background: rgba(66, 202, 77, 0.1);
border: 1px solid rgba(66, 202, 77, 0.3);
border-radius: 8px;
backdrop-filter: blur(10px);
.ai-notice-icon {
font-size: 16px;
}
.ai-notice-text {
font-size: 12px;
color: #42ca4d;
line-height: 1.4;
}
}
// 主内容区
.main-content {
padding: 0 16px; width:92%
}
// 区块样式
.section {
margin-bottom: 20px; margin-top: 10rpx;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.section-title {
font-size: 16px;
font-weight: 500;
color: #ffffff;
}
.section-tip {
font-size: 12px;
color: #8f9bb3; margin-right: 20rpx;
}
}
}
// 参考图模块
.reference-images {
display: flex;
flex-wrap: wrap;
gap: 12px;
.reference-item {
width: 100px;
height: 100px;
border-radius: 12px;
overflow: hidden;
position: relative;
.ref-image {
width: 100%;
height: 100%;
}
.ref-delete {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 12px;
color: #ffffff;
}
}
.ref-tag {
position: absolute;
bottom: 4px;
left: 4px;
padding: 2px 6px;
background: rgba(0, 214, 178, 0.9);
border-radius: 4px;
font-size: 10px;
color: #ffffff;
}
}
.reference-add {
width: 100px;
height: 100px;
border: 2px dashed #3d4458;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(61, 68, 88, 0.2);
.add-icon {
font-size: 32px;
color: #8f9bb3;
margin-bottom: 4px;
}
.add-text {
font-size: 12px;
color: #8f9bb3;
}
}
}
// 提示词输入
.prompt-section {
.prompt-input-wrapper {
position: relative;
background: rgba(61, 68, 88, 0.3);
border-radius: 16px;
padding: 16px;
min-height: 120px;
.prompt-input {
width: 100%;
min-height: 88px;
font-size: 15px;
color: #ffffff;
line-height: 1.6;
}
.voice-btn {
position: absolute;
bottom: 16px;
right: 16px;
width: 44px;
height: 44px;
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.iconfont {
font-size: 24px;
color: #ffffff;
}
&.recording {
background: #ff6b6b;
width: 56px;
height: 56px;
.recording-animation {
width: 20px;
height: 20px;
background: #ffffff;
border-radius: 50%;
animation: pulse 1s infinite;
margin-bottom: 2px;
}
.recording-time {
font-size: 10px;
color: #ffffff;
font-weight: 500;
}
}
}
}
// 录音提示
.recording-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 12px;
padding: 8px 16px;
background: rgba(255, 107, 107, 0.1);
border-radius: 20px;
border: 1px solid rgba(255, 107, 107, 0.3);
animation: blink 1.5s infinite;
.hint-icon {
font-size: 16px;
}
.hint-text {
font-size: 13px;
color: #ff6b6b;
}
}
.prompt-suggestions {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
.suggestion-item {
padding: 6px 12px;
background: rgba(61, 68, 88, 0.4);
border-radius: 16px;
.suggestion-text {
font-size: 13px;
color: #8f9bb3;
}
}
}
}
// 生成类型切换
.type-section {
background: rgba(61, 68, 88, 0.3);
border-radius: 16px;
padding: 0px;
margin-bottom: 16px;
.type-tabs {
display: flex;
gap: 8px;
.type-tab {
flex: 1;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border-radius: 12px;
transition: all 0.3s ease;
cursor: pointer;
.tab-text {
font-size: 15px;
color: #8f9bb3;
font-weight: 500;
transition: all 0.3s ease;
}
&.active {
background-color: #404641;
.tab-text {
color: #ffffff;
}
}
&:not(.active):active {
background: rgba(61, 68, 88, 0.5);
transform: scale(0.98);
}
}
}
}
// 配置选项
.config-section {
// 比例选择区域
.ratio-section {
background: rgba(61, 68, 88, 0.3);
border-radius: 14px;
padding: 14px;
margin-bottom: 16px;
.ratio-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.ratio-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
.ratio-title {
font-size: 15px;
font-weight: 500;
color: #ffffff;
}
.ratio-current {
font-size: 14px;
color: #42ca4d;
font-weight: 500;
}
}
.iconfont {
font-size: 14px;
color: #8f9bb3;
}
}
.ratio-content {
margin-top: 12px;
}
}
.config-item {
margin-bottom: 20px;
.config-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.config-label {
display: flex;
align-items: center;
gap: 8px;
.iconfont {
font-size: 18px;
color: #42ca4d;
}
.label-text {
font-size: 15px;
color: #ffffff;
}
.label-version {
font-size: 15px;
color: #8f9bb3;
}
}
.config-action {
display: flex;
align-items: center;
gap: 4px;
.action-text {
font-size: 13px;
color: #42ca4d;
}
.iconfont {
font-size: 12px;
color: #42ca4d;
}
}
}
}
}
// 比例列表
.ratio-list {
display: flex;
gap: 12px;
.ratio-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
background: rgba(61, 68, 88, 0.3);
border-radius: 10px;
border: 2px solid transparent;
.ratio-box {
width: 20px;
background: #8f9bb3;
border-radius: 4px;
margin-bottom: 6px;
}
.ratio-text {
font-size: 12px;
color: #8f9bb3;
}
&.active {
background: rgba(6, 67, 14, 0.1);
border-color: #42ca4d;
.ratio-box {
background: #42ca4d;
}
.ratio-text {
color: #42ca4d;
}
}
}
}
// 联想词助手
.assistant-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px;
background: rgba(61, 68, 88, 0.3);
border-radius: 12px;
.assistant-info {
flex: 1;
.assistant-title {
display: block;
font-size: 15px;
color: #ffffff;
margin-bottom: 4px;
}
.assistant-desc {
display: block;
font-size: 12px;
color: #8f9bb3;
}
}
}
// 底部占位
.bottom-placeholder {
height: 20px;
}
// 底部操作栏
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px 24px;
background: linear-gradient(0deg, rgba(10, 14, 26, 0.95) 0%, rgba(10, 14, 26, 0.8) 100%);
backdrop-filter: blur(20px);
display: flex;
gap: 12px;
.generate-btn {
flex: 2;
height: 50px;
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
.btn-text {
font-size: 16px;
font-weight: 500;
color: #ffffff;
}
.btn-count {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
}
.gallery-btn {
flex: 1;
height: 50px;
background: rgba(61, 68, 88, 0.4);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border: none;
.iconfont {
font-size: 18px;
color: #ffffff;
}
.btn-text {
font-size: 14px;
color: #ffffff;
}
}
}
// 动画
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.8);
opacity: 0.6;
}
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>