测试:将ip地址都改为192.168.1.164
This commit is contained in:
parent
e2765ecf00
commit
92386f4597
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
归档/
|
归档/
|
||||||
|
xunifriend_RaeeC/runtime/log/202602/01.log
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -4,7 +4,7 @@ import re
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import oss2
|
import oss2
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
@ -474,7 +474,7 @@ def _pick_available_voice(db: Session, lover: Lover, user_row: User) -> VoiceLib
|
||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int) -> str:
|
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int, request: Request = None) -> str:
|
||||||
"""
|
"""
|
||||||
上传 TTS 音频文件。
|
上传 TTS 音频文件。
|
||||||
优先使用 OSS,如果未配置则保存到本地。
|
优先使用 OSS,如果未配置则保存到本地。
|
||||||
|
|
@ -520,8 +520,16 @@ def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int) -> str
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(file_bytes)
|
f.write(file_bytes)
|
||||||
|
|
||||||
# 返回完整 URL(使用环境变量配置的后端地址)
|
# 自动检测请求来源,生成正确的 URL
|
||||||
|
if request:
|
||||||
|
# 从请求头获取 Host
|
||||||
|
host = request.headers.get("host", "127.0.0.1:8000")
|
||||||
|
scheme = "https" if request.url.scheme == "https" else "http"
|
||||||
|
backend_url = f"{scheme}://{host}"
|
||||||
|
else:
|
||||||
|
# 降级使用环境变量
|
||||||
backend_url = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
backend_url = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
||||||
|
|
||||||
return f"{backend_url.rstrip('/')}/tts/{lover_id}/{message_id}.mp3"
|
return f"{backend_url.rstrip('/')}/tts/{lover_id}/{message_id}.mp3"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=f"保存语音文件失败: {exc}") from exc
|
raise HTTPException(status_code=500, detail=f"保存语音文件失败: {exc}") from exc
|
||||||
|
|
@ -653,6 +661,7 @@ def list_messages(
|
||||||
@router.post("/messages/tts/{message_id}", response_model=ApiResponse[TTSOut])
|
@router.post("/messages/tts/{message_id}", response_model=ApiResponse[TTSOut])
|
||||||
def generate_message_tts(
|
def generate_message_tts(
|
||||||
message_id: int,
|
message_id: int,
|
||||||
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: AuthedUser = Depends(get_current_user),
|
user: AuthedUser = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
@ -706,7 +715,7 @@ def generate_message_tts(
|
||||||
model=model,
|
model=model,
|
||||||
voice=voice.voice_code,
|
voice=voice.voice_code,
|
||||||
)
|
)
|
||||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id)
|
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id, request)
|
||||||
except HTTPException as exc:
|
except HTTPException as exc:
|
||||||
detail_text = str(exc.detail) if hasattr(exc, "detail") else str(exc)
|
detail_text = str(exc.detail) if hasattr(exc, "detail") else str(exc)
|
||||||
fallback_done = False
|
fallback_done = False
|
||||||
|
|
@ -721,7 +730,7 @@ def generate_message_tts(
|
||||||
model=fallback_model,
|
model=fallback_model,
|
||||||
voice=fallback_voice,
|
voice=fallback_voice,
|
||||||
)
|
)
|
||||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id)
|
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id, request)
|
||||||
msg.tts_voice_id = None # 兜底音色不绑定库ID
|
msg.tts_voice_id = None # 兜底音色不绑定库ID
|
||||||
msg.tts_model_id = fallback_model
|
msg.tts_model_id = fallback_model
|
||||||
fallback_done = True
|
fallback_done = True
|
||||||
|
|
|
||||||
BIN
public/tts/47/776.mp3
Normal file
BIN
public/tts/47/776.mp3
Normal file
Binary file not shown.
|
|
@ -76,6 +76,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { baseURL } from '@/utils/request.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -262,7 +264,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
baseURL() {
|
baseURL() {
|
||||||
return 'http://127.0.0.1:8080';
|
return baseURL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,7 @@
|
||||||
SingGenerate,
|
SingGenerate,
|
||||||
SingGenerateTask
|
SingGenerateTask
|
||||||
} from '@/utils/api.js'
|
} from '@/utils/api.js'
|
||||||
|
import { baseURL, baseURLPy } from '@/utils/request.js'
|
||||||
import notHave from '@/components/not-have.vue';
|
import notHave from '@/components/not-have.vue';
|
||||||
import topSafety from '@/components/top-safety.vue';
|
import topSafety from '@/components/top-safety.vue';
|
||||||
import ai from '@/components/ai.vue';
|
import ai from '@/components/ai.vue';
|
||||||
|
|
@ -353,8 +354,8 @@
|
||||||
// 消息编辑相关
|
// 消息编辑相关
|
||||||
editModalVisible: false,
|
editModalVisible: false,
|
||||||
editingMessage: null,
|
editingMessage: null,
|
||||||
baseURL: 'http://127.0.0.1:8080',
|
baseURL: baseURL,
|
||||||
baseURLPy: 'http://127.0.0.1:8000',
|
baseURLPy: baseURLPy,
|
||||||
// 思考时间相关
|
// 思考时间相关
|
||||||
messageSentTime: 0, // 消息发送时间戳
|
messageSentTime: 0, // 消息发送时间戳
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { LoverBasic } from '@/utils/api.js';
|
import { LoverBasic } from '@/utils/api.js';
|
||||||
|
import { baseURLPy } from '@/utils/request.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -136,7 +137,11 @@ export default {
|
||||||
|
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
const token = uni.getStorageSync('token');
|
const token = uni.getStorageSync('token');
|
||||||
const wsUrl = `ws://127.0.0.1:8000/voice/call?token=${token}&ptt=true`;
|
// 将 http:// 替换为 ws://,https:// 替换为 wss://
|
||||||
|
const wsBaseUrl = baseURLPy.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||||
|
const wsUrl = `${wsBaseUrl}/voice/call?token=${token}&ptt=true`;
|
||||||
|
|
||||||
|
console.log('WebSocket URL:', wsUrl);
|
||||||
|
|
||||||
this.websocket = uni.connectSocket({
|
this.websocket = uni.connectSocket({
|
||||||
url: wsUrl,
|
url: wsUrl,
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ import {
|
||||||
ConfigVoicesAvailable,
|
ConfigVoicesAvailable,
|
||||||
LoverVoiceSimple
|
LoverVoiceSimple
|
||||||
} from '@/utils/api.js'
|
} from '@/utils/api.js'
|
||||||
|
import { baseURLPy } from '@/utils/request.js'
|
||||||
import notHave from '@/components/not-have.vue';
|
import notHave from '@/components/not-have.vue';
|
||||||
import topSafety from '@/components/top-safety.vue';
|
import topSafety from '@/components/top-safety.vue';
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ export default {
|
||||||
cloning: false,
|
cloning: false,
|
||||||
cloneStatus: '',
|
cloneStatus: '',
|
||||||
cloneVoiceId: '',
|
cloneVoiceId: '',
|
||||||
baseURLPy: 'http://127.0.0.1:8000',
|
baseURLPy: baseURLPy,
|
||||||
// 音频输入方式:'file' 或 'url'
|
// 音频输入方式:'file' 或 'url'
|
||||||
audioInputMode: 'url', // 默认使用 URL 输入
|
audioInputMode: 'url', // 默认使用 URL 输入
|
||||||
audioUrlInput: '', // 用户输入的音频 URL
|
audioUrlInput: '', // 用户输入的音频 URL
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
:duration="300"
|
:duration="300"
|
||||||
:indicator-dots="false">
|
:indicator-dots="false">
|
||||||
|
|
||||||
<!-- 首页内容 -->
|
<!-- 首页内容 (index 0) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<scroll-view class="swiper-scroll" scroll-y="true">
|
<scroll-view class="swiper-scroll" scroll-y="true">
|
||||||
<view class="backA"></view>
|
<view class="backA"></view>
|
||||||
|
|
@ -140,19 +140,33 @@
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 聊天页面 -->
|
<!-- 聊天页面 (index 1) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content" @click="tochat">
|
<scroll-view class="swiper-scroll" scroll-y="true">
|
||||||
<view class="feature-page">
|
<view class="chat-preview-container">
|
||||||
<view class="feature-icon">💬</view>
|
<view class="chat-preview-header">
|
||||||
<view class="feature-title">聊天</view>
|
<view class="chat-preview-title">💬 与她聊天 (Tab索引: {{currentTab}})</view>
|
||||||
<view class="feature-desc">与她开始对话</view>
|
<view class="chat-preview-btn" @click="tochat">进入完整聊天</view>
|
||||||
<view class="feature-btn">进入聊天</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 聊天记录预览 -->
|
||||||
|
<view class="chat-preview-list">
|
||||||
|
<view class="chat-preview-tip">点击上方按钮进入完整聊天页面</view>
|
||||||
|
<view class="chat-preview-image">
|
||||||
|
<image
|
||||||
|
v-if="loverBasicList"
|
||||||
|
:src="loverBasicList.image_url || '/static/images/avatar.png'"
|
||||||
|
mode="aspectFill">
|
||||||
|
</image>
|
||||||
|
</view>
|
||||||
|
<view class="chat-preview-name">{{ loverBasicList.name || '你的恋人' }}</view>
|
||||||
|
<view class="chat-preview-desc">开始你们的对话吧~</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 唱歌页面 -->
|
<!-- 唱歌页面 (index 2) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content feature-page">
|
<view class="swiper-content feature-page">
|
||||||
<view class="feature-icon">🎤</view>
|
<view class="feature-icon">🎤</view>
|
||||||
|
|
@ -171,7 +185,7 @@
|
||||||
</view>
|
</view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 跳舞页面 -->
|
<!-- 跳舞页面 (index 3) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content feature-page">
|
<view class="swiper-content feature-page">
|
||||||
<view class="feature-icon">💃</view>
|
<view class="feature-icon">💃</view>
|
||||||
|
|
@ -188,19 +202,84 @@
|
||||||
</view>
|
</view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 换服装页面 -->
|
<!-- 换服装页面 (index 4) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content" @click="toreplacement">
|
<scroll-view class="swiper-scroll" scroll-y="true">
|
||||||
<view class="feature-page">
|
<view class="outfit-container">
|
||||||
<view class="feature-icon">👗</view>
|
<view class="outfit-header">
|
||||||
<view class="feature-title">换服装</view>
|
<text class="outfit-title">👗 换装搭配</text>
|
||||||
<view class="feature-desc">为她挑选不同的服装</view>
|
<text class="outfit-count">剩余次数: {{ outfitData ? outfitData.clothes_num : 0 }}</text>
|
||||||
<view class="feature-btn">进入换装</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!outfitData" class="outfit-loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="outfit-content">
|
||||||
|
<!-- 上衣 -->
|
||||||
|
<view class="outfit-section">
|
||||||
|
<view class="section-title-top">上衣</view>
|
||||||
|
<view class="outfit-grid-wrapper">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in outfitData.top"
|
||||||
|
:key="item.id"
|
||||||
|
@click="selectOutfitTop(item)"
|
||||||
|
class="outfit-item-wrapper">
|
||||||
|
<view class="outfit-grid-inner" :style="getOutfitBorderStyle(item.id, 'top')">
|
||||||
|
<image class="outfit-grid-img" :src="item.image_url" mode="aspectFill"></image>
|
||||||
|
<view v-if="item.is_lock && !item.is_free" class="outfit-grid-lock">🔒</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view style="clear: both;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 下装 -->
|
||||||
|
<view class="outfit-section">
|
||||||
|
<view class="section-title-top">下装</view>
|
||||||
|
<view class="outfit-grid-wrapper">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in outfitData.bottom"
|
||||||
|
:key="item.id"
|
||||||
|
@click="selectOutfitBottom(item)"
|
||||||
|
class="outfit-item-wrapper">
|
||||||
|
<view class="outfit-grid-inner" :style="getOutfitBorderStyle(item.id, 'bottom')">
|
||||||
|
<image class="outfit-grid-img" :src="item.image_url" mode="aspectFill"></image>
|
||||||
|
<view v-if="item.is_lock && !item.is_free" class="outfit-grid-lock">🔒</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view style="clear: both;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 连体服 -->
|
||||||
|
<view class="outfit-section">
|
||||||
|
<view class="section-title-top">连体服</view>
|
||||||
|
<view class="outfit-grid-wrapper">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in outfitData.dress"
|
||||||
|
:key="item.id"
|
||||||
|
@click="selectOutfitDress(item)"
|
||||||
|
class="outfit-item-wrapper">
|
||||||
|
<view class="outfit-grid-inner" :style="getOutfitBorderStyle(item.id, 'dress')">
|
||||||
|
<image class="outfit-grid-img" :src="item.image_url" mode="aspectFill"></image>
|
||||||
|
<view v-if="item.is_lock && !item.is_free" class="outfit-grid-lock">🔒</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view style="clear: both;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<view class="outfit-generate-btn" @click="generateOutfit">
|
||||||
|
<text>生成换装</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 刷礼物页面 -->
|
<!-- 刷礼物页面 (index 5) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content" @click="togift">
|
<view class="swiper-content" @click="togift">
|
||||||
<view class="feature-page">
|
<view class="feature-page">
|
||||||
|
|
@ -212,7 +291,7 @@
|
||||||
</view>
|
</view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 商城页面 -->
|
<!-- 商城页面 (index 6) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content feature-page">
|
<view class="swiper-content feature-page">
|
||||||
<view class="feature-icon">🛒</view>
|
<view class="feature-icon">🛒</view>
|
||||||
|
|
@ -235,7 +314,7 @@
|
||||||
</view>
|
</view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
|
|
||||||
<!-- 短剧页面 -->
|
<!-- 短剧页面 (index 7) -->
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="swiper-content feature-page">
|
<view class="swiper-content feature-page">
|
||||||
<view class="feature-icon">🎬</view>
|
<view class="feature-icon">🎬</view>
|
||||||
|
|
@ -288,7 +367,9 @@
|
||||||
SingSongs,
|
SingSongs,
|
||||||
SingGenerate,
|
SingGenerate,
|
||||||
SingGenerateTask,
|
SingGenerateTask,
|
||||||
DanceGenerate
|
DanceGenerate,
|
||||||
|
OutfitList,
|
||||||
|
OutfitChange
|
||||||
} from '@/utils/api.js'
|
} from '@/utils/api.js'
|
||||||
import notHave from '@/components/not-have.vue';
|
import notHave from '@/components/not-have.vue';
|
||||||
import topSafety from '@/components/top-safety.vue';
|
import topSafety from '@/components/top-safety.vue';
|
||||||
|
|
@ -322,6 +403,12 @@
|
||||||
dancePrompt: '',
|
dancePrompt: '',
|
||||||
singSongsList: [],
|
singSongsList: [],
|
||||||
songId: 0,
|
songId: 0,
|
||||||
|
// 换服装相关数据
|
||||||
|
outfitData: null,
|
||||||
|
selectedTopId: null,
|
||||||
|
selectedBottomId: null,
|
||||||
|
selectedDressId: null,
|
||||||
|
outfitMode: null, // 1=连体服, 2=上衣+下装
|
||||||
statusBarHeight: uni.getWindowInfo().statusBarHeight,
|
statusBarHeight: uni.getWindowInfo().statusBarHeight,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
chartData: {},
|
chartData: {},
|
||||||
|
|
@ -382,9 +469,27 @@
|
||||||
methods: {
|
methods: {
|
||||||
// Tab 切换方法
|
// Tab 切换方法
|
||||||
switchTab(index) {
|
switchTab(index) {
|
||||||
|
console.log('点击 Tab,切换到索引:', index, '对应 Tab:', this.tabs[index].name);
|
||||||
|
|
||||||
|
// 如果点击的是"聊天" tab(索引为1),直接跳转到聊天页面
|
||||||
|
if (index === 1) {
|
||||||
|
this.tochat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果点击的是"换服装" tab(索引为4),加载服装数据
|
||||||
|
if (index === 4) {
|
||||||
|
console.log('切换到换服装 tab,准备加载数据');
|
||||||
|
// 延迟一下再加载,确保页面已经渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadOutfitData();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
this.currentTab = index;
|
this.currentTab = index;
|
||||||
},
|
},
|
||||||
onSwiperChange(e) {
|
onSwiperChange(e) {
|
||||||
|
console.log('Swiper 滑动,当前索引:', e.detail.current, '对应 Tab:', this.tabs[e.detail.current].name);
|
||||||
this.currentTab = e.detail.current;
|
this.currentTab = e.detail.current;
|
||||||
},
|
},
|
||||||
// 选择歌曲
|
// 选择歌曲
|
||||||
|
|
@ -433,6 +538,202 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 加载换装数据
|
||||||
|
loadOutfitData() {
|
||||||
|
console.log('开始加载换装数据,当前 outfitData:', this.outfitData);
|
||||||
|
|
||||||
|
if (this.outfitData) {
|
||||||
|
console.log('数据已存在,跳过加载');
|
||||||
|
return; // 已加载过就不重复加载
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('显示加载提示...');
|
||||||
|
uni.showLoading({ title: '加载中...' });
|
||||||
|
|
||||||
|
console.log('调用 OutfitList API...');
|
||||||
|
OutfitList({}).then(res => {
|
||||||
|
console.log('OutfitList API 返回:', res);
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (res.code == 1) {
|
||||||
|
this.outfitData = res.data;
|
||||||
|
console.log('设置 outfitData 成功:', this.outfitData);
|
||||||
|
|
||||||
|
// 处理数据,标记当前使用的服装
|
||||||
|
this.processOutfitData();
|
||||||
|
|
||||||
|
// 自动选中当前使用的服装
|
||||||
|
this.selectCurrentOutfit();
|
||||||
|
} else {
|
||||||
|
console.error('API 返回错误:', res.msg);
|
||||||
|
uni.showToast({ title: res.msg || '加载失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('OutfitList API 异常:', err);
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '网络错误,请检查后端服务', icon: 'none' });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 处理换装数据
|
||||||
|
processOutfitData() {
|
||||||
|
if (!this.outfitData) return;
|
||||||
|
|
||||||
|
const { current_outfit, owned_outfit_ids } = this.outfitData;
|
||||||
|
|
||||||
|
// 处理上衣
|
||||||
|
this.outfitData.top.forEach(item => {
|
||||||
|
item.is_current = current_outfit.top_id === item.id;
|
||||||
|
item.is_lock = !owned_outfit_ids.includes(item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理下装
|
||||||
|
this.outfitData.bottom.forEach(item => {
|
||||||
|
item.is_current = current_outfit.bottom_id === item.id;
|
||||||
|
item.is_lock = !owned_outfit_ids.includes(item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理连体服
|
||||||
|
this.outfitData.dress.forEach(item => {
|
||||||
|
item.is_current = current_outfit.dress_id === item.id;
|
||||||
|
item.is_lock = !owned_outfit_ids.includes(item.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 自动选中当前使用的服装
|
||||||
|
selectCurrentOutfit() {
|
||||||
|
if (!this.outfitData || !this.outfitData.current_outfit) return;
|
||||||
|
|
||||||
|
const { current_outfit } = this.outfitData;
|
||||||
|
|
||||||
|
// 检查连体服
|
||||||
|
if (current_outfit.dress_id) {
|
||||||
|
this.outfitMode = 1;
|
||||||
|
this.selectedDressId = current_outfit.dress_id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查上衣下装
|
||||||
|
if (current_outfit.top_id || current_outfit.bottom_id) {
|
||||||
|
this.outfitMode = 2;
|
||||||
|
this.selectedTopId = current_outfit.top_id;
|
||||||
|
this.selectedBottomId = current_outfit.bottom_id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取服装边框样式
|
||||||
|
getOutfitBorderStyle(itemId, type) {
|
||||||
|
let isSelected = false;
|
||||||
|
|
||||||
|
if (type === 'top') {
|
||||||
|
isSelected = this.selectedTopId === itemId;
|
||||||
|
} else if (type === 'bottom') {
|
||||||
|
isSelected = this.selectedBottomId === itemId;
|
||||||
|
} else if (type === 'dress') {
|
||||||
|
isSelected = this.selectedDressId === itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
border: isSelected ? '5rpx solid #ff0000' : '5rpx solid #ddd',
|
||||||
|
boxShadow: isSelected ? '0 4rpx 20rpx rgba(255, 0, 0, 0.4)' : '0 4rpx 16rpx rgba(0, 0, 0, 0.15)'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// 选择上衣
|
||||||
|
selectOutfitTop(item) {
|
||||||
|
console.log('点击上衣,item.id:', item.id, '当前selectedTopId:', this.selectedTopId);
|
||||||
|
|
||||||
|
if (item.is_lock && !item.is_free) {
|
||||||
|
uni.showToast({ title: '该服装未解锁', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.outfitMode === 1 && this.selectedDressId) {
|
||||||
|
uni.showToast({ title: '连体服模式下不能选择上衣', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outfitMode = 2;
|
||||||
|
this.selectedTopId = this.selectedTopId === item.id ? null : item.id;
|
||||||
|
|
||||||
|
console.log('选择后selectedTopId:', this.selectedTopId);
|
||||||
|
},
|
||||||
|
// 选择下装
|
||||||
|
selectOutfitBottom(item) {
|
||||||
|
console.log('点击下装,item.id:', item.id, '当前selectedBottomId:', this.selectedBottomId);
|
||||||
|
|
||||||
|
if (item.is_lock && !item.is_free) {
|
||||||
|
uni.showToast({ title: '该服装未解锁', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.outfitMode === 1 && this.selectedDressId) {
|
||||||
|
uni.showToast({ title: '连体服模式下不能选择下装', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outfitMode = 2;
|
||||||
|
this.selectedBottomId = this.selectedBottomId === item.id ? null : item.id;
|
||||||
|
|
||||||
|
console.log('选择后selectedBottomId:', this.selectedBottomId);
|
||||||
|
},
|
||||||
|
// 选择连体服
|
||||||
|
selectOutfitDress(item) {
|
||||||
|
console.log('点击连体服,item.id:', item.id, '当前selectedDressId:', this.selectedDressId);
|
||||||
|
|
||||||
|
if (item.is_lock && !item.is_free) {
|
||||||
|
uni.showToast({ title: '该服装未解锁', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.outfitMode === 2 && (this.selectedTopId || this.selectedBottomId)) {
|
||||||
|
uni.showToast({ title: '上衣下装模式下不能选择连体服', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outfitMode = 1;
|
||||||
|
this.selectedDressId = this.selectedDressId === item.id ? null : item.id;
|
||||||
|
if (this.selectedDressId) {
|
||||||
|
this.selectedTopId = null;
|
||||||
|
this.selectedBottomId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('选择后selectedDressId:', this.selectedDressId);
|
||||||
|
},
|
||||||
|
// 生成换装
|
||||||
|
generateOutfit() {
|
||||||
|
if (!this.outfitData || !this.outfitData.clothes_num) {
|
||||||
|
uni.showToast({ title: '换装次数不足', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {};
|
||||||
|
|
||||||
|
if (this.outfitMode === 1 && this.selectedDressId) {
|
||||||
|
params = { dress_item_id: this.selectedDressId, save_to_look: false };
|
||||||
|
} else if (this.outfitMode === 2 && (this.selectedTopId || this.selectedBottomId)) {
|
||||||
|
params = {
|
||||||
|
top_item_id: this.selectedTopId || '',
|
||||||
|
bottom_item_id: this.selectedBottomId || '',
|
||||||
|
save_to_look: false
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '请先选择服装', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '生成中...' });
|
||||||
|
|
||||||
|
OutfitChange(params).then(res => {
|
||||||
|
uni.hideLoading();
|
||||||
|
if (res.code == 1) {
|
||||||
|
uni.showToast({ title: '换装成功', icon: 'success' });
|
||||||
|
// 重新加载数据
|
||||||
|
this.outfitData = null;
|
||||||
|
this.loadOutfitData();
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.msg, icon: 'none' });
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
uni.hideLoading();
|
||||||
|
});
|
||||||
|
},
|
||||||
// 轮询监听唱歌任务结果
|
// 轮询监听唱歌任务结果
|
||||||
getSingGenerateTask(task_id) {
|
getSingGenerateTask(task_id) {
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
@ -1396,3 +1697,255 @@
|
||||||
height: 4rpx;
|
height: 4rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
/* 聊天预览容器样式 */
|
||||||
|
.chat-preview-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-btn {
|
||||||
|
padding: 15rpx 30rpx;
|
||||||
|
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-tip {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-image {
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(159, 71, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-image image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-name {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview-desc {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 换装页面样式 - 和谐配色 */
|
||||||
|
.outfit-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 30rpx;
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-count {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-content {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-section {
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx 20rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 25rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
border-left: 4rpx solid #8a7cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-top {
|
||||||
|
font-size: 70rpx !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #000 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
margin: 0 0 40rpx 0 !important;
|
||||||
|
padding: 20rpx 0 !important;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
border-radius: 12rpx !important;
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 网格容器 - 使用固定宽度 */
|
||||||
|
.outfit-grid-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-item-wrapper {
|
||||||
|
float: left;
|
||||||
|
width: 220rpx;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-grid-inner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 280rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 5rpx solid #ddd;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-grid-item-selected .outfit-grid-inner {
|
||||||
|
border: 5rpx solid #ff0000 !important;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(255, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-grid-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-check-mark {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 8rpx !important;
|
||||||
|
left: 8rpx !important;
|
||||||
|
width: 40rpx !important;
|
||||||
|
height: 40rpx !important;
|
||||||
|
background: #8a7cff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10 !important;
|
||||||
|
pointer-events: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-grid-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background: rgba(255, 0, 0, 0.9);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-grid-lock {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 60rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-generate-btn {
|
||||||
|
margin: 20rpx 0;
|
||||||
|
padding: 28rpx 0;
|
||||||
|
background: linear-gradient(135deg, #8a7cff 0%, #6b5ce7 100%);
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(138, 124, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outfit-generate-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(138, 124, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@
|
||||||
WxappGetPhone,
|
WxappGetPhone,
|
||||||
AppLoginWx
|
AppLoginWx
|
||||||
} from '@/utils/api.js'
|
} from '@/utils/api.js'
|
||||||
|
import { baseURLPy } from '@/utils/request.js'
|
||||||
import {
|
import {
|
||||||
isPhone,
|
isPhone,
|
||||||
getWxCode
|
getWxCode
|
||||||
|
|
@ -146,7 +147,7 @@
|
||||||
dataLogin: [],
|
dataLogin: [],
|
||||||
selectStats: false,
|
selectStats: false,
|
||||||
inviteCode: '', // 邀请码
|
inviteCode: '', // 邀请码
|
||||||
baseURLPy: 'http://127.0.0.1:8000',
|
baseURLPy: baseURLPy,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { baseURLPy } from '@/utils/request.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -50,7 +52,7 @@
|
||||||
inviteCount: 0,
|
inviteCount: 0,
|
||||||
inviteReward: 0,
|
inviteReward: 0,
|
||||||
qrCodeUrl: '',
|
qrCodeUrl: '',
|
||||||
baseURLPy: 'http://127.0.0.1:8000',
|
baseURLPy: baseURLPy,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// 本地开发 - 电脑浏览器调试使用
|
// 本地开发 - 电脑浏览器调试使用
|
||||||
export const baseURL = 'http://127.0.0.1:8080'
|
// export const baseURL = 'http://127.0.0.1:8080'
|
||||||
export const baseURLPy = 'http://127.0.0.1:8000'
|
// export const baseURLPy = 'http://127.0.0.1:8000'
|
||||||
|
|
||||||
// 开发环境 - 手机端调试使用局域网IP(需要时取消注释)
|
// 开发环境 - 手机端调试使用局域网IP(需要时取消注释)
|
||||||
// export const baseURL = 'http://192.168.1.164:8080'
|
export const baseURL = 'http://192.168.1.164:8080'
|
||||||
// export const baseURLPy = 'http://192.168.1.164:8000'
|
export const baseURLPy = 'http://192.168.1.164:8000'
|
||||||
|
|
||||||
export const sid = 2
|
export const sid = 2
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
35672
xunifriend_RaeeC/runtime/log/202602/1769936477-01.log
Normal file
35672
xunifriend_RaeeC/runtime/log/202602/1769936477-01.log
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
||||||
|
# 主要功能
|
||||||
1. 将密码校验删除,因为无法生成模型,用最简单的方法来尝试这些内容。
|
1. 将密码校验删除,因为无法生成模型,用最简单的方法来尝试这些内容。
|
||||||
- [ ] 增加tab栏但是还没有加上对应的功能
|
- [ ] 增加tab栏但是还没有加上对应的功能
|
||||||
3. 增加聊天背景选择功能,会员可以自定义背景
|
3. 增加聊天背景选择功能,会员可以自定义背景
|
||||||
|
|
@ -9,10 +10,15 @@
|
||||||
- [x] 克隆音色API填写然后给你测试
|
- [x] 克隆音色API填写然后给你测试
|
||||||
7. 二维码推广功能:创建邀请码邀请新用户,但是没有二维码生成API
|
7. 二维码推广功能:创建邀请码邀请新用户,但是没有二维码生成API
|
||||||
|
|
||||||
|
# 次要接口对齐和UI完善
|
||||||
|
> 1. Tab栏中各内容对齐
|
||||||
|
> 2. 短剧、商城、音乐库对接
|
||||||
|
> 3. UI完善:对齐筑梦岛
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
上线需调整
|
上线需调整
|
||||||
|
|
||||||
1. oss存储桶:用来存放播放的音色
|
1. oss存储桶:用来存放播放的音色
|
||||||
2. DashCope阿里云模型api:sk-xxx 并启用下面模型
|
2. DashCope阿里云模型api:sk-xxx 并启用下面模型
|
||||||
- qwen-plus:AI对话聊天
|
- qwen-plus:AI对话聊天
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user