From 21c75461b4b8c15690e59d5d329375932bad4bfc Mon Sep 17 00:00:00 2001
From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com>
Date: Tue, 3 Feb 2026 17:13:56 +0800
Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=9ATab=E6=A0=8F?=
=?UTF-8?q?=E7=BE=8E=E5=8C=96=E5=B9=B6=E5=B0=86=E6=8E=A5=E5=8F=A3=E6=8E=A5?=
=?UTF-8?q?=E9=BD=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lover/main.py | 2 +
lover/models.py | 29 +
lover/response.py | 4 +
lover/routers/music_library.py | 398 ++++
start_php_advanced.bat | 161 ++
test_reset.py | 51 -
test_vip.py | 33 -
xuniYou/pages/index/index.vue | 3113 +++++++++++++++++++++++++++--
开发/2026年2月3日/2026年2月3日.md | 2 +
开发/2026年2月3日/音乐库.sql | 34 +
10 files changed, 3600 insertions(+), 227 deletions(-)
create mode 100644 lover/routers/music_library.py
create mode 100644 start_php_advanced.bat
delete mode 100644 test_reset.py
delete mode 100644 test_vip.py
create mode 100644 开发/2026年2月3日/2026年2月3日.md
create mode 100644 开发/2026年2月3日/音乐库.sql
diff --git a/lover/main.py b/lover/main.py
index e29015d..98a9c12 100644
--- a/lover/main.py
+++ b/lover/main.py
@@ -21,6 +21,7 @@ from lover.routers import friend as friend_router
from lover.routers import msg as msg_router
from lover.routers import huanxin as huanxin_router
from lover.routers import user as user_router
+from lover.routers import music_library as music_library_router
from lover.task_queue import start_sing_workers
from lover.config import settings
@@ -82,6 +83,7 @@ app.include_router(friend_router.router)
app.include_router(msg_router.router)
app.include_router(huanxin_router.router)
app.include_router(user_router.router)
+app.include_router(music_library_router.router)
@app.exception_handler(HTTPException)
diff --git a/lover/models.py b/lover/models.py
index 2f5cf79..a0b1cf9 100644
--- a/lover/models.py
+++ b/lover/models.py
@@ -422,6 +422,35 @@ class OutfitItem(Base):
updatetime = Column(BigInteger)
+class MusicLibrary(Base):
+ __tablename__ = "nf_music_library"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ user_id = Column(BigInteger, nullable=False, index=True)
+ title = Column(String(255), nullable=False)
+ artist = Column(String(255))
+ music_url = Column(String(500), nullable=False)
+ cover_url = Column(String(500))
+ duration = Column(Integer)
+ upload_type = Column(String(10), nullable=False, default="link") # file, link
+ is_public = Column(Integer, nullable=False, default=1)
+ play_count = Column(Integer, nullable=False, default=0)
+ like_count = Column(Integer, nullable=False, default=0)
+ status = Column(String(20), nullable=False, default="approved") # pending, approved, rejected
+ created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
+ updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
+ deleted_at = Column(DateTime)
+
+
+class MusicLike(Base):
+ __tablename__ = "nf_music_likes"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ user_id = Column(BigInteger, nullable=False, index=True)
+ music_id = Column(BigInteger, nullable=False, index=True)
+ created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
+
+
class OutfitLook(Base):
__tablename__ = "nf_outfit_looks"
diff --git a/lover/response.py b/lover/response.py
index 321f242..8a00081 100644
--- a/lover/response.py
+++ b/lover/response.py
@@ -18,3 +18,7 @@ class ApiResponse(BaseModel, Generic[T]):
def success_response(data: Optional[T] = None, msg: str = "ok") -> ApiResponse[T]:
return ApiResponse[T](code=1, msg=msg, data=data)
+
+
+def error_response(msg: str = "error", code: int = 0, data: Optional[T] = None) -> ApiResponse[T]:
+ return ApiResponse[T](code=code, msg=msg, data=data)
diff --git a/lover/routers/music_library.py b/lover/routers/music_library.py
new file mode 100644
index 0000000..feec0a1
--- /dev/null
+++ b/lover/routers/music_library.py
@@ -0,0 +1,398 @@
+"""
+音乐库路由
+"""
+from typing import List, Optional
+from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
+from sqlalchemy.orm import Session
+from pydantic import BaseModel, Field
+from datetime import datetime
+import os
+import uuid
+
+from lover.db import get_db
+from lover.deps import get_current_user
+from lover.models import User, MusicLibrary, MusicLike
+from lover.response import success_response, error_response, ApiResponse
+from lover.config import settings
+
+router = APIRouter(prefix="/music", tags=["音乐库"])
+
+
+# ========== Pydantic 模型 ==========
+class MusicOut(BaseModel):
+ id: int
+ user_id: int
+ username: str = ""
+ user_avatar: str = ""
+ title: str
+ artist: Optional[str] = None
+ music_url: str
+ cover_url: Optional[str] = None
+ duration: Optional[int] = None
+ upload_type: str
+ play_count: int = 0
+ like_count: int = 0
+ is_liked: bool = False
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class MusicListResponse(BaseModel):
+ total: int
+ list: List[MusicOut]
+
+
+class MusicAddLinkRequest(BaseModel):
+ title: str = Field(..., min_length=1, max_length=255, description="歌曲标题")
+ artist: Optional[str] = Field(None, max_length=255, description="艺术家")
+ music_url: str = Field(..., min_length=1, max_length=500, description="音乐链接")
+ cover_url: Optional[str] = Field(None, max_length=500, description="封面图链接")
+ duration: Optional[int] = Field(None, description="时长(秒)")
+
+
+# ========== 辅助函数 ==========
+def _cdnize(url: str) -> str:
+ """将相对路径转换为 CDN 完整路径"""
+ if not url:
+ return ""
+ if url.startswith("http://") or url.startswith("https://"):
+ return url
+ cdn_domain = getattr(settings, "CDN_DOMAIN", "")
+ if cdn_domain:
+ return f"{cdn_domain.rstrip('/')}/{url.lstrip('/')}"
+ return url
+
+
+def _save_upload_file(file: UploadFile, folder: str = "music") -> str:
+ """保存上传的文件"""
+ # 生成唯一文件名
+ ext = os.path.splitext(file.filename)[1]
+ filename = f"{uuid.uuid4().hex}{ext}"
+
+ # 创建保存路径
+ upload_dir = os.path.join("public", folder)
+ os.makedirs(upload_dir, exist_ok=True)
+
+ file_path = os.path.join(upload_dir, filename)
+
+ # 保存文件
+ with open(file_path, "wb") as f:
+ f.write(file.file.read())
+
+ # 返回相对路径
+ return f"{folder}/{filename}"
+
+
+# ========== API 路由 ==========
+@router.get("/library", response_model=ApiResponse[MusicListResponse])
+def get_music_library(
+ page: int = 1,
+ page_size: int = 20,
+ keyword: Optional[str] = None,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 获取音乐库列表(所有公开的音乐)
+ """
+ query = db.query(MusicLibrary).filter(
+ MusicLibrary.deleted_at.is_(None),
+ MusicLibrary.is_public == 1,
+ MusicLibrary.status == "approved"
+ )
+
+ # 关键词搜索
+ if keyword:
+ query = query.filter(
+ (MusicLibrary.title.like(f"%{keyword}%")) |
+ (MusicLibrary.artist.like(f"%{keyword}%"))
+ )
+
+ # 总数
+ total = query.count()
+
+ # 分页
+ offset = (page - 1) * page_size
+ music_list = query.order_by(MusicLibrary.created_at.desc()).offset(offset).limit(page_size).all()
+
+ # 获取用户点赞信息
+ liked_music_ids = set()
+ if user:
+ likes = db.query(MusicLike.music_id).filter(MusicLike.user_id == user.id).all()
+ liked_music_ids = {like.music_id for like in likes}
+
+ # 获取上传用户信息
+ user_ids = [m.user_id for m in music_list]
+ users_map = {}
+ if user_ids:
+ users = db.query(User).filter(User.id.in_(user_ids)).all()
+ users_map = {u.id: u for u in users}
+
+ # 构建返回数据
+ result = []
+ for music in music_list:
+ uploader = users_map.get(music.user_id)
+ result.append(MusicOut(
+ id=music.id,
+ user_id=music.user_id,
+ username=uploader.username if uploader else "未知用户",
+ user_avatar=_cdnize(uploader.avatar) if uploader and uploader.avatar else "",
+ title=music.title,
+ artist=music.artist,
+ music_url=_cdnize(music.music_url),
+ cover_url=_cdnize(music.cover_url) if music.cover_url else "",
+ duration=music.duration,
+ upload_type=music.upload_type,
+ play_count=music.play_count,
+ like_count=music.like_count,
+ is_liked=music.id in liked_music_ids,
+ created_at=music.created_at
+ ))
+
+ return success_response(MusicListResponse(total=total, list=result))
+
+
+@router.post("/add-link", response_model=ApiResponse[MusicOut])
+def add_music_link(
+ data: MusicAddLinkRequest,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 添加音乐链接到音乐库
+ """
+ # 创建音乐记录
+ music = MusicLibrary(
+ user_id=user.id,
+ title=data.title,
+ artist=data.artist,
+ music_url=data.music_url,
+ cover_url=data.cover_url,
+ duration=data.duration,
+ upload_type="link",
+ is_public=1,
+ status="approved"
+ )
+
+ db.add(music)
+ db.commit()
+ db.refresh(music)
+
+ return success_response(MusicOut(
+ id=music.id,
+ user_id=music.user_id,
+ username=user.username,
+ user_avatar=_cdnize(user.avatar) if user.avatar else "",
+ title=music.title,
+ artist=music.artist,
+ music_url=_cdnize(music.music_url),
+ cover_url=_cdnize(music.cover_url) if music.cover_url else "",
+ duration=music.duration,
+ upload_type=music.upload_type,
+ play_count=music.play_count,
+ like_count=music.like_count,
+ is_liked=False,
+ created_at=music.created_at
+ ))
+
+
+@router.post("/upload", response_model=ApiResponse[MusicOut])
+async def upload_music_file(
+ title: str = Form(...),
+ artist: Optional[str] = Form(None),
+ duration: Optional[int] = Form(None),
+ music_file: UploadFile = File(...),
+ cover_file: Optional[UploadFile] = File(None),
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 上传音乐文件到音乐库
+ """
+ # 检查文件类型
+ allowed_audio = [".mp3", ".wav", ".m4a", ".flac", ".ogg"]
+ allowed_image = [".jpg", ".jpeg", ".png", ".gif", ".webp"]
+
+ music_ext = os.path.splitext(music_file.filename)[1].lower()
+ if music_ext not in allowed_audio:
+ return error_response("不支持的音频格式")
+
+ # 保存音乐文件
+ music_path = _save_upload_file(music_file, "music")
+
+ # 保存封面文件
+ cover_path = None
+ if cover_file:
+ cover_ext = os.path.splitext(cover_file.filename)[1].lower()
+ if cover_ext in allowed_image:
+ cover_path = _save_upload_file(cover_file, "music/covers")
+
+ # 创建音乐记录
+ music = MusicLibrary(
+ user_id=user.id,
+ title=title,
+ artist=artist,
+ music_url=music_path,
+ cover_url=cover_path,
+ duration=duration,
+ upload_type="file",
+ is_public=1,
+ status="approved"
+ )
+
+ db.add(music)
+ db.commit()
+ db.refresh(music)
+
+ return success_response(MusicOut(
+ id=music.id,
+ user_id=music.user_id,
+ username=user.username,
+ user_avatar=_cdnize(user.avatar) if user.avatar else "",
+ title=music.title,
+ artist=music.artist,
+ music_url=_cdnize(music.music_url),
+ cover_url=_cdnize(music.cover_url) if music.cover_url else "",
+ duration=music.duration,
+ upload_type=music.upload_type,
+ play_count=music.play_count,
+ like_count=music.like_count,
+ is_liked=False,
+ created_at=music.created_at
+ ))
+
+
+@router.post("/{music_id}/like", response_model=ApiResponse[dict])
+def like_music(
+ music_id: int,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 点赞音乐
+ """
+ # 检查音乐是否存在
+ music = db.query(MusicLibrary).filter(
+ MusicLibrary.id == music_id,
+ MusicLibrary.deleted_at.is_(None)
+ ).first()
+
+ if not music:
+ return error_response("音乐不存在")
+
+ # 检查是否已点赞
+ existing_like = db.query(MusicLike).filter(
+ MusicLike.user_id == user.id,
+ MusicLike.music_id == music_id
+ ).first()
+
+ if existing_like:
+ # 取消点赞
+ db.delete(existing_like)
+ music.like_count = max(0, music.like_count - 1)
+ db.commit()
+ return success_response({"is_liked": False, "like_count": music.like_count})
+ else:
+ # 添加点赞
+ like = MusicLike(user_id=user.id, music_id=music_id)
+ db.add(like)
+ music.like_count += 1
+ db.commit()
+ return success_response({"is_liked": True, "like_count": music.like_count})
+
+
+@router.post("/{music_id}/play", response_model=ApiResponse[dict])
+def record_play(
+ music_id: int,
+ db: Session = Depends(get_db),
+):
+ """
+ 记录播放次数
+ """
+ music = db.query(MusicLibrary).filter(
+ MusicLibrary.id == music_id,
+ MusicLibrary.deleted_at.is_(None)
+ ).first()
+
+ if not music:
+ return error_response("音乐不存在")
+
+ music.play_count += 1
+ db.commit()
+
+ return success_response({"play_count": music.play_count})
+
+
+@router.delete("/{music_id}", response_model=ApiResponse[dict])
+def delete_music(
+ music_id: int,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 删除音乐(仅限上传者)
+ """
+ music = db.query(MusicLibrary).filter(
+ MusicLibrary.id == music_id,
+ MusicLibrary.deleted_at.is_(None)
+ ).first()
+
+ if not music:
+ return error_response("音乐不存在")
+
+ if music.user_id != user.id:
+ return error_response("无权删除此音乐")
+
+ music.deleted_at = datetime.utcnow()
+ db.commit()
+
+ return success_response({"message": "删除成功"})
+
+
+@router.get("/my", response_model=ApiResponse[MusicListResponse])
+def get_my_music(
+ page: int = 1,
+ page_size: int = 20,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ 获取我上传的音乐
+ """
+ query = db.query(MusicLibrary).filter(
+ MusicLibrary.user_id == user.id,
+ MusicLibrary.deleted_at.is_(None)
+ )
+
+ total = query.count()
+
+ offset = (page - 1) * page_size
+ music_list = query.order_by(MusicLibrary.created_at.desc()).offset(offset).limit(page_size).all()
+
+ # 获取点赞信息
+ liked_music_ids = set()
+ likes = db.query(MusicLike.music_id).filter(MusicLike.user_id == user.id).all()
+ liked_music_ids = {like.music_id for like in likes}
+
+ result = []
+ for music in music_list:
+ result.append(MusicOut(
+ id=music.id,
+ user_id=music.user_id,
+ username=user.username,
+ user_avatar=_cdnize(user.avatar) if user.avatar else "",
+ title=music.title,
+ artist=music.artist,
+ music_url=_cdnize(music.music_url),
+ cover_url=_cdnize(music.cover_url) if music.cover_url else "",
+ duration=music.duration,
+ upload_type=music.upload_type,
+ play_count=music.play_count,
+ like_count=music.like_count,
+ is_liked=music.id in liked_music_ids,
+ created_at=music.created_at
+ ))
+
+ return success_response(MusicListResponse(total=total, list=result))
diff --git a/start_php_advanced.bat b/start_php_advanced.bat
new file mode 100644
index 0000000..855be97
--- /dev/null
+++ b/start_php_advanced.bat
@@ -0,0 +1,161 @@
+@echo off
+chcp 65001 >nul
+title PHP 开发服务器
+
+:MENU
+cls
+echo ========================================
+echo PHP 开发服务器启动脚本 (高级版)
+echo ========================================
+echo.
+echo 请选择启动模式:
+echo.
+echo [1] 快速启动 (端口 8080)
+echo [2] 自定义端口
+echo [3] 查看 PHP 信息
+echo [4] 退出
+echo.
+echo ========================================
+set /p choice=请输入选项 (1-4):
+
+if "%choice%"=="1" goto QUICK_START
+if "%choice%"=="2" goto CUSTOM_PORT
+if "%choice%"=="3" goto PHP_INFO
+if "%choice%"=="4" goto END
+echo [错误] 无效选项,请重新选择
+timeout /t 2 >nul
+goto MENU
+
+:QUICK_START
+set PORT=8080
+goto START_SERVER
+
+:CUSTOM_PORT
+echo.
+set /p PORT=请输入端口号 (例如: 8080):
+if "%PORT%"=="" (
+ echo [错误] 端口号不能为空
+ timeout /t 2 >nul
+ goto MENU
+)
+goto START_SERVER
+
+:START_SERVER
+cls
+echo ========================================
+echo 正在启动 PHP 开发服务器...
+echo ========================================
+echo.
+
+REM 设置 PHP 路径
+set PHP_PATH=D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe
+
+REM 检查 PHP 是否存在
+if not exist "%PHP_PATH%" (
+ echo [错误] PHP 未找到: %PHP_PATH%
+ echo.
+ echo 请修改脚本中的 PHP_PATH 变量
+ pause
+ goto MENU
+)
+
+REM 显示 PHP 版本
+echo [信息] PHP 版本:
+"%PHP_PATH%" -v | findstr /C:"PHP"
+echo.
+
+REM 设置项目根目录
+set PROJECT_ROOT=%~dp0xunifriend_RaeeC\public
+
+REM 检查项目目录是否存在
+if not exist "%PROJECT_ROOT%" (
+ echo [错误] 项目目录未找到: %PROJECT_ROOT%
+ pause
+ goto MENU
+)
+
+REM 获取本机 IP 地址
+for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /C:"IPv4"') do (
+ set LOCAL_IP=%%a
+ goto :IP_FOUND
+)
+:IP_FOUND
+set LOCAL_IP=%LOCAL_IP: =%
+
+REM 设置服务器参数
+set HOST=0.0.0.0
+
+echo [信息] 项目目录: %PROJECT_ROOT%
+echo [信息] 服务器端口: %PORT%
+echo.
+echo ========================================
+echo 访问地址:
+echo ========================================
+echo.
+echo [本地访问]
+echo http://127.0.0.1:%PORT%
+echo http://localhost:%PORT%
+echo.
+echo [局域网访问]
+echo http://%LOCAL_IP%:%PORT%
+echo.
+echo [管理后台]
+echo http://127.0.0.1:%PORT%/admin
+echo.
+echo ========================================
+echo.
+echo [提示] 按 Ctrl+C 停止服务器
+echo.
+
+REM 询问是否打开浏览器
+set /p OPEN_BROWSER=是否自动打开浏览器? (Y/N):
+if /i "%OPEN_BROWSER%"=="Y" (
+ echo [信息] 正在打开浏览器...
+ start http://127.0.0.1:%PORT%
+)
+
+echo.
+echo [信息] 服务器启动中...
+echo ========================================
+echo.
+
+REM 启动 PHP 内置服务器
+cd /d "%PROJECT_ROOT%"
+"%PHP_PATH%" -S %HOST%:%PORT% -t .
+
+pause
+goto MENU
+
+:PHP_INFO
+cls
+echo ========================================
+echo PHP 信息
+echo ========================================
+echo.
+
+set PHP_PATH=D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe
+
+if not exist "%PHP_PATH%" (
+ echo [错误] PHP 未找到: %PHP_PATH%
+ pause
+ goto MENU
+)
+
+echo [PHP 版本]
+"%PHP_PATH%" -v
+echo.
+echo [PHP 配置文件]
+"%PHP_PATH%" --ini
+echo.
+echo [已加载的扩展]
+"%PHP_PATH%" -m
+echo.
+
+pause
+goto MENU
+
+:END
+echo.
+echo 感谢使用!
+timeout /t 1 >nul
+exit
diff --git a/test_reset.py b/test_reset.py
deleted file mode 100644
index 9ab0b28..0000000
--- a/test_reset.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env python3
-"""测试视频生成次数重置功能"""
-from datetime import datetime, date
-from lover.db import SessionLocal
-from lover.models import User
-
-def test_reset_logic():
- db = SessionLocal()
- try:
- user = db.query(User).filter(User.id == 70).with_for_update().first()
- if not user:
- print("用户不存在")
- return
-
- print(f"重置前:")
- print(f" video_gen_remaining: {user.video_gen_remaining}")
- print(f" video_gen_reset_date: {user.video_gen_reset_date}")
-
- # 模拟重置逻辑
- current_timestamp = int(datetime.utcnow().timestamp())
- is_vip = user.vip_endtime and user.vip_endtime > current_timestamp
-
- if is_vip:
- last_reset = user.video_gen_reset_date
- today = datetime.utcnow().date()
-
- print(f"\n检查:")
- print(f" 是否 VIP: {is_vip}")
- print(f" 上次重置日期: {last_reset}")
- print(f" 今天日期: {today}")
- print(f" 需要重置: {not last_reset or last_reset < today}")
-
- if not last_reset or last_reset < today:
- user.video_gen_remaining = 2
- user.video_gen_reset_date = today
- db.add(user)
- db.commit()
-
- print(f"\n重置后:")
- print(f" video_gen_remaining: {user.video_gen_remaining}")
- print(f" video_gen_reset_date: {user.video_gen_reset_date}")
- else:
- print("\n今天已经重置过了,不需要再次重置")
- else:
- print("用户不是 VIP,不重置")
-
- finally:
- db.close()
-
-if __name__ == "__main__":
- test_reset_logic()
diff --git a/test_vip.py b/test_vip.py
deleted file mode 100644
index 88447f2..0000000
--- a/test_vip.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python3
-"""测试 VIP 功能"""
-from datetime import datetime
-from lover.db import SessionLocal
-from lover.models import User
-
-def test_vip_status():
- db = SessionLocal()
- try:
- # 测试用户 70 和 84
- for user_id in [70, 84]:
- user = db.query(User).filter(User.id == user_id).first()
- if not user:
- print(f"用户 {user_id} 不存在")
- continue
-
- current_timestamp = int(datetime.utcnow().timestamp())
- is_vip = user.vip_endtime and user.vip_endtime > current_timestamp
-
- print(f"\n用户 {user_id} ({user.nickname}):")
- print(f" VIP 到期时间戳: {user.vip_endtime}")
- if user.vip_endtime:
- vip_end_date = datetime.fromtimestamp(user.vip_endtime)
- print(f" VIP 到期日期: {vip_end_date}")
- print(f" 当前时间戳: {current_timestamp}")
- print(f" 是否 VIP: {is_vip}")
- print(f" 视频生成次数: {user.video_gen_remaining}")
- print(f" 上次重置日期: {user.video_gen_reset_date}")
- finally:
- db.close()
-
-if __name__ == "__main__":
- test_vip_status()
diff --git a/xuniYou/pages/index/index.vue b/xuniYou/pages/index/index.vue
index 8d29ec6..9ca18bf 100644
--- a/xuniYou/pages/index/index.vue
+++ b/xuniYou/pages/index/index.vue
@@ -64,81 +64,210 @@
:duration="300"
:indicator-dots="false">
-
+
-
-
-
-
-
-
- 你好,{{ getBobbiesList.nickname ? getBobbiesList.nickname : '匿名' }}
-
-
-
- {{ getBobbiesList.level ? getBobbiesList.level : 0 }}
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Lv.{{ getBobbiesList.level ? getBobbiesList.level : 0 }}({{
- getBobbiesList.intimacy ? getBobbiesList.intimacy : 0 }}/{{
- getBobbiesList.next_level_intimacy ? getBobbiesList.next_level_intimacy : 0
- }})
+
+
+
+
+
+ 💎
+ 精品商城
+
+
+
+
+
+
+
+
+
+ 超值优惠
+ 海量道具 · 限时特惠 · 每日上新
+
+
+
+
+ 🎁
+ 新人礼包
+
+
+ ⚡
+ 限时秒杀
+
+
+ 💰
+ 超值折扣
+
+
+ 🔥
+ 热门推荐
+
+
+
+
+
+ 立即进入商城
+ →
+
+
+
+
+ ✨
+ 点击任意位置进入商城
-
-
-
-
-
-
- {{ getBobbiesList.bond_today ? getBobbiesList.bond_today : 0 }}/{{
- getBobbiesList.bond_today_all ? getBobbiesList.bond_today_all : 0
- }}
+
+
+
+
+
+
+
+
+
+
+
+ ✦
+ 选择一位互动对象
+ ✦
+ 搜索更多
+
+
+
+
+
+
+ {{ loverBasicList && loverBasicList.name ? loverBasicList.name : '简寻' }}
+
+
+
+
+
+
+
+ 多面伪神
+ 甜蜜贴心
+
+
+
+
+ 靠近点,再近点…你就能看清我眼里藏着什么了。
+
+
+
+
+
+
+
+
+ 和TA聊天
- 每日牵绊
-
-
-
-
-
-
-
-
-
-
- 去聊天
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- 礼物匣
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HOT
+
+
+
+ 精品商城
+ 🛒
+
+ 海量道具 · 超值优惠
+
+
+
+
+
+ 立即前往
+ →
+
+
+
+
+
+
+
+
+
+ Lv.{{ getBobbiesList.level ? getBobbiesList.level : 0 }}
+
+
+
+ {{ getBobbiesList.intimacy ? getBobbiesList.intimacy : 0 }}/{{ getBobbiesList.next_level_intimacy ? getBobbiesList.next_level_intimacy : 0 }}
-
-
-
-
- 装束设置
-
-
-
@@ -190,19 +319,97 @@
-
- 🎤
- 唱歌功能
- 让她为你唱一首歌
-
-
-
-
-
+
-
- 👗
- 换装搭配
- 正在跳转到装束设置页面...
-
+
+
+
+ 加载中...
+
+
+ 上衣
+
+
+
+
+ ✓
+ 使用中
+ 🔒
+
+
+
+
+
+ 下装
+
+
+
+
+ ✓
+ 使用中
+ 🔒
+
+
+
+
+
+ 连体服
+
+
+
+
+ ✓
+ 使用中
+ 🔒
+
+
+
+
+
+ {{ outfitSaveToLook ? '已勾选:保存到形象栏' : '勾选:保存到形象栏' }}
+ 换装
+
+
+
-
-
- 🎁
- 送礼物
- 送她一份心意
- 打开礼物匣
+
+
+
+
+
+
+
+
+ {{ item.name }}
+ {{ item.price }}金币
+ +{{ item.intimacy_value }}好感
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ giftInfoOptions && giftInfoOptions.name ? giftInfoOptions.name : '' }}
+ +{{ giftInfoOptions && giftInfoOptions.intimacy_value ? giftInfoOptions.intimacy_value : '0' }} 好感度
+ {{ giftInfoOptions && giftInfoOptions.price ? giftInfoOptions.price : '0' }} 金币
+
+
+
+
+ 数量
+
+
+
+
+
+
+
+ 余额:{{ giftUserInfo && giftUserInfo.money != null ? giftUserInfo.money : 0 }} 金币
+
+
+ 充值并赠送
+ 赠送Ta
+
+
@@ -304,41 +609,75 @@
-
- 🎬
- 短剧
- 观看精彩短剧内容
-
-
-
-
-
- 推荐
-
- 🎬 精彩短剧推荐
- 点击观看更多精彩内容
- 立即观看 →
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔥
+ 热门推荐
-
-
-
-
- 短剧标题 {{i}}
- 精彩剧情简介...
- 立即播放
-
+
+ 精彩短剧
+ 海量热门短剧等你来看
+
+
+
+ 💕 甜宠
+ 👑 霸总
+ 🎭 逆袭
+ ⚡ 爽文
+
+
+
+
+ ▶
+ 立即观看
+
+
+
+
+ ✨
+ 点击任意位置进入短剧世界
-
+
+
+
+
+
+ 📺
+ 海量剧集
+
+
+ 🆓
+ 免费观看
+
+
+ ⚡
+ 每日更新
+
+
-
+
-
+
@@ -397,6 +736,9 @@
正在生成视频中
预计等待15分钟
+
+ 取消生成
+
@@ -404,6 +746,48 @@
正在生成视频中
预计等待15分钟
+
+ 取消生成
+
+
+
+
+
+
+
+
+
+
+ 歌曲标题 *
+
+
+
+ 艺术家
+
+
+
+ 音乐链接 *
+
+
+
+ 封面图链接
+
+
+
+ 时长(秒)
+
+
+
+ 提交
+
+
@@ -414,6 +798,14 @@
GetUserBasic,
LoverInit,
LoverBasic,
+ OutfitList,
+ OutfitPurchase,
+ OutfitChange,
+ GiftsLists,
+ GiftsGive,
+ GiftsGiveApi,
+ CreateGiftsOrderApi,
+ JinbiPayApi,
SingSongs,
SingGenerate,
SingGenerateTask,
@@ -424,11 +816,12 @@
SessionInit,
SessionSend
} from '@/utils/api.js'
- import { baseURLPy } from '@/utils/request.js'
+ import { baseURL, baseURLPy } from '@/utils/request.js'
import notHave from '@/components/not-have.vue';
import topSafety from '@/components/top-safety.vue';
import tabBar from '@/components/tab-bar.vue';
import UnderAge from '@/components/under-age.vue'; // 导入under-age组件
+ import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue'; // 导入uni-nav-bar组件
import {
useConversationStore
} from '@/stores/conversation';
@@ -439,22 +832,27 @@
topSafety,
tabBar,
UnderAge,
+ uniNavBar,
},
data() {
return {
// Tab 相关
- currentTab: 0,
- tabIntoView: 'tab-0',
+ currentTab: 1, // 默认显示首页(商城是 0,首页是 1)
+ tabIntoView: 'tab-1',
tabs: [
+ { name: '商城' },
{ name: '首页' },
{ name: '聊天' },
{ name: '唱歌' },
{ name: '跳舞' },
{ name: '换服装' },
{ name: '刷礼物' },
- { name: '商城' },
{ name: '短剧' }
],
+ // 首页形象栏相关
+ homeLooksList: [],
+ selectedLookId: null,
+ currentLookImageUrl: '',
// 聊天相关
chatMessages: [],
chatInputText: '',
@@ -473,6 +871,49 @@
singGeneratingTaskId: 0,
danceGenerating: false, // 跳舞视频生成中状态
danceGeneratingTaskId: 0,
+ singPollTimer: null,
+ dancePollTimer: null,
+ // 音乐库相关
+ singCurrentTab: 'songs', // songs, library, history
+ musicLibraryList: [],
+ musicLibraryPage: 1,
+ musicLibraryPageSize: 20,
+ musicLibraryTotal: 0,
+ musicLibraryHasMore: true,
+ showMusicModal: false,
+ musicForm: {
+ title: '',
+ artist: '',
+ music_url: '',
+ cover_url: '',
+ duration: null
+ },
+ outfitListInfo: null,
+ outfitBuyForm: {
+ item_id: ''
+ },
+ outfitMode: null,
+ outfitFormTopBottom: {
+ top_item_id: '',
+ bottom_item_id: '',
+ save_to_look: false
+ },
+ outfitFormDress: {
+ dress_item_id: '',
+ save_to_look: false
+ },
+ outfitSaveToLook: false,
+ giftGlobal: baseURL,
+ giftStats: false,
+ successStats: false,
+ giftForm: {
+ gifts_id: '',
+ nums: 1,
+ to_user_id: ''
+ },
+ giftOptions: [],
+ giftInfoOptions: null,
+ giftUserInfo: {},
statusBarHeight: uni.getWindowInfo().statusBarHeight,
currentStep: 0,
chartData: {},
@@ -539,6 +980,7 @@
if (uni.getStorageSync('token')) {
this.getUserBasic()
this.loverBasic()
+ this.loadHomeLooks() // 加载首页形象栏
}
// 获取歌曲列表
@@ -550,47 +992,60 @@
if (this.currentTab === 3) {
this.restoreDanceGeneration();
}
+ if (this.currentTab === 4) {
+ this.ensureOutfitLoaded();
+ }
+ if (this.currentTab === 5) {
+ this.ensureGiftLoaded();
+ }
},
methods: {
// Tab 切换方法
switchTab(index) {
console.log('点击 Tab,切换到索引:', index, '对应 Tab:', this.tabs[index].name);
- // 如果点击的是"换服装" tab(索引为4),直接跳转到装束设置页面
- if (index === 4) {
- this.toreplacement();
- return;
- }
- // 如果切换到聊天 tab,初始化聊天会话
- if (index === 1 && !this.chatSessionId) {
+ // 如果切换到聊天 tab(现在是 index 2),初始化聊天会话
+ if (index === 2 && !this.chatSessionId) {
this.initChatSession();
}
this.currentTab = index;
this.updateTabIntoView(index);
- if (index === 2) {
+ if (index === 3) {
this.restoreSingGeneration();
}
- if (index === 3) {
+ if (index === 4) {
this.restoreDanceGeneration();
}
+ if (index === 5) {
+ this.ensureOutfitLoaded();
+ }
+ if (index === 6) {
+ this.ensureGiftLoaded();
+ }
},
onSwiperChange(e) {
console.log('Swiper 滑动,当前索引:', e.detail.current, '对应 Tab:', this.tabs[e.detail.current].name);
this.currentTab = e.detail.current;
this.updateTabIntoView(e.detail.current);
- // 如果滑动到聊天 tab,初始化聊天会话
- if (e.detail.current === 1 && !this.chatSessionId) {
+ // 如果滑动到聊天 tab(现在是 index 2),初始化聊天会话
+ if (e.detail.current === 2 && !this.chatSessionId) {
this.initChatSession();
}
- if (e.detail.current === 2) {
+ if (e.detail.current === 3) {
this.restoreSingGeneration();
}
- if (e.detail.current === 3) {
+ if (e.detail.current === 4) {
this.restoreDanceGeneration();
}
+ if (e.detail.current === 5) {
+ this.ensureOutfitLoaded();
+ }
+ if (e.detail.current === 6) {
+ this.ensureGiftLoaded();
+ }
},
updateTabIntoView(index) {
this.$nextTick(() => {
@@ -694,6 +1149,295 @@
}
}).catch(() => {});
},
+ ensureOutfitLoaded() {
+ if (!uni.getStorageSync('token')) {
+ return;
+ }
+ if (this.outfitListInfo) {
+ return;
+ }
+ this.fetchOutfitList();
+ },
+ fetchOutfitList() {
+ OutfitList({}).then(res => {
+ if (res && res.code == 1) {
+ this.outfitListInfo = res.data || null;
+ this.normalizeOutfitList();
+ this.selectCurrentOutfitInTab();
+ } else {
+ this.outfitListInfo = null;
+ uni.showToast({ title: (res && res.msg) ? res.msg : '加载失败', icon: 'none' });
+ }
+ }).catch(() => {
+ this.outfitListInfo = null;
+ uni.showToast({ title: '加载失败', icon: 'none' });
+ });
+ },
+ normalizeOutfitList() {
+ if (!this.outfitListInfo) return;
+ const owned = Array.isArray(this.outfitListInfo.owned_outfit_ids) ? this.outfitListInfo.owned_outfit_ids : [];
+ const current = this.outfitListInfo.current_outfit || {};
+ const mark = (arr, type) => {
+ if (!Array.isArray(arr)) return;
+ for (let i = 0; i < arr.length; i++) {
+ const it = arr[i];
+ if (type === 'top') it.is_current = (current.top_id == it.id);
+ if (type === 'bottom') it.is_current = (current.bottom_id == it.id);
+ if (type === 'dress') it.is_current = (current.dress_id == it.id);
+ it.is_lock = !owned.includes(it.id);
+ }
+ };
+ mark(this.outfitListInfo.top, 'top');
+ mark(this.outfitListInfo.bottom, 'bottom');
+ mark(this.outfitListInfo.dress, 'dress');
+ },
+ selectCurrentOutfitInTab() {
+ if (!this.outfitListInfo) return;
+ const cur = this.outfitListInfo.current_outfit || {};
+ if (cur.dress_id) {
+ this.outfitMode = 1;
+ this.outfitFormDress.dress_item_id = cur.dress_id;
+ this.outfitFormTopBottom.top_item_id = '';
+ this.outfitFormTopBottom.bottom_item_id = '';
+ return;
+ }
+ if (cur.top_id || cur.bottom_id) {
+ this.outfitMode = 2;
+ this.outfitFormTopBottom.top_item_id = cur.top_id || '';
+ this.outfitFormTopBottom.bottom_item_id = cur.bottom_id || '';
+ this.outfitFormDress.dress_item_id = '';
+ }
+ },
+ isSelectedTop(id) {
+ return this.outfitFormTopBottom.top_item_id === id;
+ },
+ isSelectedBottom(id) {
+ return this.outfitFormTopBottom.bottom_item_id === id;
+ },
+ isSelectedDress(id) {
+ return this.outfitFormDress.dress_item_id === id;
+ },
+ openOutfitDetail(item) {
+ if (!item || !item.id) return;
+ this.outfitBuyForm.item_id = item.id;
+ const price = item.price_gold ? String(item.price_gold) : '0';
+ uni.showModal({
+ title: '购买服装',
+ content: `${item.name ? item.name : '该服装'}\n价格:${price}金币`,
+ confirmText: '购买',
+ cancelText: '取消',
+ success: (r) => {
+ if (r.confirm) {
+ this.buyOutfit();
+ }
+ }
+ });
+ },
+ buyOutfit() {
+ if (!this.outfitBuyForm.item_id) return;
+ OutfitPurchase(this.outfitBuyForm).then(res => {
+ if (res && res.code == 1) {
+ uni.showToast({ title: '购买成功', icon: 'none' });
+ this.fetchOutfitList();
+ } else {
+ uni.showToast({ title: (res && res.msg) ? res.msg : '购买失败', icon: 'none' });
+ }
+ }).catch(() => {
+ uni.showToast({ title: '购买失败', icon: 'none' });
+ });
+ },
+ selectTop(item) {
+ if (!item) return;
+ if (item.is_lock && !item.is_free) {
+ this.openOutfitDetail(item);
+ return;
+ }
+ if (this.outfitMode === 1 && this.outfitFormDress.dress_item_id) {
+ uni.showToast({ title: '连体服模式下不能选择上衣', icon: 'none' });
+ return;
+ }
+ this.outfitMode = 2;
+ this.outfitFormTopBottom.top_item_id = (this.outfitFormTopBottom.top_item_id === item.id) ? '' : item.id;
+ },
+ selectBottom(item) {
+ if (!item) return;
+ if (item.is_lock && !item.is_free) {
+ this.openOutfitDetail(item);
+ return;
+ }
+ if (this.outfitMode === 1 && this.outfitFormDress.dress_item_id) {
+ uni.showToast({ title: '连体服模式下不能选择下装', icon: 'none' });
+ return;
+ }
+ this.outfitMode = 2;
+ this.outfitFormTopBottom.bottom_item_id = (this.outfitFormTopBottom.bottom_item_id === item.id) ? '' : item.id;
+ },
+ selectDress(item) {
+ if (!item) return;
+ if (item.is_lock && !item.is_free) {
+ this.openOutfitDetail(item);
+ return;
+ }
+ if (this.outfitMode === 2 && (this.outfitFormTopBottom.top_item_id || this.outfitFormTopBottom.bottom_item_id)) {
+ uni.showToast({ title: '上衣下装模式下不能选择连体服', icon: 'none' });
+ return;
+ }
+ this.outfitMode = 1;
+ if (this.outfitFormDress.dress_item_id === item.id) {
+ this.outfitFormDress.dress_item_id = '';
+ } else {
+ this.outfitFormTopBottom.top_item_id = '';
+ this.outfitFormTopBottom.bottom_item_id = '';
+ this.outfitFormDress.dress_item_id = item.id;
+ }
+ },
+ toggleSaveToLook() {
+ this.outfitSaveToLook = !this.outfitSaveToLook;
+ this.outfitFormDress.save_to_look = this.outfitSaveToLook;
+ this.outfitFormTopBottom.save_to_look = this.outfitSaveToLook;
+ },
+ confirmOutfitChange() {
+ if (!this.outfitListInfo || !this.outfitListInfo.clothes_num) {
+ uni.showToast({ title: '当前暂无次数', icon: 'none' });
+ return;
+ }
+ const hasDress = this.outfitMode === 1 && this.outfitFormDress.dress_item_id;
+ const hasTopBottom = this.outfitMode === 2 && (this.outfitFormTopBottom.top_item_id || this.outfitFormTopBottom.bottom_item_id);
+ if (!hasDress && !hasTopBottom) {
+ uni.showToast({ title: '请先选择要换装的服装', icon: 'none' });
+ return;
+ }
+ uni.showModal({
+ title: '提示',
+ content: '确认是否换装',
+ success: (r) => {
+ if (r.confirm) {
+ this.doOutfitChange();
+ }
+ }
+ });
+ },
+ doOutfitChange() {
+ const payload = (this.outfitMode === 1 && this.outfitFormDress.dress_item_id) ? this.outfitFormDress : this.outfitFormTopBottom;
+ OutfitChange(payload).then(res => {
+ if (res && res.code == 1) {
+ uni.showToast({ title: '换装成功', icon: 'success' });
+ this.fetchOutfitList();
+ } else {
+ uni.showToast({ title: (res && res.msg) ? res.msg : '换装失败', icon: 'none' });
+ }
+ }).catch(() => {
+ uni.showToast({ title: '换装失败', icon: 'none' });
+ });
+ },
+ ensureGiftLoaded() {
+ if (!uni.getStorageSync('token')) {
+ return;
+ }
+ if (this.giftOptions && this.giftOptions.length > 0 && this.giftUserInfo) {
+ return;
+ }
+ this.giftGiftsLists();
+ this.giftGetUserBasic();
+ },
+ giftGetUserBasic() {
+ GetUserBasic({}).then(res => {
+ if (res && res.code == 1) {
+ this.giftUserInfo = res.data || {};
+ }
+ }).catch(() => {});
+ },
+ giftGiftsLists() {
+ GiftsLists({}).then(res => {
+ if (res && res.code == 1) {
+ this.giftOptions = (res.data && res.data.data) ? res.data.data : [];
+ } else {
+ this.giftOptions = [];
+ uni.showToast({ title: (res && res.msg) ? res.msg : '加载失败', icon: 'none', position: 'top' });
+ }
+ }).catch(() => {
+ this.giftOptions = [];
+ uni.showToast({ title: '加载失败', icon: 'none', position: 'top' });
+ });
+ },
+ giftGiftClick(index) {
+ this.giftInfoOptions = (this.giftOptions && this.giftOptions[index]) ? this.giftOptions[index] : null;
+ this.giftStats = !this.giftStats;
+ },
+ giftGiveClick() {
+ if (!this.giftInfoOptions) {
+ uni.showToast({ title: '请选择礼物', icon: 'none', position: 'top' });
+ return;
+ }
+ this.giftForm.gifts_id = this.giftInfoOptions.id;
+ const total = Number(this.giftInfoOptions.price || 0) * Number(this.giftForm.nums || 0);
+ const money = Number(this.giftUserInfo && this.giftUserInfo.money != null ? this.giftUserInfo.money : 0);
+ if (total > money) {
+ this.giftGiftsTopUp();
+ } else {
+ this.giftGiftsGive();
+ }
+ },
+ giftGiftsGive() {
+ let fn = GiftsGiveApi;
+ if (this.giftForm.to_user_id && String(this.giftForm.to_user_id).trim()) {
+ fn = GiftsGive;
+ }
+ fn(this.giftForm).then(res => {
+ if (res && res.code == 1) {
+ uni.showToast({ title: '赠送成功', icon: 'none', position: 'top' });
+ setTimeout(() => {
+ this.giftGetUserBasic();
+ this.getUserBasic(); // 刷新好感度数据
+ this.giftStats = false;
+ }, 1000);
+ } else {
+ uni.showToast({ title: (res && res.msg) ? res.msg : '赠送失败', icon: 'none', position: 'top' });
+ }
+ }).catch(() => {
+ uni.showToast({ title: '赠送失败', icon: 'none', position: 'top' });
+ });
+ },
+ giftGiftsTopUp() {
+ if (!this.giftInfoOptions) return;
+ CreateGiftsOrderApi({
+ gifts_id: this.giftInfoOptions.id,
+ nums: this.giftForm.nums,
+ pay_type: 'miniWechat'
+ }).then(res => {
+ if (res && res.code == 1 && res.data && res.data.out_trade_no) {
+ this.giftGoPay(res.data.out_trade_no);
+ }
+ }).catch(() => {});
+ },
+ giftGoPay(orderId) {
+ JinbiPayApi({ out_trade_no: orderId }).then(res => {
+ if (res && res.code == 1) {
+ setTimeout(() => {
+ this.giftGiftsGive();
+ }, 1000);
+ } else {
+ uni.showToast({ title: (res && res.msg) ? res.msg : '支付失败', icon: 'none', position: 'top' });
+ }
+ }).catch(() => {
+ uni.showToast({ title: '支付失败', icon: 'none', position: 'top' });
+ });
+ },
+ giftCloseClick() {
+ this.giftStats = false;
+ this.giftForm.nums = 1;
+ this.giftInfoOptions = null;
+ },
+ giftIncreaseNumber() {
+ this.giftForm.nums = parseInt(this.giftForm.nums) || 0;
+ this.giftForm.nums++;
+ },
+ giftDecreaseNumber() {
+ this.giftForm.nums = parseInt(this.giftForm.nums) || 0;
+ if (this.giftForm.nums > 1) {
+ this.giftForm.nums--;
+ }
+ },
// 请求跳舞
requestDance() {
if (!this.dancePrompt || !this.dancePrompt.trim()) {
@@ -933,12 +1677,19 @@
const maxAttempts = 225; // 15分钟左右 (225*4s)
const doPoll = () => {
+ if (!that.singGenerating || that.singGeneratingTaskId !== task_id) {
+ return;
+ }
attempts++;
if (attempts > maxAttempts) {
that.singGenerating = false;
that.singGeneratingTaskId = 0;
uni.setStorageSync('singGenerating', false);
uni.setStorageSync('singGeneratingTaskId', 0);
+ if (that.singPollTimer) {
+ clearTimeout(that.singPollTimer);
+ that.singPollTimer = null;
+ }
uni.showToast({
title: '处理超时,请稍后查看',
icon: 'none',
@@ -977,13 +1728,17 @@
that.singGeneratingTaskId = task_id;
uni.setStorageSync('singGenerating', true);
uni.setStorageSync('singGeneratingTaskId', task_id);
- setTimeout(doPoll, 4000);
+ that.singPollTimer = setTimeout(doPoll, 4000);
}
} else {
that.singGenerating = false;
that.singGeneratingTaskId = 0;
uni.setStorageSync('singGenerating', false);
uni.setStorageSync('singGeneratingTaskId', 0);
+ if (that.singPollTimer) {
+ clearTimeout(that.singPollTimer);
+ that.singPollTimer = null;
+ }
uni.showToast({
title: res.msg,
icon: 'none',
@@ -1010,12 +1765,19 @@
let attempts = 0;
const maxAttempts = 225; // 15分钟左右 (225*4s)
const doPoll = () => {
+ if (!that.danceGenerating || that.danceGeneratingTaskId !== task_id) {
+ return;
+ }
attempts++;
if (attempts > maxAttempts) {
that.danceGenerating = false;
that.danceGeneratingTaskId = 0;
uni.setStorageSync('danceGenerating', false);
uni.setStorageSync('danceGeneratingTaskId', 0);
+ if (that.dancePollTimer) {
+ clearTimeout(that.dancePollTimer);
+ that.dancePollTimer = null;
+ }
uni.showToast({ title: '处理超时,请稍后查看', icon: 'none', duration: 2000 });
return;
}
@@ -1040,13 +1802,17 @@
that.danceGeneratingTaskId = task_id;
uni.setStorageSync('danceGenerating', true);
uni.setStorageSync('danceGeneratingTaskId', task_id);
- setTimeout(doPoll, 4000);
+ that.dancePollTimer = setTimeout(doPoll, 4000);
}
} else {
that.danceGenerating = false;
that.danceGeneratingTaskId = 0;
uni.setStorageSync('danceGenerating', false);
uni.setStorageSync('danceGeneratingTaskId', 0);
+ if (that.dancePollTimer) {
+ clearTimeout(that.dancePollTimer);
+ that.dancePollTimer = null;
+ }
uni.showToast({ title: res.msg, icon: 'none', duration: 2000 });
}
}).catch(() => {
@@ -1054,11 +1820,37 @@
that.danceGeneratingTaskId = 0;
uni.setStorageSync('danceGenerating', false);
uni.setStorageSync('danceGeneratingTaskId', 0);
+ if (that.dancePollTimer) {
+ clearTimeout(that.dancePollTimer);
+ that.dancePollTimer = null;
+ }
uni.showToast({ title: '查询失败,请重试', icon: 'none', duration: 2000 });
});
};
doPoll();
},
+ cancelSingGeneration() {
+ this.singGenerating = false;
+ this.singGeneratingTaskId = 0;
+ uni.setStorageSync('singGenerating', false);
+ uni.setStorageSync('singGeneratingTaskId', 0);
+ if (this.singPollTimer) {
+ clearTimeout(this.singPollTimer);
+ this.singPollTimer = null;
+ }
+ uni.showToast({ title: '已取消生成', icon: 'none', duration: 1500 });
+ },
+ cancelDanceGeneration() {
+ this.danceGenerating = false;
+ this.danceGeneratingTaskId = 0;
+ uni.setStorageSync('danceGenerating', false);
+ uni.setStorageSync('danceGeneratingTaskId', 0);
+ if (this.dancePollTimer) {
+ clearTimeout(this.dancePollTimer);
+ this.dancePollTimer = null;
+ }
+ uni.showToast({ title: '已取消生成', icon: 'none', duration: 1500 });
+ },
// 处理青少年模式状态改变事件
handleUnderAgeStatusChange(status) {
this.underAgeEnabled = status;
@@ -1212,6 +2004,13 @@
console.log('最终结果:', result);
return result;
},
+ calculateProgressPercent() {
+ if (this.getBobbiesList.intimacy == null || this.getBobbiesList.next_level_intimacy == null) {
+ return 0;
+ }
+ const percent = (this.getBobbiesList.intimacy / this.getBobbiesList.next_level_intimacy) * 100;
+ return Math.min(percent, 100);
+ },
togenerate() {
this.loverBasic()
this.loverInit()
@@ -1303,8 +2102,14 @@
const userInfo = uni.getStorageSync('userinfo');
this.chatUserAvatar = userInfo?.avatar || '/static/images/avatar.png';
- // 获取恋人头像
- this.chatLoverAvatar = this.loverBasicList?.image_url || '/static/images/avatar.png';
+ // 获取恋人头像 - 优先使用当前选中的形象,否则使用基本信息中的头像
+ if (this.currentLookImageUrl) {
+ this.chatLoverAvatar = this.currentLookImageUrl;
+ } else if (this.loverBasicList?.image_url) {
+ this.chatLoverAvatar = this.loverBasicList.image_url;
+ } else {
+ this.chatLoverAvatar = '/static/images/avatar.png';
+ }
uni.showLoading({ title: '加载中...' });
@@ -1423,6 +2228,220 @@
this.$nextTick(() => {
this.chatIntoView = 'chat-bottom';
});
+ },
+
+ // ========== 首页形象栏相关方法 ==========
+ // 加载首页形象栏
+ loadHomeLooks() {
+ OutfitList({}).then(res => {
+ if (res && res.code == 1 && res.data) {
+ this.homeLooksList = res.data.looks || [];
+ // 如果有形象栏数据,默认选中第一个
+ if (this.homeLooksList.length > 0) {
+ this.selectedLookId = this.homeLooksList[0].id;
+ this.currentLookImageUrl = this.homeLooksList[0].image_url;
+ // 同时更新聊天头像
+ this.chatLoverAvatar = this.homeLooksList[0].image_url;
+ }
+ }
+ }).catch(err => {
+ console.error('加载形象栏失败:', err);
+ });
+ },
+
+ // 切换形象
+ switchLook(look) {
+ if (!look || !look.id) return;
+
+ // 调用后端 API 切换形象
+ uni.request({
+ url: baseURLPy + '/outfit/looks/use/' + look.id,
+ method: 'POST',
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync("token")
+ },
+ success: (res) => {
+ if (res.data && res.data.code === 1) {
+ this.selectedLookId = look.id;
+ this.currentLookImageUrl = look.image_url;
+
+ // 更新聊天页面的头像
+ this.chatLoverAvatar = look.image_url;
+
+ // 更新女友基本信息(刷新图片)
+ this.loverBasic();
+
+ uni.showToast({
+ title: '形象切换成功',
+ icon: 'success',
+ duration: 1500
+ });
+ } else {
+ uni.showToast({
+ title: res.data.msg || '切换失败',
+ icon: 'none',
+ duration: 1500
+ });
+ }
+ },
+ fail: (err) => {
+ console.error('切换形象失败:', err);
+ uni.showToast({
+ title: '切换失败,请重试',
+ icon: 'none',
+ duration: 1500
+ });
+ }
+ });
+ },
+
+ // ========== 音乐库相关方法 ==========
+ // 切换唱歌Tab
+ switchSingTab(tab) {
+ this.singCurrentTab = tab;
+ if (tab === 'library' && this.musicLibraryList.length === 0) {
+ this.loadMusicLibrary();
+ }
+ },
+
+ // 加载音乐库
+ loadMusicLibrary(page = 1) {
+ uni.request({
+ url: baseURLPy + '/music/library',
+ method: 'GET',
+ data: {
+ page: page,
+ page_size: this.musicLibraryPageSize
+ },
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync("token")
+ },
+ success: (res) => {
+ if (res.data && res.data.code === 1) {
+ const data = res.data.data;
+ if (page === 1) {
+ this.musicLibraryList = data.list || [];
+ } else {
+ this.musicLibraryList = this.musicLibraryList.concat(data.list || []);
+ }
+ this.musicLibraryTotal = data.total || 0;
+ this.musicLibraryPage = page;
+ this.musicLibraryHasMore = this.musicLibraryList.length < this.musicLibraryTotal;
+ }
+ },
+ fail: (err) => {
+ console.error('加载音乐库失败:', err);
+ uni.showToast({ title: '加载失败', icon: 'none' });
+ }
+ });
+ },
+
+ // 加载更多音乐
+ loadMoreMusic() {
+ if (!this.musicLibraryHasMore) return;
+ this.loadMusicLibrary(this.musicLibraryPage + 1);
+ },
+
+ // 显示添加音乐弹窗
+ showAddMusicModal() {
+ this.showMusicModal = true;
+ this.musicForm = {
+ title: '',
+ artist: '',
+ music_url: '',
+ cover_url: '',
+ duration: null
+ };
+ },
+
+ // 关闭添加音乐弹窗
+ closeMusicModal() {
+ this.showMusicModal = false;
+ },
+
+ // 提交音乐
+ submitMusic() {
+ if (!this.musicForm.title || !this.musicForm.music_url) {
+ uni.showToast({ title: '请填写歌曲标题和音乐链接', icon: 'none' });
+ return;
+ }
+
+ uni.showLoading({ title: '提交中...' });
+
+ uni.request({
+ url: baseURLPy + '/music/add-link',
+ method: 'POST',
+ data: this.musicForm,
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync("token"),
+ 'Content-Type': 'application/json'
+ },
+ success: (res) => {
+ uni.hideLoading();
+ if (res.data && res.data.code === 1) {
+ uni.showToast({ title: '添加成功', icon: 'success' });
+ this.closeMusicModal();
+ this.loadMusicLibrary(1); // 刷新列表
+ } else {
+ uni.showToast({ title: res.data.msg || '添加失败', icon: 'none' });
+ }
+ },
+ fail: (err) => {
+ uni.hideLoading();
+ console.error('添加音乐失败:', err);
+ uni.showToast({ title: '添加失败', icon: 'none' });
+ }
+ });
+ },
+
+ // 从音乐库选择歌曲
+ selectMusicFromLibrary(music) {
+ // 记录播放次数
+ uni.request({
+ url: baseURLPy + '/music/' + music.id + '/play',
+ method: 'POST',
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync("token")
+ }
+ });
+
+ // 使用音乐库的歌曲生成视频
+ if (this.singGenerating) {
+ uni.showToast({ title: '视频生成中,请稍候...', icon: 'none', duration: 2000 });
+ return;
+ }
+
+ // 这里需要调用唱歌API,传入音乐库的音乐URL
+ uni.showModal({
+ title: '提示',
+ content: `确定让她唱《${music.title}》吗?`,
+ success: (modalRes) => {
+ if (modalRes.confirm) {
+ // TODO: 调用唱歌API,使用 music.music_url
+ uni.showToast({ title: '功能开发中...', icon: 'none' });
+ }
+ }
+ });
+ },
+
+ // 切换音乐点赞
+ toggleMusicLike(music) {
+ uni.request({
+ url: baseURLPy + '/music/' + music.id + '/like',
+ method: 'POST',
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync("token")
+ },
+ success: (res) => {
+ if (res.data && res.data.code === 1) {
+ music.is_liked = res.data.data.is_liked;
+ music.like_count = res.data.data.like_count;
+ }
+ },
+ fail: (err) => {
+ console.error('点赞失败:', err);
+ }
+ });
}
}
}
@@ -1433,6 +2452,561 @@
}
+
+
diff --git a/开发/2026年2月3日/2026年2月3日.md b/开发/2026年2月3日/2026年2月3日.md
new file mode 100644
index 0000000..35ec6d3
--- /dev/null
+++ b/开发/2026年2月3日/2026年2月3日.md
@@ -0,0 +1,2 @@
+1. 将所有tab栏的功能接上并且将界面美化
+2. 增加音乐库功能
\ No newline at end of file
diff --git a/开发/2026年2月3日/音乐库.sql b/开发/2026年2月3日/音乐库.sql
new file mode 100644
index 0000000..b73a0a3
--- /dev/null
+++ b/开发/2026年2月3日/音乐库.sql
@@ -0,0 +1,34 @@
+-- 音乐库表
+CREATE TABLE IF NOT EXISTS `nf_music_library` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `user_id` bigint(20) NOT NULL COMMENT '上传用户ID',
+ `title` varchar(255) NOT NULL COMMENT '歌曲标题',
+ `artist` varchar(255) DEFAULT NULL COMMENT '艺术家',
+ `music_url` varchar(500) NOT NULL COMMENT '音乐文件URL或链接',
+ `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图URL',
+ `duration` int(11) DEFAULT NULL COMMENT '时长(秒)',
+ `upload_type` enum('file','link') NOT NULL DEFAULT 'link' COMMENT '上传类型:file=文件上传,link=链接',
+ `is_public` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否公开:1=公开,0=私有',
+ `play_count` int(11) NOT NULL DEFAULT 0 COMMENT '播放次数',
+ `like_count` int(11) NOT NULL DEFAULT 0 COMMENT '点赞次数',
+ `status` enum('pending','approved','rejected') NOT NULL DEFAULT 'approved' COMMENT '审核状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_is_public` (`is_public`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐库表';
+
+-- 用户音乐点赞表
+CREATE TABLE IF NOT EXISTS `nf_music_likes` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `user_id` bigint(20) NOT NULL COMMENT '用户ID',
+ `music_id` bigint(20) NOT NULL COMMENT '音乐ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_user_music` (`user_id`, `music_id`),
+ KEY `idx_music_id` (`music_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户音乐点赞表';