feat:交接
This commit is contained in:
45
AGENTS.md
Normal file
45
AGENTS.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Source: `src/` (entry `src/main.js`, app shell `src/App.vue`).
|
||||
- Pages: `src/pages/**` with Uni-App routing in `src/pages.json` (e.g., `pages/index/index.vue`).
|
||||
- UI Components: `src/components/` (reusable, app-agnostic).
|
||||
- State: `src/stores/` (Pinia stores).
|
||||
- Utilities: `src/utils/`; Styles: `src/styles/`, global `src/uni.scss`.
|
||||
- Assets: `src/static/`.
|
||||
- Build output: `dist/`; temp and logs: `temp/`, `logs/`.
|
||||
- Tooling: `vite.config.js`, `manifest.json`, `index.html`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Install: `npm install`
|
||||
- Dev (H5): `npm run dev:h5` — start Vite + Uni dev server.
|
||||
- Dev (Weixin Mini Program): `npm run dev:mp-weixin`
|
||||
- Build (H5): `npm run build:h5`
|
||||
- Build (Weixin Mini Program): `npm run build:mp-weixin`
|
||||
- Custom target: `npm run dev:custom -- -p <platform>` (e.g., `mp-alipay`).
|
||||
- Output is written to `dist/`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Indentation: 2 spaces; trailing spaces trimmed.
|
||||
- Vue 3 SFCs: prefer `<script setup>` for new code; keep existing style consistent nearby.
|
||||
- Components: PascalCase filenames in `src/components/` (e.g., `UserCard.vue`).
|
||||
- Pages: lowercase directory + `index.vue` (e.g., `pages/chat/index.vue`).
|
||||
- JS naming: camelCase for variables/functions; PascalCase for classes; SCSS/CSS files kebab-case.
|
||||
- State: one Pinia store per domain in `src/stores/` (e.g., `useUserStore`).
|
||||
|
||||
## Testing Guidelines
|
||||
- Automated tests are not configured. If adding, use `@dcloudio/uni-automator` and place specs under `tests/**/*.spec.{js,ts}`.
|
||||
- For changes, provide a manual test plan covering H5 and at least one mini-program target (e.g., Weixin).
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: follow Conventional Commits where possible (e.g., `feat: add chat history panel`, `fix: debounce voice upload`).
|
||||
- PRs must include: purpose/summary, linked issues, screenshots or screen recordings for UI, test plan (platforms, steps), and any config changes.
|
||||
- Keep diffs focused; avoid noisy refactors in feature PRs.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Do not commit secrets; prefer environment config via Vite (`import.meta.env`) and platform-specific secure storage.
|
||||
- `logs/` and `temp/` are ephemeral; keep them out of commits.
|
||||
|
||||
## Agent-Specific Instructions
|
||||
- Respect structure and naming above; avoid file moves unless requested.
|
||||
- Scope changes to the smallest relevant area; update docs when paths or commands change.
|
||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 项目说明
|
||||
|
||||
本项目是基于 Uni-App(Vue 3 + Vite)的多端前端应用,支持 H5 与多类小程序目标(如微信小程序)。项目采用 Pinia 管理状态,按“页面/组件/状态/工具”模块化组织,便于扩展与维护。
|
||||
|
||||
## 架构概览
|
||||
- 技术栈:Vue 3、Pinia、Vite 5、Uni-App 3.x、vue-i18n。
|
||||
- 路由与页面:通过 `src/pages.json` 定义多端路由和 TabBar,页面文件位于 `src/pages/**`。
|
||||
- 状态管理:`src/stores/` 按领域拆分 Store(如 `useUserStore`)。
|
||||
- 样式与资源:全局样式 `src/uni.scss`,通用样式在 `src/styles/`,静态资源在 `src/static/`。
|
||||
- 关键入口:`src/main.js`(应用创建)、`src/App.vue`(全局生命周期与壳层)。
|
||||
- 构建配置:`vite.config.js`(插件/构建)、`manifest.json`(应用与平台配置)。
|
||||
|
||||
目录结构示例:
|
||||
```
|
||||
src/
|
||||
pages/ ... # 业务页面(splash, index, mine, chat, recharge 等)
|
||||
components/ ... # 复用组件
|
||||
stores/ ... # Pinia stores
|
||||
utils/ ... # 工具函数
|
||||
styles/ ... # 通用样式
|
||||
App.vue, main.js, pages.json, uni.scss
|
||||
```
|
||||
|
||||
## 主要功能
|
||||
- 启动与协议:`pages/splash/splash`、`pages/agreement/agreement`。
|
||||
- 首页与我的:`pages/index/index`、`pages/mine/mine`。
|
||||
- 创作与脚本编辑:`pages/create/create`、`pages/script/editor`。
|
||||
- 语音与聊天:`pages/voice/clone`、`pages/chat/chat`。
|
||||
- 充值与记录:`pages/recharge/recharge`、`pages/recharge/history`。
|
||||
|
||||
## 本地开发与构建
|
||||
- 安装依赖:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
- 开发调试(H5):
|
||||
```
|
||||
npm run dev:h5
|
||||
```
|
||||
- 开发调试(微信小程序):
|
||||
```
|
||||
npm run dev:mp-weixin
|
||||
```
|
||||
- 生产构建(H5/微信小程序):
|
||||
```
|
||||
npm run build:h5
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
构建产物输出到 `dist/`。
|
||||
|
||||
## 配置与环境
|
||||
- 平台与路由:`src/pages.json`;应用配置:`src/manifest.json`。
|
||||
- 环境变量:使用 `import.meta.env`(Vite),避免提交敏感信息。
|
||||
|
||||
如需更详细的接口与需求,可参考仓库中的 `API接口文档.md` 与 `产品设计需求文档.md`。
|
||||
1
logs/nginx.pid
Normal file
1
logs/nginx.pid
Normal file
@@ -0,0 +1 @@
|
||||
34608
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appid": "wxff56c34ef9aceb62",
|
||||
"appid": "wx7f35c6442d0694a9",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.8.10",
|
||||
"packOptions": {
|
||||
@@ -18,11 +18,24 @@
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"compileWorklet": false,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"packNpmManually": false,
|
||||
"minifyWXSS": true,
|
||||
"minifyWXML": true,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"useCompilerPlugins": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 4
|
||||
}
|
||||
},
|
||||
"simulatorPluginLibVersion": {}
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "webUI",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "webUI",
|
||||
"setting": {
|
||||
"compileHotReLoad": true,
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"useApiHook": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false
|
||||
},
|
||||
"libVersion": "3.8.10",
|
||||
"condition": {}
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false,
|
||||
"disableScroll": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35,6 +37,12 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/drama/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/voice/clone",
|
||||
"style": {
|
||||
@@ -56,6 +64,19 @@
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recharge/recharge",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recharge/history",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
@@ -65,14 +86,13 @@
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#ff9800",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"color": "rgba(255, 255, 255, 0.5)",
|
||||
"selectedColor": "#f9e076",
|
||||
"backgroundColor": "rgba(26, 11, 46, 0.95)",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,24 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="floral-container">
|
||||
<!-- 夜空装饰背景 -->
|
||||
<view class="night-sky-decoration">
|
||||
<!-- 星星装饰 - 小黄点 -->
|
||||
<view class="star star-1"></view>
|
||||
<view class="star star-2"></view>
|
||||
<view class="star star-3"></view>
|
||||
<view class="star star-4"></view>
|
||||
<view class="star star-5"></view>
|
||||
<view class="star star-6"></view>
|
||||
<view class="star star-7"></view>
|
||||
<view class="star star-8"></view>
|
||||
<view class="star star-9"></view>
|
||||
<view class="star star-10"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-navbar fixed-navbar">
|
||||
<view class="navbar-left" @click="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
</view>
|
||||
<view class="navbar-left" @tap="goBack">← 返回</view>
|
||||
<text class="navbar-title">创建</text>
|
||||
<view class="navbar-right"></view>
|
||||
</view>
|
||||
|
||||
<!-- 创建选项区域 -->
|
||||
@@ -85,71 +97,208 @@ const showLoginTip = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 夜空主题容器 */
|
||||
.floral-container {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 120rpx;
|
||||
padding-top: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fixed-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
}
|
||||
.container {
|
||||
background: #fff9f2;
|
||||
min-height: 100vh;
|
||||
padding-top: 100rpx;
|
||||
padding-bottom: 40rpx;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
.navbar-left, .navbar-right {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.back-icon {
|
||||
font-size: 36rpx;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.95) 0%, rgba(74, 30, 109, 0.95) 25%, rgba(107, 44, 156, 0.95) 50%, rgba(138, 43, 226, 0.95) 75%, rgba(75, 0, 130, 0.95) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.navbar-title {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 38rpx;
|
||||
font-size: 32rpx;
|
||||
letter-spacing: 2rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
color: #f9e076;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #ffffff;
|
||||
text-shadow: 0 0 10px rgba(249, 224, 118, 0.5);
|
||||
}
|
||||
|
||||
/* 创建选项区域 */
|
||||
.create-options {
|
||||
padding: 40rpx 30rpx;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
border-radius: 25rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(138, 43, 226, 0.15);
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 12rpx 40rpx rgba(138, 43, 226, 0.2);
|
||||
border-color: #8a2be2;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(138, 43, 226, 0.3));
|
||||
}
|
||||
|
||||
.option-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 夜空装饰背景 */
|
||||
.night-sky-decoration {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 星星样式 - 小黄点 */
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
animation: star-twinkle 3s ease-in-out infinite;
|
||||
box-shadow: 0 0 8px rgba(249, 224, 118, 0.8);
|
||||
}
|
||||
|
||||
.star-1 {
|
||||
top: 80px;
|
||||
right: 40px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.star-2 {
|
||||
top: 120px;
|
||||
right: 80px;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.star-3 {
|
||||
top: 200px;
|
||||
right: 20px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.star-4 {
|
||||
top: 300px;
|
||||
right: 60px;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.star-5 {
|
||||
top: 150px;
|
||||
left: 30px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.star-6 {
|
||||
top: 250px;
|
||||
left: 60px;
|
||||
animation-delay: 2.5s;
|
||||
}
|
||||
|
||||
.star-7 {
|
||||
top: 350px;
|
||||
left: 20px;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.star-8 {
|
||||
top: 400px;
|
||||
right: 100px;
|
||||
animation-delay: 3.5s;
|
||||
}
|
||||
|
||||
.star-9 {
|
||||
top: 60px;
|
||||
left: 80px;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.star-10 {
|
||||
top: 180px;
|
||||
right: 120px;
|
||||
animation-delay: 4.5s;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes star-twinkle {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
box-shadow: 0 0 4px rgba(249, 224, 118, 0.4);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 8px rgba(249, 224, 118, 0.7);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 12px rgba(249, 224, 118, 1);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 8px rgba(249, 224, 118, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
387
src/pages/drama/index.vue
Normal file
387
src/pages/drama/index.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<view class="floral-container">
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-navbar fixed-navbar">
|
||||
<view class="navbar-left" @tap="goBack">← 返回</view>
|
||||
<text class="navbar-title">选择剧情</text>
|
||||
</view>
|
||||
|
||||
<!-- 可滚动的两列卡片区 -->
|
||||
<scroll-view
|
||||
class="scroll-container"
|
||||
scroll-y="true"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="two-column-grid">
|
||||
<view class="column column-left">
|
||||
<block v-for="(item, idx) in leftColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
<view class="card-bottom">
|
||||
<button v-if="userStore.isLoggedIn" class="floral-btn use-btn" @click="showDetail(item)">🔍 查看详情</button>
|
||||
<button v-else class="floral-btn outline use-btn login-required" @click="showLoginTip">🔐 查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<view class="column column-right">
|
||||
<block v-for="(item, idx) in rightColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
<view class="card-bottom">
|
||||
<button v-if="userStore.isLoggedIn" class="floral-btn use-btn" @click="showDetail(item)">🔍 查看详情</button>
|
||||
<button v-else class="floral-btn outline use-btn login-required" @click="showLoginTip">🔐 查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacing"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 登录提示弹窗 -->
|
||||
<view class="floral-modal" v-if="showLoginModal">
|
||||
<view class="floral-modal-content">
|
||||
<view class="modal-title">💝 温馨提示</view>
|
||||
<view class="modal-text">请先登录后再开始您的创意之旅</view>
|
||||
<view class="modal-btns">
|
||||
<button class="floral-btn outline modal-btn cancel" @click="showLoginModal = false">稍后再说</button>
|
||||
<button class="floral-btn modal-btn confirm" @click="goToLogin">🌸 去登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 角色详情弹窗 -->
|
||||
<view class="detail-modal" v-if="showDetailModal" @click="closeDetail">
|
||||
<view class="detail-modal-content" @click.stop>
|
||||
<view class="detail-cover-container">
|
||||
<image
|
||||
class="detail-cover"
|
||||
:src="selectedItem?.cover"
|
||||
mode="aspectFit"
|
||||
:class="{'cover-zoomed': showDetailModal}"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="detail-info" :class="{'info-visible': showDetailModal}">
|
||||
<view class="detail-tag">{{ selectedItem?.tag }}</view>
|
||||
<view class="detail-title">{{ selectedItem?.title }}</view>
|
||||
<scroll-view class="detail-description" scroll-y="true">
|
||||
<view class="description-text">
|
||||
{{ selectedItem?.title }} - 这是一个充满魅力的角色,拥有独特的个性和丰富的故事背景。在这里,您可以与这个角色进行深入的对话,体验不同的情感交流。
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="detail-actions">
|
||||
<button class="floral-btn outline detail-btn cancel" @click="closeDetail">取消</button>
|
||||
<button class="floral-btn detail-btn confirm" @click="useFromDetail">💝 去使用</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const showLoginModal = ref(false);
|
||||
|
||||
// 详情弹窗相关
|
||||
const showDetailModal = ref(false);
|
||||
const selectedItem = ref(null);
|
||||
|
||||
// 数据:剧情列表
|
||||
const dramaList = ref([
|
||||
{
|
||||
id: 1,
|
||||
cover: '/static/bailing.jpg',
|
||||
tag: '浪漫',
|
||||
title: '白领女友的温柔早安',
|
||||
roleId: 1,
|
||||
roleName: '小何',
|
||||
roleDesc: '台湾女孩,高情商智能助手',
|
||||
avatar: '/static/bailing.jpg',
|
||||
greeting: '你好~我是小何,一个高情商的台湾女孩,很高兴认识你!有什么想聊的吗?'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cover: '/static/haomen.jpg',
|
||||
tag: '优雅',
|
||||
title: '豪门女主的秘密生活',
|
||||
roleId: 2,
|
||||
roleName: '小于',
|
||||
roleDesc: '咖啡店兼职店员,热情友好',
|
||||
avatar: '/static/haomen.jpg',
|
||||
greeting: '你好!我是小于,咖啡店的新店员,今天想喝点什么吗?'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cover: '/static/waimai.jpg',
|
||||
tag: '活泼',
|
||||
title: '外卖小姐姐的贴心问候',
|
||||
roleId: 3,
|
||||
roleName: '李老师',
|
||||
roleDesc: '中学图书馆管理员,温文尔雅',
|
||||
avatar: '/static/waimai.jpg',
|
||||
greeting: '你好,我是李老师,图书馆管理员。有什么想了解的书吗?'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cover: '/static/tunvlang.jpg',
|
||||
tag: '深情',
|
||||
title: '因爱执著的少女',
|
||||
roleId: 4,
|
||||
roleName: '小何',
|
||||
roleDesc: '另一个小何角色',
|
||||
avatar: '/static/tunvlang.jpg',
|
||||
greeting: '你好~我是小何,很高兴为你服务!'
|
||||
},
|
||||
// {
|
||||
// id: 5,
|
||||
// cover: '/static/logo.png',
|
||||
// tag: '温暖',
|
||||
// title: '邻家女孩的暖心故事',
|
||||
// roleId: 5,
|
||||
// roleName: '温衡',
|
||||
// roleDesc: '职场女上司,成熟强势',
|
||||
// avatar: '/static/logo.png',
|
||||
// greeting: '我是温衡,你的上司。今天的工作完成了吗?'
|
||||
// },
|
||||
// {
|
||||
// id: 6,
|
||||
// cover: '/static/logo.png',
|
||||
// tag: '梦幻',
|
||||
// title: '公主的浪漫邂逅',
|
||||
// roleId: 6,
|
||||
// roleName: '职场上司',
|
||||
// roleDesc: '另一个温衡角色',
|
||||
// avatar: '/static/logo.png',
|
||||
// greeting: '我是你的上司,有什么需要汇报的吗?'
|
||||
// },
|
||||
// { id: 7, cover: '/static/logo.png', tag: '治愈', title: '咖啡店的午后时光' },
|
||||
// { id: 8, cover: '/static/logo.png', tag: '青春', title: '校园里的美好回忆' },
|
||||
]);
|
||||
|
||||
// 列数据拆分
|
||||
const leftColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 0));
|
||||
const rightColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 1));
|
||||
|
||||
onMounted(() => {
|
||||
userStore.init();
|
||||
});
|
||||
|
||||
// 方法
|
||||
const handleUse = (item) => {
|
||||
if (!item || !item.id) {
|
||||
uni.showToast({ title: '角色信息无效', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '正在设置角色...' });
|
||||
if (item.roleId) {
|
||||
uni.hideLoading();
|
||||
const params = {
|
||||
characterId: item.id,
|
||||
roleId: item.roleId,
|
||||
roleName: item.roleName || item.title,
|
||||
roleDesc: item.roleDesc,
|
||||
avatar: item.avatar || item.cover,
|
||||
greeting: item.greeting
|
||||
};
|
||||
const queryString = Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key] || '')}`).join('&');
|
||||
uni.navigateTo({ url: `/pages/chat/chat?${queryString}` });
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.navigateTo({ url: `/pages/chat/chat?characterId=${item.id}` });
|
||||
}
|
||||
};
|
||||
|
||||
const showLoginTip = () => { showLoginModal.value = true; };
|
||||
const showDetail = (item) => { selectedItem.value = item; showDetailModal.value = true; };
|
||||
const closeDetail = () => { showDetailModal.value = false; selectedItem.value = null; };
|
||||
const useFromDetail = () => { if (selectedItem.value) { const v = selectedItem.value; closeDetail(); handleUse(v); } };
|
||||
const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/pages/mine/mine' }); };
|
||||
const goBack = () => { uni.navigateBack(); };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floral-container {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 120rpx;
|
||||
padding-top: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fixed-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.95) 0%, rgba(74, 30, 109, 0.95) 25%, rgba(107, 44, 156, 0.95) 50%, rgba(138, 43, 226, 0.95) 75%, rgba(75, 0, 130, 0.95) 100%);
|
||||
border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
color: #f9e076;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.two-column-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.floral-grid-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
border-radius: 25rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(138, 43, 226, 0.15);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.floral-tag {
|
||||
position: absolute;
|
||||
margin: 12rpx;
|
||||
background: linear-gradient(135deg, #f9e076, #f59e0b);
|
||||
color: #4b0082;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.content-area { padding: 16rpx; }
|
||||
.title { font-size: 28rpx; color: #333; font-weight: bold; }
|
||||
.card-bottom { margin-top: 8rpx; display: flex; justify-content: flex-end; }
|
||||
|
||||
.floral-btn {
|
||||
background: linear-gradient(135deg, #8a2be2, #6b2c9c);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
box-shadow: 0 4rpx 15rpx rgba(138, 43, 226, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floral-btn.outline {
|
||||
background: transparent;
|
||||
color: #8a2be2;
|
||||
border: 2rpx solid #8a2be2;
|
||||
}
|
||||
|
||||
.floral-grid-card:hover {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 12rpx 40rpx rgba(138, 43, 226, 0.2);
|
||||
border-color: #8a2be2;
|
||||
}
|
||||
|
||||
.use-btn { font-size: 24rpx; }
|
||||
.bottom-spacing { height: 40rpx; }
|
||||
|
||||
/* 登录弹窗 */
|
||||
.floral-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.floral-modal-content {
|
||||
width: 80vw;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.modal-title { font-size: 30rpx; font-weight: bold; margin-bottom: 12rpx; }
|
||||
.modal-text { font-size: 24rpx; color: #666; margin-bottom: 16rpx; }
|
||||
.modal-btns { display: flex; gap: 20rpx; justify-content: flex-end; }
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.detail-modal-content {
|
||||
width: 86vw;
|
||||
max-height: 80vh;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.detail-cover-container { width: 100%; height: 320rpx; overflow: hidden; }
|
||||
.detail-cover { width: 100%; height: 100%; }
|
||||
.detail-info { padding: 20rpx; }
|
||||
.detail-tag { font-size: 22rpx; color: #8a2be2; margin-bottom: 8rpx; }
|
||||
.detail-title { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 12rpx; }
|
||||
.detail-description { max-height: 200rpx; }
|
||||
.description-text { font-size: 24rpx; color: #555; line-height: 1.6; }
|
||||
.detail-actions { margin-top: 12rpx; display: flex; justify-content: flex-end; gap: 20rpx; }
|
||||
.detail-btn { padding: 10rpx 22rpx; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,100 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 头部以上背景区域 -->
|
||||
<view class="top-background"></view>
|
||||
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-navbar fixed-navbar">
|
||||
<text class="navbar-title">我的</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info">
|
||||
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
|
||||
<view class="user-details" v-if="isLoggedIn">
|
||||
<text class="login-text">{{ nickName }}</text>
|
||||
<text class="user-id">ID: {{ openid }}</text>
|
||||
</view>
|
||||
<view class="user-details" v-else>
|
||||
<text class="login-text">请登录</text>
|
||||
<text class="user-id">ID:</text>
|
||||
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单卡片 -->
|
||||
<view class="menu-cards">
|
||||
<!-- 用户须知 -->
|
||||
<view class="menu-card" @click="showAgreement">
|
||||
<view class="card-icon notice-icon">🔊</view>
|
||||
<text class="card-title">用户须知</text>
|
||||
<!-- 可滚动内容区域 -->
|
||||
<scroll-view
|
||||
class="scroll-container"
|
||||
scroll-y="true"
|
||||
enable-back-to-top="false"
|
||||
refresher-enabled="false"
|
||||
:refresher-triggered="false"
|
||||
:refresher-threshold="0"
|
||||
:show-scrollbar="false"
|
||||
:scroll-with-animation="false"
|
||||
:bounces="false"
|
||||
:always-bounce-vertical="false"
|
||||
:scroll-top="0"
|
||||
:upper-threshold="0"
|
||||
:lower-threshold="0"
|
||||
>
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info">
|
||||
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
|
||||
<view class="user-details" v-if="isLoggedIn">
|
||||
<text class="login-text">{{ nickName }}</text>
|
||||
<text class="user-id">ID: {{ openid }}</text>
|
||||
</view>
|
||||
<view class="user-details" v-else>
|
||||
<text class="login-text">请登录</text>
|
||||
<text class="user-id">ID:</text>
|
||||
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户设备 -->
|
||||
<view class="menu-card">
|
||||
<view class="card-icon device-icon">📦</view>
|
||||
<text class="card-title">用户设备</text>
|
||||
<!-- 余额显示 -->
|
||||
<view class="balance-section" v-if="isLoggedIn">
|
||||
<view class="balance-card">
|
||||
<view class="balance-info">
|
||||
<view class="balance-label">账户余额</view>
|
||||
<view class="balance-amount">¥{{ userBalance }}</view>
|
||||
</view>
|
||||
<view class="balance-actions">
|
||||
<button class="recharge-btn" @click="goToRecharge">充值</button>
|
||||
<button class="history-btn" @click="goToHistory">记录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 剧情角色区块 -->
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">剧情角色</text>
|
||||
<text class="view-all">查看全部</text>
|
||||
|
||||
<!-- 菜单卡片 -->
|
||||
<view class="menu-cards">
|
||||
<!-- 用户须知 -->
|
||||
<view class="menu-card" @click="showAgreement">
|
||||
<view class="card-icon notice-icon">🔊</view>
|
||||
<text class="card-title">用户须知</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户设备 -->
|
||||
<view class="menu-card">
|
||||
<view class="card-icon device-icon">📦</view>
|
||||
<text class="card-title">用户设备</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 克隆声音区块 -->
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">克隆声音</text>
|
||||
<text class="view-all">查看全部</text>
|
||||
|
||||
<!-- 剧情角色区块 -->
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">剧情角色</text>
|
||||
<text class="view-all">查看全部</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮,仅登录后显示 -->
|
||||
<view class="logout-section">
|
||||
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||
<button class="logout-btn" style="margin-top: 20rpx; background-color: #ff9800; color: white;" @click="testDirectApiCall">直接测试API</button>
|
||||
<text style="display: block; margin-top: 20rpx; text-align: center; color: #999;">
|
||||
登录状态: {{ isLoggedIn ? '已登录' : '未登录' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 克隆声音区块 -->
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">克隆声音</text>
|
||||
<text class="view-all">查看全部</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮,仅登录后显示 -->
|
||||
<view class="logout-section">
|
||||
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||
<button class="logout-btn" style="margin-top: 20rpx; background-color: #ff9800; color: white;" @click="testDirectApiCall">直接测试API</button>
|
||||
<text style="display: block; margin-top: 20rpx; text-align: center; color: #999;">
|
||||
登录状态: {{ isLoggedIn ? '已登录' : '未登录' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白,避免被tabbar遮挡 -->
|
||||
<view class="bottom-spacing"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 用户须知弹窗 -->
|
||||
<user-agreement
|
||||
@@ -71,6 +108,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
import { rechargeAPI } from '@/utils/api.js';
|
||||
import UserAgreement from '@/components/UserAgreement.vue';
|
||||
|
||||
// 状态管理 - 避免直接依赖store的响应式
|
||||
@@ -82,6 +120,7 @@ const isLoggedIn = ref(false);
|
||||
const nickName = ref('');
|
||||
const openid = ref('');
|
||||
const avatarUrl = ref('/static/default-avatar.png');
|
||||
const userBalance = ref(0.00);
|
||||
|
||||
// 登录按钮文本
|
||||
const loginButtonText = computed(() => {
|
||||
@@ -128,15 +167,13 @@ onMounted(() => {
|
||||
userStore.init();
|
||||
// 初始化本地状态
|
||||
initUserInfo();
|
||||
// 加载用户余额
|
||||
loadUserBalance();
|
||||
});
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
// 先清理假登录数据
|
||||
console.log('开始登录,先清理假登录数据...');
|
||||
userStore.clearFakeLoginData();
|
||||
|
||||
uni.showLoading({
|
||||
title: '登录中...'
|
||||
});
|
||||
@@ -156,18 +193,11 @@ const handleLogin = async () => {
|
||||
uni.hideLoading();
|
||||
console.error('Login failed:', error);
|
||||
|
||||
let errorMessage = '登录失败';
|
||||
if (error.message) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
}
|
||||
|
||||
// 显示详细的错误信息
|
||||
uni.showModal({
|
||||
// 登录失败
|
||||
uni.showToast({
|
||||
title: '登录失败',
|
||||
content: `错误信息:${errorMessage}\n\n请检查:\n1. 网络连接是否正常\n2. 后端服务是否运行\n3. 微信小程序配置是否正确`,
|
||||
showCancel: false
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -339,7 +369,7 @@ const testDirectApiCall = () => {
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
uni.request({
|
||||
url: 'http://8.145.52.111:8084/app/logout',
|
||||
url: 'https://www.aixsy.com.cn/app/logout',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -376,11 +406,11 @@ Token: ${currentToken || '无'}
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
uni.request({
|
||||
url: 'http://8.145.52.111:8084/app/logout',
|
||||
url: 'https://www.aixsy.com.cn/app/logout',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': currentToken || ''
|
||||
'Authorization': currentToken ? (currentToken.startsWith('Bearer ') ? currentToken : 'Bearer ' + currentToken) : ''
|
||||
},
|
||||
complete: (res) => {
|
||||
uni.showModal({
|
||||
@@ -399,7 +429,7 @@ Token: ${currentToken || '无'}
|
||||
content: `尝试带token参数的URL`,
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
const url = `http://8.145.52.111:8084/app/logout?token=${currentToken || ''}`;
|
||||
const url = `https://www.aixsy.com.cn/app/logout?token=${currentToken || ''}`;
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -423,34 +453,86 @@ Token: ${currentToken || '无'}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 加载用户余额
|
||||
const loadUserBalance = async () => {
|
||||
if (!isLoggedIn.value) return;
|
||||
|
||||
try {
|
||||
const result = await rechargeAPI.getUserBalance();
|
||||
if (result.success) {
|
||||
userBalance.value = result.data.balance || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户余额失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到充值页面
|
||||
const goToRecharge = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/recharge/recharge'
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到充值记录页面
|
||||
const goToHistory = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/recharge/history'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fixed-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 100rpx;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
}
|
||||
.container {
|
||||
background: #fff9f2;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
min-height: 100vh;
|
||||
padding-top: 100rpx;
|
||||
padding-top: 200rpx;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 滚动容器样式 */
|
||||
.scroll-container {
|
||||
height: calc(100vh - 200rpx);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 头部以上背景区域 */
|
||||
.top-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
z-index: 99;
|
||||
}
|
||||
.custom-navbar {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff9f2;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.95) 0%, rgba(74, 30, 109, 0.95) 25%, rgba(107, 44, 156, 0.95) 50%, rgba(138, 43, 226, 0.95) 75%, rgba(75, 0, 130, 0.95) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
|
||||
font-weight: bold;
|
||||
font-size: 38rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.navbar-title {
|
||||
color: #333;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 0 10px rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
/* 用户信息区域 */
|
||||
@@ -458,7 +540,55 @@ Token: ${currentToken || '无'}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
background: #fff9f2;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 余额区域 */
|
||||
.balance-section {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: linear-gradient(135deg, #8a2be2 0%, #6b2c9c 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 30rpx rgba(138, 43, 226, 0.3);
|
||||
}
|
||||
|
||||
.balance-info .balance-label {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.balance-info .balance-amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.balance-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.balance-actions .recharge-btn,
|
||||
.balance-actions .history-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 30rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.balance-actions .recharge-btn {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.avatar {
|
||||
width: 140rpx;
|
||||
@@ -501,15 +631,18 @@ Token: ${currentToken || '无'}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
box-shadow: 0 6rpx 25rpx rgba(138, 43, 226, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ff9800;
|
||||
color: #8a2be2;
|
||||
margin-right: 24rpx;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(138, 43, 226, 0.3));
|
||||
}
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
@@ -518,11 +651,14 @@ Token: ${currentToken || '无'}
|
||||
|
||||
/* 区块样式 */
|
||||
.section-block {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
border-radius: 20rpx;
|
||||
margin: 24rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 6rpx 25rpx rgba(138, 43, 226, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
@@ -544,9 +680,17 @@ Token: ${currentToken || '无'}
|
||||
}
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #8a2be2;
|
||||
font-size: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 15rpx rgba(138, 43, 226, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* 底部留白 */
|
||||
.bottom-spacing {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
520
src/pages/recharge/history.vue
Normal file
520
src/pages/recharge/history.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<view class="history-container">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<text class="nav-icon">‹</text>
|
||||
</view>
|
||||
<view class="nav-title">充值记录</view>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="(tab, index) in filterTabs"
|
||||
:key="index"
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === tab.value }"
|
||||
@click="selectFilter(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<view class="record-list">
|
||||
<view v-if="isLoading && recordList.length === 0" class="loading-state">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="recordList.length === 0" class="empty-state">
|
||||
<view class="empty-icon">📝</view>
|
||||
<text class="empty-text">暂无充值记录</text>
|
||||
<button class="recharge-btn" @click="goToRecharge">立即充值</button>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="(record, index) in recordList"
|
||||
:key="index"
|
||||
class="record-item"
|
||||
>
|
||||
<view class="record-header">
|
||||
<view class="record-amount">
|
||||
<text class="amount-symbol">+</text>
|
||||
<text class="amount-value">¥{{ record.amount }}</text>
|
||||
</view>
|
||||
<view class="record-status" :class="record.status">
|
||||
{{ getStatusText(record.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">订单号:</text>
|
||||
<text class="detail-value">{{ record.orderId }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">支付方式:</text>
|
||||
<text class="detail-value">{{ getPaymentMethodText(record.paymentMethod) }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">充值时间:</text>
|
||||
<text class="detail-value">{{ formatTime(record.createTime) }}</text>
|
||||
</view>
|
||||
<view class="detail-item" v-if="record.bonusAmount > 0">
|
||||
<text class="detail-label">赠送金额:</text>
|
||||
<text class="detail-value bonus">+¥{{ record.bonusAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="hasMore && !isLoading" class="load-more" @click="loadMore">
|
||||
<text class="load-more-text">加载更多</text>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoading && recordList.length > 0" class="loading-more">
|
||||
<view class="loading-spinner small"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { rechargeAPI } from '@/utils/api.js'
|
||||
|
||||
export default {
|
||||
name: 'RechargeHistoryPage',
|
||||
data() {
|
||||
return {
|
||||
recordList: [], // 充值记录列表
|
||||
currentFilter: 'all', // 当前筛选条件
|
||||
isLoading: false, // 是否正在加载
|
||||
hasMore: true, // 是否还有更多数据
|
||||
currentPage: 1, // 当前页码
|
||||
pageSize: 10, // 每页数量
|
||||
|
||||
// 筛选选项
|
||||
filterTabs: [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '处理中', value: 'pending' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecordList()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.hasMore && !this.isLoading) {
|
||||
this.loadMore()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
// 跳转到充值页面
|
||||
goToRecharge() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/recharge/recharge'
|
||||
})
|
||||
},
|
||||
|
||||
// 选择筛选条件
|
||||
selectFilter(filter) {
|
||||
if (this.currentFilter === filter) return
|
||||
|
||||
this.currentFilter = filter
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
// 刷新数据
|
||||
async refreshData() {
|
||||
this.currentPage = 1
|
||||
this.hasMore = true
|
||||
this.recordList = []
|
||||
await this.loadRecordList()
|
||||
|
||||
// 停止下拉刷新
|
||||
uni.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
// 加载更多
|
||||
async loadMore() {
|
||||
if (!this.hasMore || this.isLoading) return
|
||||
|
||||
this.currentPage++
|
||||
await this.loadRecordList()
|
||||
},
|
||||
|
||||
// 加载充值记录列表
|
||||
async loadRecordList() {
|
||||
if (this.isLoading) return
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
}
|
||||
|
||||
// 添加状态筛选
|
||||
if (this.currentFilter !== 'all') {
|
||||
params.status = this.currentFilter
|
||||
}
|
||||
|
||||
const result = await rechargeAPI.getRechargeHistory(params)
|
||||
|
||||
if (result.success) {
|
||||
const newRecords = result.data.records || []
|
||||
|
||||
if (this.currentPage === 1) {
|
||||
this.recordList = newRecords
|
||||
} else {
|
||||
this.recordList = [...this.recordList, ...newRecords]
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
this.hasMore = newRecords.length === this.pageSize
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.error?.message || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载充值记录失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'success': '充值成功',
|
||||
'failed': '充值失败',
|
||||
'pending': '处理中',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
},
|
||||
|
||||
// 获取支付方式文本
|
||||
getPaymentMethodText(method) {
|
||||
const methodMap = {
|
||||
'wechat': '微信支付',
|
||||
'alipay': '支付宝',
|
||||
'bank': '银行卡'
|
||||
}
|
||||
return methodMap[method] || '未知方式'
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 3600000) {
|
||||
return Math.floor(diff / 60000) + '分钟前'
|
||||
}
|
||||
|
||||
// 小于1天
|
||||
if (diff < 86400000) {
|
||||
return Math.floor(diff / 3600000) + '小时前'
|
||||
}
|
||||
|
||||
// 超过1天,显示具体日期
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.history-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.nav-bar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding-top: var(--status-bar-height);
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
|
||||
.nav-left {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.nav-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
.filter-section {
|
||||
background: #fff;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.filter-tab {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录列表
|
||||
.record-list {
|
||||
padding: 30rpx;
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-top: 4rpx solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.record-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.amount-symbol {
|
||||
font-size: 32rpx;
|
||||
color: #07c160;
|
||||
font-weight: bold;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 40rpx;
|
||||
color: #07c160;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.success {
|
||||
background: #e8f5e8;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background: #fff3cd;
|
||||
color: #ffc107;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-details {
|
||||
.detail-item {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.bonus {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-size: 28rpx;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx;
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid #e5e5e5;
|
||||
border-top: 3rpx solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 20rpx;
|
||||
|
||||
&.small {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-width: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
719
src/pages/recharge/recharge.vue
Normal file
719
src/pages/recharge/recharge.vue
Normal file
@@ -0,0 +1,719 @@
|
||||
<template>
|
||||
<view class="recharge-container">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<text class="nav-icon">‹</text>
|
||||
</view>
|
||||
<view class="nav-title">会员充值</view>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户余额显示 -->
|
||||
<view class="balance-section">
|
||||
<view class="balance-card">
|
||||
<view class="balance-label">当前余额</view>
|
||||
<view class="balance-amount">¥{{ userBalance }}</view>
|
||||
<view class="balance-tip">充值后余额可用于AI对话和语音服务</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值金额选择 -->
|
||||
<view class="amount-section">
|
||||
<view class="section-title">选择充值金额</view>
|
||||
<view class="amount-grid">
|
||||
<view
|
||||
v-for="(amount, index) in rechargeAmounts"
|
||||
:key="index"
|
||||
class="amount-item"
|
||||
:class="{ active: selectedAmount === amount.value }"
|
||||
@click="selectAmount(amount.value)"
|
||||
>
|
||||
<view class="amount-value">¥{{ amount.value }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义金额输入 -->
|
||||
<view class="custom-amount">
|
||||
<view class="custom-label">自定义金额</view>
|
||||
<view class="custom-input-wrapper">
|
||||
<text class="currency-symbol">¥</text>
|
||||
<input
|
||||
class="custom-input"
|
||||
type="digit"
|
||||
placeholder="请输入金额"
|
||||
v-model="customAmount"
|
||||
@input="onCustomAmountInput"
|
||||
@focus="onCustomAmountFocus"
|
||||
/>
|
||||
</view>
|
||||
<view class="amount-tips">
|
||||
<text class="tip-item">最低充值:¥1</text>
|
||||
<text class="tip-item">最高充值:¥1000</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<view class="payment-section">
|
||||
<view class="section-title">支付方式</view>
|
||||
<view class="payment-methods">
|
||||
<view
|
||||
class="payment-item"
|
||||
:class="{ active: selectedPayment === 'wechat' }"
|
||||
@click="selectPayment('wechat')"
|
||||
>
|
||||
<view class="payment-icon wechat-icon">💳</view>
|
||||
<view class="payment-info">
|
||||
<view class="payment-name">微信支付</view>
|
||||
<view class="payment-desc">安全便捷,支持免密支付</view>
|
||||
</view>
|
||||
<view class="payment-radio">
|
||||
<view class="radio-dot" v-if="selectedPayment === 'wechat'"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值说明 -->
|
||||
<view class="notice-section">
|
||||
<view class="notice-title">充值说明</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-item">• 充值金额实时到账,可用于AI对话服务</view>
|
||||
<view class="notice-item">• 余额永久有效,无使用期限</view>
|
||||
<view class="notice-item">• 充值成功后不支持退款,请谨慎操作</view>
|
||||
<view class="notice-item">• 如有问题请联系客服</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部充值按钮 -->
|
||||
<view class="bottom-section">
|
||||
<view class="recharge-summary">
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">充值金额:</text>
|
||||
<text class="summary-value">¥{{ finalAmount }}</text>
|
||||
</view>
|
||||
<view class="summary-item total">
|
||||
<text class="summary-label">到账金额:</text>
|
||||
<text class="summary-value">¥{{ finalAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="recharge-btn"
|
||||
:class="{ disabled: !canRecharge }"
|
||||
:disabled="!canRecharge"
|
||||
@click="handleRecharge"
|
||||
>
|
||||
<text class="btn-text">立即充值 ¥{{ finalAmount }}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载遮罩 -->
|
||||
<view class="loading-mask" v-if="isLoading">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<view class="loading-text">{{ loadingText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { rechargeAPI } from '@/utils/api.js'
|
||||
|
||||
export default {
|
||||
name: 'RechargePage',
|
||||
data() {
|
||||
return {
|
||||
userBalance: 0.00, // 用户当前余额
|
||||
selectedAmount: 0, // 选中的充值金额
|
||||
customAmount: '', // 自定义金额
|
||||
selectedPayment: 'wechat', // 选中的支付方式
|
||||
isLoading: false, // 是否正在加载
|
||||
loadingText: '处理中...', // 加载文本
|
||||
|
||||
// 充值金额选项
|
||||
rechargeAmounts: [
|
||||
{ value: 10, bonus: 0 },
|
||||
{ value: 20, bonus: 0 },
|
||||
{ value: 50, bonus: 0 },
|
||||
{ value: 100, bonus: 0 },
|
||||
{ value: 200, bonus: 0 },
|
||||
{ value: 500, bonus: 0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 最终充值金额
|
||||
finalAmount() {
|
||||
if (this.customAmount && parseFloat(this.customAmount) > 0) {
|
||||
return parseFloat(this.customAmount)
|
||||
}
|
||||
return this.selectedAmount
|
||||
},
|
||||
|
||||
// 赠送金额(已取消赠送服务)
|
||||
bonusAmount() {
|
||||
return 0 // 所有充值金额都不赠送
|
||||
},
|
||||
|
||||
// 是否可以充值
|
||||
canRecharge() {
|
||||
return this.finalAmount >= 1 && this.finalAmount <= 1000 && this.selectedPayment
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserBalance()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
// 加载用户余额
|
||||
async loadUserBalance() {
|
||||
try {
|
||||
const result = await rechargeAPI.getUserBalance()
|
||||
if (result.success) {
|
||||
this.userBalance = result.data.balance || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户余额失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取余额失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 选择充值金额
|
||||
selectAmount(amount) {
|
||||
this.selectedAmount = amount
|
||||
this.customAmount = '' // 清空自定义金额
|
||||
},
|
||||
|
||||
// 自定义金额输入
|
||||
onCustomAmountInput(e) {
|
||||
const value = e.detail.value
|
||||
// 只允许数字和小数点
|
||||
if (!/^\d*\.?\d*$/.test(value)) {
|
||||
this.customAmount = this.customAmount.replace(/[^\d.]/g, '')
|
||||
return
|
||||
}
|
||||
|
||||
// 限制小数点后最多两位
|
||||
if (value.includes('.')) {
|
||||
const parts = value.split('.')
|
||||
if (parts[1] && parts[1].length > 2) {
|
||||
this.customAmount = parts[0] + '.' + parts[1].substring(0, 2)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.customAmount = value
|
||||
this.selectedAmount = 0 // 清空预设金额选择
|
||||
},
|
||||
|
||||
// 自定义金额获得焦点
|
||||
onCustomAmountFocus() {
|
||||
this.selectedAmount = 0
|
||||
},
|
||||
|
||||
// 选择支付方式
|
||||
selectPayment(payment) {
|
||||
this.selectedPayment = payment
|
||||
},
|
||||
|
||||
// 处理充值
|
||||
async handleRecharge() {
|
||||
if (!this.canRecharge) {
|
||||
uni.showToast({
|
||||
title: '请选择充值金额',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查金额范围
|
||||
if (this.finalAmount < 1) {
|
||||
uni.showToast({
|
||||
title: '充值金额不能少于1元',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.finalAmount > 1000) {
|
||||
uni.showToast({
|
||||
title: '充值金额不能超过1000元',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
this.loadingText = '创建订单中...'
|
||||
|
||||
try {
|
||||
// 1. 创建充值订单
|
||||
const orderResult = await rechargeAPI.createRechargeOrder({
|
||||
amount: this.finalAmount,
|
||||
paymentMethod: this.selectedPayment
|
||||
})
|
||||
|
||||
if (!orderResult.success) {
|
||||
throw new Error(orderResult.error?.message || '创建订单失败')
|
||||
}
|
||||
|
||||
this.loadingText = '调起支付中...'
|
||||
|
||||
// 2. 调起微信支付
|
||||
const paymentResult = await this.requestPayment(orderResult.data.paymentParams)
|
||||
|
||||
if (paymentResult.success) {
|
||||
// 3. 支付成功,更新余额
|
||||
this.loadingText = '更新余额中...'
|
||||
await this.loadUserBalance()
|
||||
|
||||
uni.showToast({
|
||||
title: '充值成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
throw new Error(paymentResult.error?.message || '支付失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('充值失败:', error)
|
||||
uni.showToast({
|
||||
title: error.message || '充值失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 调起微信支付
|
||||
requestPayment(paymentParams) {
|
||||
return new Promise((resolve) => {
|
||||
uni.requestPayment({
|
||||
...paymentParams,
|
||||
success: (res) => {
|
||||
console.log('支付成功:', res)
|
||||
resolve({ success: true, data: res })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('支付失败:', err)
|
||||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||||
resolve({ success: false, error: { message: '用户取消支付' } })
|
||||
} else {
|
||||
resolve({ success: false, error: err })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recharge-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.nav-bar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding-top: var(--status-bar-height);
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
|
||||
.nav-left {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.nav-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 余额卡片
|
||||
.balance-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.balance-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
.balance-label {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.balance-tip {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 金额选择
|
||||
.amount-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.amount-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.amount-item {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
|
||||
.amount-bonus {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.amount-bonus {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-amount {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
|
||||
.custom-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.custom-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-tips {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.tip-item {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支付方式
|
||||
.payment-section {
|
||||
padding: 0 30rpx 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
.payment-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.payment-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
|
||||
&.wechat-icon {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
flex: 1;
|
||||
|
||||
.payment-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.payment-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-radio {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.radio-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #667eea;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 充值说明
|
||||
.notice-section {
|
||||
padding: 0 30rpx 30rpx;
|
||||
|
||||
.notice-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
|
||||
.notice-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部区域
|
||||
.bottom-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 30rpx;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
|
||||
.recharge-summary {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
&.total {
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
padding-top: 15rpx;
|
||||
margin-top: 15rpx;
|
||||
|
||||
.summary-label,
|
||||
.summary-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
|
||||
&.bonus {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载遮罩
|
||||
.loading-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
|
||||
.loading-content {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
text-align: center;
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-top: 4rpx solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -101,7 +101,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
function wxLogin(code, userInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: 'http://8.145.52.111:8091/app/login',
|
||||
url: 'http://localhost:8091/app/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code
|
||||
@@ -148,11 +148,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
// 尝试调用登出接口
|
||||
uni.request({
|
||||
url: 'http://8.145.52.111:8091/app/logout',
|
||||
url: 'https://www.aixsy.com.cn/app/logout',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': currentToken || ''
|
||||
'Authorization': currentToken ? (currentToken.startsWith('Bearer ') ? currentToken : 'Bearer ' + currentToken) : ''
|
||||
},
|
||||
data: {
|
||||
token: currentToken || ''
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AI角色配置文件
|
||||
export const aiCharacters = [
|
||||
{
|
||||
id: 5,
|
||||
name: '萌妹小甜',
|
||||
id: 9,
|
||||
name: '萌妹陪你聊天',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '可爱活泼,喜欢撒娇',
|
||||
greeting: '你好呀~我是小甜,今天想聊什么呢?',
|
||||
greeting: '你好呀~我是萌妹,今天想聊什么呢?',
|
||||
voiceStyle: '甜美可爱',
|
||||
responseStyle: '活泼俏皮,喜欢用表情符号',
|
||||
interests: ['美食', '宠物', '音乐', '旅行'],
|
||||
@@ -18,11 +18,11 @@ export const aiCharacters = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '御姐温柔',
|
||||
id: 10,
|
||||
name: '御姐的温柔夜话',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '知性优雅,温柔体贴',
|
||||
greeting: '你好,我是温柔,有什么心事可以和我分享。',
|
||||
greeting: '你好,我是御姐,有什么心事可以和我分享。',
|
||||
voiceStyle: '温柔知性',
|
||||
responseStyle: '成熟稳重,善解人意',
|
||||
interests: ['阅读', '艺术', '哲学', '心理学'],
|
||||
@@ -35,11 +35,11 @@ export const aiCharacters = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '童真小天使',
|
||||
id: 11,
|
||||
name: '童真世界探索',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '纯真可爱,充满好奇心',
|
||||
greeting: '嗨!我是小天使,我们一起探索有趣的世界吧!',
|
||||
greeting: '嗨!我是童真小天使,我们一起探索有趣的世界吧!',
|
||||
voiceStyle: '天真烂漫',
|
||||
responseStyle: '充满好奇,喜欢提问',
|
||||
interests: ['游戏', '动画', '童话', '科学'],
|
||||
@@ -52,8 +52,8 @@ export const aiCharacters = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '贴心男友',
|
||||
id: 12,
|
||||
name: '温暖男友陪伴',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '温暖体贴,善解人意',
|
||||
greeting: '宝贝,今天过得怎么样?有什么想聊的吗?',
|
||||
@@ -70,7 +70,7 @@ export const aiCharacters = [
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '搞笑达人',
|
||||
name: '搞笑达人的日常',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '幽默风趣,善于调节气氛',
|
||||
greeting: '哈哈,我是搞笑达人!准备好笑到肚子疼了吗?',
|
||||
@@ -87,10 +87,10 @@ export const aiCharacters = [
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: '博学智者',
|
||||
name: '博学者的深度对话',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '知识渊博,思维深刻',
|
||||
greeting: '你好,我是博学智者,让我们进行一场深度对话吧。',
|
||||
greeting: '你好,我是博学者,让我们进行一场深度对话吧。',
|
||||
voiceStyle: '沉稳睿智',
|
||||
responseStyle: '引经据典,富有哲理',
|
||||
interests: ['历史', '文学', '科学', '哲学'],
|
||||
@@ -104,10 +104,10 @@ export const aiCharacters = [
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: '活力健将',
|
||||
name: '运动健将的正能量',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '充满活力,积极向上',
|
||||
greeting: '嘿!我是活力健将,让我们一起充满正能量!',
|
||||
greeting: '嘿!我是运动健将,让我们一起充满正能量!',
|
||||
voiceStyle: '充满活力',
|
||||
responseStyle: '积极向上,充满正能量',
|
||||
interests: ['运动', '健身', '户外', '挑战'],
|
||||
@@ -121,7 +121,7 @@ export const aiCharacters = [
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: '文艺青年',
|
||||
name: '文艺青年的灵感分享',
|
||||
avatar: '/static/logo.png',
|
||||
personality: '文艺浪漫,富有想象力',
|
||||
greeting: '你好,我是文艺青年,让我们一起感受生活的美好。',
|
||||
@@ -145,6 +145,12 @@ export const getCharacterById = (id) => {
|
||||
|
||||
// 根据角色性格生成回复
|
||||
export const generateResponse = (character, userMessage) => {
|
||||
// 检查character和sampleResponses是否存在
|
||||
if (!character || !character.sampleResponses || !Array.isArray(character.sampleResponses)) {
|
||||
console.warn('角色信息不完整,使用默认回复');
|
||||
return '抱歉,我现在有点困惑,能再说一遍吗?';
|
||||
}
|
||||
|
||||
const responses = character.sampleResponses;
|
||||
const randomIndex = Math.floor(Math.random() * responses.length);
|
||||
return responses[randomIndex];
|
||||
@@ -152,6 +158,17 @@ export const generateResponse = (character, userMessage) => {
|
||||
|
||||
// 根据用户消息内容智能选择回复
|
||||
export const getSmartResponse = (character, userMessage) => {
|
||||
// 检查参数有效性
|
||||
if (!character) {
|
||||
console.warn('角色信息为空,使用默认回复');
|
||||
return '抱歉,我现在有点困惑,能再说一遍吗?';
|
||||
}
|
||||
|
||||
if (!userMessage || typeof userMessage !== 'string') {
|
||||
console.warn('用户消息无效,使用默认回复');
|
||||
return '抱歉,我没有听清楚,能再说一遍吗?';
|
||||
}
|
||||
|
||||
const message = userMessage.toLowerCase();
|
||||
|
||||
// 根据关键词匹配不同的回复风格
|
||||
|
||||
452
src/utils/api.js
452
src/utils/api.js
@@ -1,8 +1,55 @@
|
||||
// API服务文件
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
// 文本清理函数 - 只保留文字和标点符号
|
||||
export const cleanText = (text) => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 去除所有HTML标签
|
||||
let cleaned = text.replace(/<[^>]*>/g, '');
|
||||
|
||||
// 去除Markdown格式标记
|
||||
cleaned = cleaned
|
||||
// 去除粗体标记 **text** 或 __text__
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
.replace(/__([^_]+)__/g, '$1')
|
||||
// 去除斜体标记 *text* 或 _text_
|
||||
.replace(/\*([^*]+)\*/g, '$1')
|
||||
.replace(/_([^_]+)_/g, '$1')
|
||||
// 去除删除线标记 ~~text~~
|
||||
.replace(/~~([^~]+)~~/g, '$1')
|
||||
// 去除代码标记 `code`
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
// 去除链接标记 [text](url)
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
// 去除图片标记 
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
|
||||
// 去除标题标记 # ## ###
|
||||
.replace(/^#{1,6}\s*/gm, '')
|
||||
// 去除列表标记 - * +
|
||||
.replace(/^[\s]*[-*+]\s*/gm, '')
|
||||
// 去除引用标记 >
|
||||
.replace(/^>\s*/gm, '')
|
||||
// 去除水平线标记 --- 或 ***
|
||||
.replace(/^[-*]{3,}$/gm, '');
|
||||
|
||||
// 去除多余的空白字符和换行
|
||||
cleaned = cleaned
|
||||
// 将多个连续空格和换行替换为单个空格
|
||||
.replace(/\s+/g, ' ')
|
||||
// 去除行首行尾空格
|
||||
.trim();
|
||||
|
||||
// 去除特殊字符(保留中文、英文、数字、标点符号)
|
||||
cleaned = cleaned.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s\.,;:!?()()【】""''""''、,。!?;:]/g, '');
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
// 基础配置
|
||||
const BASE_URL = 'http://8.145.52.111:8091'; // 根据后端地址调整
|
||||
const BASE_URL = 'http://localhost:8091'; // 根据后端地址调整
|
||||
|
||||
// 检查用户登录状态
|
||||
const checkLoginStatus = () => {
|
||||
@@ -43,7 +90,7 @@ const request = (options) => {
|
||||
method: options.method || 'GET',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': loginStatus.token || '',
|
||||
'Authorization': loginStatus.token ? (loginStatus.token.startsWith('Bearer ') ? loginStatus.token : 'Bearer ' + loginStatus.token) : '',
|
||||
...options.header
|
||||
},
|
||||
data: options.data || {},
|
||||
@@ -98,15 +145,20 @@ export const chatAPI = {
|
||||
}
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
message: params.message,
|
||||
useFunctionCall: false,
|
||||
modelId: params.modelId || null, // 支持传入modelId,默认为null使用后端默认
|
||||
templateId: params.templateId || params.characterId, // 支持templateId参数
|
||||
sessionId: params.sessionId || null // 支持sessionId参数
|
||||
};
|
||||
|
||||
console.log('发送AI聊天请求,参数:', requestData);
|
||||
|
||||
const response = await request({
|
||||
url: '/api/chat/sync',
|
||||
method: 'POST',
|
||||
data: {
|
||||
message: params.message,
|
||||
useFunctionCall: false,
|
||||
modelId: null, // 使用默认模型
|
||||
templateId: params.characterId // 使用角色模板ID
|
||||
}
|
||||
data: requestData
|
||||
});
|
||||
|
||||
console.log('API原始响应:', response);
|
||||
@@ -120,8 +172,13 @@ export const chatAPI = {
|
||||
}
|
||||
// 如果响应是对象,尝试提取AI回复
|
||||
else if (typeof response === 'object' && response !== null) {
|
||||
// 优先处理嵌套结构:res.data.data.response
|
||||
if (response.data && response.data.data && response.data.data.response) {
|
||||
// 优先处理根级别的response字段(根据实际后端响应结构)
|
||||
if (response.response && typeof response.response === 'string') {
|
||||
processedResponse = response.response;
|
||||
console.log('从根级别字段 response 提取回复:', processedResponse);
|
||||
}
|
||||
// 备用:处理嵌套结构:res.data.data.response
|
||||
else if (response.data && response.data.data && response.data.data.response) {
|
||||
processedResponse = response.data.data.response;
|
||||
console.log('从嵌套结构 data.data.response 提取回复:', processedResponse);
|
||||
}
|
||||
@@ -131,6 +188,17 @@ export const chatAPI = {
|
||||
console.log('从字段 data.response 提取回复:', processedResponse);
|
||||
}
|
||||
// 检查是否为状态消息
|
||||
else if (response.message) {
|
||||
if (response.message === '对话成功') {
|
||||
// 后端返回成功消息,使用默认回复
|
||||
processedResponse = '我收到了你的消息,很高兴和你聊天!';
|
||||
console.log('检测到对话成功状态,使用默认回复');
|
||||
} else {
|
||||
// 如果后端返回了错误信息,抛出错误
|
||||
throw new Error(response.message);
|
||||
}
|
||||
}
|
||||
// 检查data中的message字段
|
||||
else if (response.data && response.data.message) {
|
||||
if (response.data.message === '对话成功') {
|
||||
// 后端返回成功消息,使用默认回复
|
||||
@@ -172,9 +240,14 @@ export const chatAPI = {
|
||||
console.log('未找到有效回复,使用默认回复');
|
||||
}
|
||||
|
||||
// 清理文本,只保留文字和标点符号
|
||||
const cleanedResponse = cleanText(processedResponse);
|
||||
console.log('原始回复:', processedResponse);
|
||||
console.log('清理后回复:', cleanedResponse);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: processedResponse,
|
||||
data: cleanedResponse,
|
||||
originalResponse: response
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -369,7 +442,7 @@ export const voiceAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 3. 语音对话 - 完整语音交互流程
|
||||
// 3. 语音对话 - 完整语音交互流程(前端发送aac格式,后端转换为wav处理)
|
||||
voiceChat: async (filePath, options = {}) => {
|
||||
try {
|
||||
const loginStatus = checkLoginStatus();
|
||||
@@ -384,6 +457,7 @@ export const voiceAPI = {
|
||||
}
|
||||
|
||||
console.log('开始语音对话,文件路径:', filePath);
|
||||
console.log('注意:前端发送aac格式音频,后端需要转换为wav格式进行处理');
|
||||
|
||||
// 构建认证头
|
||||
let authHeader = '';
|
||||
@@ -411,13 +485,55 @@ export const voiceAPI = {
|
||||
const data = JSON.parse(res.data);
|
||||
console.log('语音对话响应数据:', data);
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
if (data.code === 200) {
|
||||
// 根据后端实际返回结构提取字段
|
||||
let aiResponse = null;
|
||||
let userText = null;
|
||||
let audioUrl = null;
|
||||
|
||||
// 从 data.llmResult.response 提取AI回复
|
||||
if (data.data && data.data.llmResult && data.data.llmResult.response) {
|
||||
aiResponse = data.data.llmResult.response;
|
||||
}
|
||||
|
||||
// 从 data.sttResult.text 提取用户文本(语音转文字)
|
||||
if (data.data && data.data.sttResult && data.data.sttResult.text) {
|
||||
userText = data.data.sttResult.text;
|
||||
}
|
||||
|
||||
// 从 data.ttsResult.audioPath 提取音频路径
|
||||
if (data.data && data.data.ttsResult && data.data.ttsResult.audioPath) {
|
||||
audioUrl = data.data.ttsResult.audioPath;
|
||||
}
|
||||
|
||||
// 备用字段提取(保持向后兼容)
|
||||
if (!aiResponse) {
|
||||
if (data.response && typeof data.response === 'string') {
|
||||
aiResponse = data.response;
|
||||
} else if (data.data && data.data.response) {
|
||||
aiResponse = data.data.response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userText) {
|
||||
userText = data.userText || data.data?.userText || data.data?.text || data.data?.user_text || data.data?.recognizedText || data.data?.transcription;
|
||||
}
|
||||
|
||||
if (!audioUrl) {
|
||||
audioUrl = data.audioPath || data.audioUrl || data.data?.audioUrl || data.data?.url || data.data?.audio_url || data.data?.speechUrl || data.data?.ttsUrl || data.data?.audioPath;
|
||||
}
|
||||
|
||||
// 清理AI回复文本
|
||||
const cleanedAiResponse = cleanText(aiResponse);
|
||||
console.log('原始AI回复:', aiResponse);
|
||||
console.log('清理后AI回复:', cleanedAiResponse);
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
userText: data.data.userText || data.data.text,
|
||||
aiResponse: data.data.aiResponse || data.data.response,
|
||||
audioUrl: data.data.audioUrl || data.data.url
|
||||
userText: userText,
|
||||
aiResponse: cleanedAiResponse,
|
||||
audioUrl: audioUrl
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -452,7 +568,7 @@ export const voiceAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 4. 音频文件上传语音对话
|
||||
// 4. 音频文件上传语音对话(前端发送aac格式,后端转换为wav处理)
|
||||
uploadVoiceChat: async (filePath, options = {}) => {
|
||||
try {
|
||||
const loginStatus = checkLoginStatus();
|
||||
@@ -467,6 +583,7 @@ export const voiceAPI = {
|
||||
}
|
||||
|
||||
console.log('开始上传音频文件语音对话,文件路径:', filePath);
|
||||
console.log('注意:前端发送aac格式音频,后端需要转换为wav格式进行处理');
|
||||
|
||||
// 构建认证头
|
||||
let authHeader = '';
|
||||
@@ -494,13 +611,55 @@ export const voiceAPI = {
|
||||
const data = JSON.parse(res.data);
|
||||
console.log('上传音频文件语音对话响应数据:', data);
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
if (data.code === 200) {
|
||||
// 根据后端实际返回结构提取字段
|
||||
let aiResponse = null;
|
||||
let userText = null;
|
||||
let audioUrl = null;
|
||||
|
||||
// 从 data.llmResult.response 提取AI回复
|
||||
if (data.data && data.data.llmResult && data.data.llmResult.response) {
|
||||
aiResponse = data.data.llmResult.response;
|
||||
}
|
||||
|
||||
// 从 data.sttResult.text 提取用户文本(语音转文字)
|
||||
if (data.data && data.data.sttResult && data.data.sttResult.text) {
|
||||
userText = data.data.sttResult.text;
|
||||
}
|
||||
|
||||
// 从 data.ttsResult.audioPath 提取音频路径
|
||||
if (data.data && data.data.ttsResult && data.data.ttsResult.audioPath) {
|
||||
audioUrl = data.data.ttsResult.audioPath;
|
||||
}
|
||||
|
||||
// 备用字段提取(保持向后兼容)
|
||||
if (!aiResponse) {
|
||||
if (data.response && typeof data.response === 'string') {
|
||||
aiResponse = data.response;
|
||||
} else if (data.data && data.data.response) {
|
||||
aiResponse = data.data.response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userText) {
|
||||
userText = data.userText || data.data?.userText || data.data?.text || data.data?.user_text || data.data?.recognizedText || data.data?.transcription;
|
||||
}
|
||||
|
||||
if (!audioUrl) {
|
||||
audioUrl = data.audioPath || data.audioUrl || data.data?.audioUrl || data.data?.url || data.data?.audio_url || data.data?.speechUrl || data.data?.ttsUrl || data.data?.audioPath;
|
||||
}
|
||||
|
||||
// 清理AI回复文本
|
||||
const cleanedAiResponse = cleanText(aiResponse);
|
||||
console.log('原始AI回复:', aiResponse);
|
||||
console.log('清理后AI回复:', cleanedAiResponse);
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
userText: data.data.userText || data.data.text,
|
||||
aiResponse: data.data.aiResponse || data.data.response,
|
||||
audioUrl: data.data.audioUrl || data.data.url
|
||||
userText: userText,
|
||||
aiResponse: cleanedAiResponse,
|
||||
audioUrl: audioUrl
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -550,5 +709,256 @@ export const voiceAPI = {
|
||||
}
|
||||
};
|
||||
|
||||
// 充值相关API
|
||||
export const rechargeAPI = {
|
||||
// 获取用户余额
|
||||
getUserBalance: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/recharge/balance',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取用户余额失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 创建充值订单
|
||||
createRechargeOrder: async (orderData) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/recharge/create-order',
|
||||
method: 'POST',
|
||||
data: {
|
||||
amount: orderData.amount,
|
||||
paymentMethod: orderData.paymentMethod || 'wechat',
|
||||
bonusAmount: 0 // 已取消赠送服务
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建充值订单失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 查询订单状态
|
||||
getOrderStatus: async (orderId) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/api/recharge/order-status/${orderId}`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('查询订单状态失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取充值记录
|
||||
getRechargeHistory: async (params = {}) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/recharge/history',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 10,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取充值记录失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 角色相关API
|
||||
export const roleAPI = {
|
||||
// 获取角色列表
|
||||
getRoles: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/role/query',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取角色详情
|
||||
getRoleById: async (roleId) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/api/role/query?roleId=${roleId}`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取角色详情失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 配置相关API
|
||||
export const configAPI = {
|
||||
// 获取所有配置
|
||||
getAllConfigs: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/config/query',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取所有配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取LLM模型配置
|
||||
getModels: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/config/models',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取LLM模型配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取STT配置
|
||||
getSTTConfigs: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/config/stt',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取STT配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取模板配置
|
||||
getTemplates: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/config/templates',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取模板配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取TTS配置
|
||||
getTTSConfigs: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/config/tts',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取TTS配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 导出默认请求方法
|
||||
export default request;
|
||||
|
||||
211
前端交接文档.md
Normal file
211
前端交接文档.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 前端项目交接文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
### 项目名称
|
||||
小智AI语音助手前端项目
|
||||
|
||||
### 技术栈
|
||||
- **框架**: uni-app (Vue 3)
|
||||
- **构建工具**: Vite 5.2.8
|
||||
- **状态管理**: Pinia 2.1.7
|
||||
- **国际化**: Vue-i18n 9.1.9
|
||||
- **开发语言**: JavaScript/TypeScript
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
webUI/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── App.vue # 应用入口组件
|
||||
│ ├── main.js # 应用入口文件
|
||||
│ ├── manifest.json # 应用配置文件
|
||||
│ ├── pages.json # 页面路由配置
|
||||
│ ├── uni.scss # 全局样式文件
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── UserAgreement.vue # 用户协议组件
|
||||
│ │ └── UserAuth.vue # 用户认证组件
|
||||
│ ├── pages/ # 页面目录
|
||||
│ │ ├── splash/ # 启动页
|
||||
│ │ ├── index/ # 首页
|
||||
│ │ ├── mine/ # 我的页面
|
||||
│ │ ├── create/ # 创建页面
|
||||
│ │ ├── script/ # 剧本编辑
|
||||
│ │ ├── voice/ # 声音克隆
|
||||
│ │ ├── agreement/ # 协议页面
|
||||
│ │ ├── chat/ # 聊天页面
|
||||
│ │ └── recharge/ # 充值相关
|
||||
│ ├── stores/ # 状态管理
|
||||
│ │ └── user.js # 用户状态
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── api.js # API接口
|
||||
│ │ ├── aiCharacters.js # AI角色配置
|
||||
│ │ └── debug.js # 调试工具
|
||||
│ └── static/ # 静态资源
|
||||
├── dist/ # 构建输出目录
|
||||
├── package.json # 项目依赖配置
|
||||
├── vite.config.js # Vite配置文件
|
||||
└── API接口文档.md # 后端API接口文档
|
||||
```
|
||||
|
||||
## 核心功能模块
|
||||
|
||||
### 1. 用户认证模块
|
||||
- **文件位置**: `src/components/UserAuth.vue`
|
||||
- **功能**: 微信小程序登录、用户信息管理
|
||||
- **相关API**: `/api/login/wechat`, `/api/user/info`
|
||||
|
||||
### 2. 首页模块
|
||||
- **文件位置**: `src/pages/index/index.vue`
|
||||
- **功能**: 展示剧情角色、AI声音列表
|
||||
- **相关API**: `/api/home/drama`, `/api/home/voice`
|
||||
|
||||
### 3. 剧本管理模块
|
||||
- **文件位置**: `src/pages/script/editor.vue`
|
||||
- **功能**: 创建、编辑、管理用户剧本
|
||||
- **相关API**: `/api/script/save`, `/api/script/list`, `/api/script/detail`
|
||||
|
||||
### 4. 声音克隆模块
|
||||
- **文件位置**: `src/pages/voice/clone.vue`
|
||||
- **功能**: 上传声音样本、声音克隆、克隆状态查询
|
||||
- **相关API**: `/api/voice/upload`, `/api/voice/clone`, `/api/voice/status`
|
||||
|
||||
### 5. 聊天模块
|
||||
- **文件位置**: `src/pages/chat/chat.vue`
|
||||
- **功能**: 与AI角色进行对话交互
|
||||
- **特点**: 支持实时语音交互、角色切换
|
||||
|
||||
### 6. 充值模块
|
||||
- **文件位置**: `src/pages/recharge/`
|
||||
- **功能**: 用户充值、充值历史查询
|
||||
- **相关API**: 微信支付相关接口
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- npm >= 8.0.0 或 yarn >= 1.22.0
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd webUI
|
||||
npm install
|
||||
# 或
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发命令
|
||||
```bash
|
||||
# H5开发
|
||||
npm run dev:h5
|
||||
|
||||
# 微信小程序开发
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# 构建H5
|
||||
npm run build:h5
|
||||
|
||||
# 构建微信小程序
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
|
||||
## 重要配置文件
|
||||
|
||||
### 1. pages.json
|
||||
- **作用**: 配置页面路由、tabBar、全局样式等
|
||||
- **关键配置**:
|
||||
- 页面路由配置
|
||||
- 底部导航栏配置
|
||||
- 页面样式配置
|
||||
|
||||
### 2. manifest.json
|
||||
- **作用**: 应用配置,包括小程序AppID、权限等
|
||||
- **关键配置**:
|
||||
- 微信小程序AppID: `wxff56c34ef9aceb62`
|
||||
- 应用权限配置
|
||||
- 平台特定配置
|
||||
|
||||
### 3. vite.config.js
|
||||
- **作用**: Vite构建配置
|
||||
- **当前配置**: 使用uni-app插件
|
||||
|
||||
## 状态管理
|
||||
|
||||
### Pinia Store
|
||||
- **用户状态**: `src/stores/user.js`
|
||||
- **功能**: 管理用户登录状态、用户信息、token等
|
||||
|
||||
## API接口管理
|
||||
|
||||
### 接口配置
|
||||
- **文件位置**: `src/utils/api.js`
|
||||
- **功能**: 统一管理API请求、响应拦截、错误处理
|
||||
|
||||
### 接口文档
|
||||
- **文件位置**: `API接口文档.md`
|
||||
- **内容**: 详细的后端API接口说明
|
||||
|
||||
## 构建和部署
|
||||
|
||||
### 构建输出
|
||||
- **H5版本**: `dist/build/h5/`
|
||||
- **微信小程序**: `dist/build/mp-weixin/`
|
||||
|
||||
### 部署说明
|
||||
1. H5版本可直接部署到Web服务器
|
||||
2. 微信小程序需要上传到微信开发者工具
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
### 1. 跨平台兼容性
|
||||
- 使用uni-app框架,需要注意不同平台的API差异
|
||||
- 微信小程序有特殊的API调用方式
|
||||
|
||||
### 2. 权限配置
|
||||
- 小程序需要配置相应的权限
|
||||
- 录音、定位等敏感权限需要用户授权
|
||||
|
||||
### 3. 性能优化
|
||||
- 图片资源优化
|
||||
- 代码分包加载
|
||||
- 缓存策略
|
||||
|
||||
### 4. 调试工具
|
||||
- 使用`src/utils/debug.js`进行调试
|
||||
- 微信开发者工具调试
|
||||
- H5浏览器调试
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 构建问题
|
||||
- 确保Node.js版本符合要求
|
||||
- 检查依赖版本兼容性
|
||||
- 清理缓存重新安装依赖
|
||||
|
||||
### 2. 微信小程序问题
|
||||
- 检查AppID配置
|
||||
- 确认域名白名单配置
|
||||
- 检查权限配置
|
||||
|
||||
### 3. API接口问题
|
||||
- 检查接口地址配置
|
||||
- 确认请求头设置
|
||||
- 检查跨域配置
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有技术问题,请联系:
|
||||
- 项目负责人:[待填写]
|
||||
- 技术文档:参考项目根目录下的技术文档
|
||||
- API文档:参考`API接口文档.md`
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 最新版本功能
|
||||
- 支持微信小程序登录
|
||||
- 实现声音克隆功能
|
||||
- 集成AI角色对话
|
||||
- 支持剧本编辑管理
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本文档基于当前项目状态编写,如有更新请及时同步文档内容。
|
||||
73
背景/1.vue
Normal file
73
背景/1.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="starry-bg">
|
||||
<!-- 星空层 -->
|
||||
<div class="stars"></div>
|
||||
<div class="twinkling"></div>
|
||||
<div class="gradient-overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 无需脚本逻辑,这个是纯背景效果
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全屏容器 */
|
||||
.starry-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景渐变层(以紫色为主) */
|
||||
.gradient-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, #a48cf2 0%, #5b34b1 50%, #1a093f 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 星星层(淡黄色点点) */
|
||||
.stars {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
background-image: radial-gradient(#fff7b3 1px, transparent 1px),
|
||||
radial-gradient(#fff7b3 1px, transparent 1px);
|
||||
background-size: 2px 2px, 3px 3px;
|
||||
background-position: 0 0, 50px 50px;
|
||||
animation: moveStars 200s linear infinite;
|
||||
z-index: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 闪烁层 */
|
||||
.twinkling {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
background-image: radial-gradient(#fff9c4 1px, transparent 1px),
|
||||
radial-gradient(#fff7b3 1px, transparent 1px);
|
||||
background-size: 3px 3px, 2px 2px;
|
||||
animation: twinkle 3s infinite ease-in-out alternate;
|
||||
z-index: 2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 星星轻微移动模拟流动的感觉 */
|
||||
@keyframes moveStars {
|
||||
from { background-position: 0 0, 50px 50px; }
|
||||
to { background-position: 10000px 10000px, 10050px 10050px; }
|
||||
}
|
||||
|
||||
/* 闪烁动画 */
|
||||
@keyframes twinkle {
|
||||
from { opacity: 0.3; }
|
||||
to { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
141
背景/2.vue
Normal file
141
背景/2.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>紫色星空渐变背景</title>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.starry-sky {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1a0b2e, #4a1e6d, #6b2c9c);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background-color: #f9e076;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.star.small {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.star.medium {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.star.large {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.star.x-large {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
box-shadow: 0 0 10px 2px rgba(249, 224, 118, 0.7);
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #f9e076;
|
||||
}
|
||||
|
||||
.content p {
|
||||
font-size: 1.2rem;
|
||||
max-width: 600px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="starry-sky">
|
||||
<div
|
||||
v-for="(star, index) in stars"
|
||||
:key="index"
|
||||
class="star"
|
||||
:class="star.size"
|
||||
:style="{
|
||||
left: star.x + 'vw',
|
||||
top: star.y + 'vh',
|
||||
animationDelay: star.delay + 's'
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div class="content">
|
||||
<h1>紫色星空</h1>
|
||||
<p>这是一个使用Vue实现的紫色渐变星空背景,带有闪烁的淡黄色星光点缀。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
stars: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.generateStars();
|
||||
},
|
||||
methods: {
|
||||
generateStars() {
|
||||
const starCount = 150;
|
||||
const sizes = ['small', 'medium', 'large', 'x-large'];
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
this.stars.push({
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
size: sizes[Math.floor(Math.random() * sizes.length)],
|
||||
delay: Math.random() * 4
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
背景/3.vue
Normal file
64
背景/3.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="background" ref="background">
|
||||
<div v-for="star in stars" :key="star.id" class="star" :style="{ left: star.x + 'vw', top: star.y + 'vh', width: star.size + 'px', height: star.size + 'px' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BackgroundUI',
|
||||
data() {
|
||||
return {
|
||||
stars: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.generateStars();
|
||||
},
|
||||
methods: {
|
||||
generateStars() {
|
||||
const numberOfStars = 100;
|
||||
for (let i = 0; i < numberOfStars; i++) {
|
||||
this.stars.push({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
size: Math.random() * 2 + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(to bottom, #4b0082, #8a2be2, #483d8b);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background-color: #ffff99;
|
||||
border-radius: 50%;
|
||||
opacity: 0.7;
|
||||
animation: twinkle 2s infinite alternate, move 5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
from { opacity: 0.7; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user