feat:交接

This commit is contained in:
2025-11-04 19:25:16 +08:00
parent 4b8498203d
commit d45e556c20
20 changed files with 4441 additions and 677 deletions

45
AGENTS.md Normal file
View 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
View File

@@ -0,0 +1,55 @@
# 项目说明
本项目是基于 Uni-AppVue 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
View File

@@ -0,0 +1 @@
34608

View File

@@ -1,5 +1,5 @@
{ {
"appid": "wxff56c34ef9aceb62", "appid": "wx7f35c6442d0694a9",
"compileType": "miniprogram", "compileType": "miniprogram",
"libVersion": "3.8.10", "libVersion": "3.8.10",
"packOptions": { "packOptions": {
@@ -18,11 +18,24 @@
"ignore": [], "ignore": [],
"disablePlugins": [], "disablePlugins": [],
"outputPath": "" "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": {}, "condition": {},
"editorSetting": { "editorSetting": {
"tabIndent": "insertSpaces", "tabIndent": "insertSpaces",
"tabSize": 4 "tabSize": 4
} },
"simulatorPluginLibVersion": {}
} }

View File

@@ -1,7 +1,23 @@
{ {
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "webUI", "projectname": "webUI",
"setting": { "setting": {
"compileHotReLoad": true "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": {}
} }

View File

@@ -14,7 +14,9 @@
{ {
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"enablePullDownRefresh": false,
"disableScroll": false
} }
}, },
{ {
@@ -35,6 +37,12 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "pages/drama/index",
"style": {
"navigationStyle": "custom"
}
},
{ {
"path": "pages/voice/clone", "path": "pages/voice/clone",
"style": { "style": {
@@ -56,6 +64,19 @@
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/recharge/recharge",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/recharge/history",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
} }
], ],
"globalStyle": { "globalStyle": {
@@ -65,14 +86,13 @@
"backgroundColor": "#F8F8F8" "backgroundColor": "#F8F8F8"
}, },
"tabBar": { "tabBar": {
"color": "#999999", "color": "rgba(255, 255, 255, 0.5)",
"selectedColor": "#ff9800", "selectedColor": "#f9e076",
"backgroundColor": "#ffffff", "backgroundColor": "rgba(26, 11, 46, 0.95)",
"borderStyle": "black", "borderStyle": "white",
"list": [ "list": [
{ {
"pagePath": "pages/index/index", "pagePath": "pages/index/index",
"text": "首页" "text": "首页"
}, },
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,24 @@
<template> <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="custom-navbar fixed-navbar">
<view class="navbar-left" @click="goBack"> <view class="navbar-left" @tap="goBack"> 返回</view>
<text class="back-icon"></text>
</view>
<text class="navbar-title">创建</text> <text class="navbar-title">创建</text>
<view class="navbar-right"></view>
</view> </view>
<!-- 创建选项区域 --> <!-- 创建选项区域 -->
@@ -85,71 +97,208 @@ const showLoginTip = () => {
</script> </script>
<style scoped> <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 { .fixed-navbar {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
z-index: 100; z-index: 1002;
}
.container {
background: #fff9f2;
min-height: 100vh;
padding-top: 100rpx;
padding-bottom: 40rpx;
} }
.custom-navbar { .custom-navbar {
height: 100rpx; height: 100rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
background: linear-gradient(90deg, #ffe5c2, #fff6e5); 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%);
padding: 0 30rpx; backdrop-filter: blur(10px);
} border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
.navbar-left, .navbar-right {
width: 60rpx;
display: flex;
align-items: center;
}
.back-icon {
font-size: 36rpx;
font-weight: bold; font-weight: bold;
color: #333; font-size: 32rpx;
}
.navbar-title {
color: #333;
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx; 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 { .create-options {
padding: 40rpx 30rpx; padding: 40rpx 30rpx;
z-index: 2;
position: relative;
} }
.option-card { .option-card {
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: rgba(255, 255, 255, 0.9);
border-radius: 20rpx; 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; padding: 40rpx 30rpx;
margin-bottom: 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 { .option-icon {
font-size: 60rpx; font-size: 60rpx;
margin-right: 30rpx; margin-right: 30rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(138, 43, 226, 0.3));
} }
.option-content { .option-content {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
} }
.option-title { .option-title {
font-size: 34rpx; font-size: 34rpx;
font-weight: bold; font-weight: bold;
margin-bottom: 10rpx; margin-bottom: 10rpx;
color: #333;
} }
.option-desc { .option-desc {
font-size: 28rpx; 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> </style>

387
src/pages/drama/index.vue Normal file
View 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

View File

@@ -1,63 +1,100 @@
<template> <template>
<view class="container"> <view class="container">
<!-- 头部以上背景区域 -->
<view class="top-background"></view>
<!-- 顶部自定义导航栏 --> <!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar"> <view class="custom-navbar fixed-navbar">
<text class="navbar-title">我的</text> <text class="navbar-title">我的</text>
</view> </view>
<!-- 用户信息区域 --> <!-- 可滚动内容区域 -->
<view class="user-info"> <scroll-view
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image> class="scroll-container"
<view class="user-details" v-if="isLoggedIn"> scroll-y="true"
<text class="login-text">{{ nickName }}</text> enable-back-to-top="false"
<text class="user-id">ID: {{ openid }}</text> refresher-enabled="false"
</view> :refresher-triggered="false"
<view class="user-details" v-else> :refresher-threshold="0"
<text class="login-text">请登录</text> :show-scrollbar="false"
<text class="user-id">ID:</text> :scroll-with-animation="false"
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button> :bounces="false"
</view> :always-bounce-vertical="false"
</view> :scroll-top="0"
:upper-threshold="0"
<!-- 菜单卡片 --> :lower-threshold="0"
<view class="menu-cards"> >
<!-- 用户须知 --> <!-- 用户信息区域 -->
<view class="menu-card" @click="showAgreement"> <view class="user-info">
<view class="card-icon notice-icon">🔊</view> <image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
<text class="card-title">用户须知</text> <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>
<!-- 用户设备 --> <!-- 余额显示 -->
<view class="menu-card"> <view class="balance-section" v-if="isLoggedIn">
<view class="card-icon device-icon">📦</view> <view class="balance-card">
<text class="card-title">用户设备</text> <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>
<!-- 菜单卡片 -->
<!-- 剧情角色区块 --> <view class="menu-cards">
<view class="section-block"> <!-- 用户须知 -->
<view class="section-header"> <view class="menu-card" @click="showAgreement">
<text class="section-title">剧情角色</text> <view class="card-icon notice-icon">🔊</view>
<text class="view-all">查看全部</text> <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>
<!-- 剧情角色区块 -->
<!-- 克隆声音区块 --> <view class="section-block">
<view class="section-block"> <view class="section-header">
<view class="section-header"> <text class="section-title">剧情角色</text>
<text class="section-title">克隆声音</text> <text class="view-all">查看全部</text>
<text class="view-all">查看全部</text> </view>
</view> </view>
</view>
<!-- 克隆声音区块 -->
<!-- 退出登录按钮仅登录后显示 --> <view class="section-block">
<view class="logout-section"> <view class="section-header">
<button class="logout-btn" @click="handleLogout">退出登录</button> <text class="section-title">克隆声音</text>
<button class="logout-btn" style="margin-top: 20rpx; background-color: #ff9800; color: white;" @click="testDirectApiCall">直接测试API</button> <text class="view-all">查看全部</text>
<text style="display: block; margin-top: 20rpx; text-align: center; color: #999;"> </view>
登录状态: {{ isLoggedIn ? '已登录' : '未登录' }} </view>
</text>
</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 <user-agreement
@@ -71,6 +108,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useUserStore } from '@/stores/user.js'; import { useUserStore } from '@/stores/user.js';
import { rechargeAPI } from '@/utils/api.js';
import UserAgreement from '@/components/UserAgreement.vue'; import UserAgreement from '@/components/UserAgreement.vue';
// 状态管理 - 避免直接依赖store的响应式 // 状态管理 - 避免直接依赖store的响应式
@@ -82,6 +120,7 @@ const isLoggedIn = ref(false);
const nickName = ref(''); const nickName = ref('');
const openid = ref(''); const openid = ref('');
const avatarUrl = ref('/static/default-avatar.png'); const avatarUrl = ref('/static/default-avatar.png');
const userBalance = ref(0.00);
// 登录按钮文本 // 登录按钮文本
const loginButtonText = computed(() => { const loginButtonText = computed(() => {
@@ -128,15 +167,13 @@ onMounted(() => {
userStore.init(); userStore.init();
// 初始化本地状态 // 初始化本地状态
initUserInfo(); initUserInfo();
// 加载用户余额
loadUserBalance();
}); });
// 登录处理 // 登录处理
const handleLogin = async () => { const handleLogin = async () => {
try { try {
// 先清理假登录数据
console.log('开始登录,先清理假登录数据...');
userStore.clearFakeLoginData();
uni.showLoading({ uni.showLoading({
title: '登录中...' title: '登录中...'
}); });
@@ -156,18 +193,11 @@ const handleLogin = async () => {
uni.hideLoading(); uni.hideLoading();
console.error('Login failed:', error); console.error('Login failed:', error);
let errorMessage = '登录失败'; // 登录失败
if (error.message) { uni.showToast({
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
}
// 显示详细的错误信息
uni.showModal({
title: '登录失败', title: '登录失败',
content: `错误信息:${errorMessage}\n\n请检查\n1. 网络连接是否正常\n2. 后端服务是否运行\n3. 微信小程序配置是否正确`, icon: 'none',
showCancel: false duration: 2000
}); });
} }
}; };
@@ -339,7 +369,7 @@ const testDirectApiCall = () => {
showCancel: false, showCancel: false,
success: () => { success: () => {
uni.request({ uni.request({
url: 'http://8.145.52.111:8084/app/logout', url: 'https://www.aixsy.com.cn/app/logout',
method: 'POST', method: 'POST',
header: { header: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -376,11 +406,11 @@ Token: ${currentToken || '无'}
showCancel: false, showCancel: false,
success: () => { success: () => {
uni.request({ uni.request({
url: 'http://8.145.52.111:8084/app/logout', url: 'https://www.aixsy.com.cn/app/logout',
method: 'POST', method: 'POST',
header: { header: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': currentToken || '' 'Authorization': currentToken ? (currentToken.startsWith('Bearer ') ? currentToken : 'Bearer ' + currentToken) : ''
}, },
complete: (res) => { complete: (res) => {
uni.showModal({ uni.showModal({
@@ -399,7 +429,7 @@ Token: ${currentToken || '无'}
content: `尝试带token参数的URL`, content: `尝试带token参数的URL`,
showCancel: false, showCancel: false,
success: () => { 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({ uni.request({
url: url, url: url,
method: 'POST', 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> </script>
<style scoped> <style scoped>
.fixed-navbar { .fixed-navbar {
position: fixed; position: fixed;
top: 0; top: 100rpx;
left: 0; left: 0;
width: 100vw; width: 100vw;
z-index: 100; z-index: 100;
} }
.container { .container {
background: #fff9f2; background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
min-height: 100vh; min-height: 100vh;
padding-top: 100rpx; padding-top: 200rpx;
padding-bottom: 120rpx; 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 { .custom-navbar {
height: 100rpx; height: 100rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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-weight: bold;
font-size: 38rpx; font-size: 38rpx;
letter-spacing: 2rpx; letter-spacing: 2rpx;
} }
.navbar-title { .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; display: flex;
align-items: center; align-items: center;
padding: 32rpx; 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 { .avatar {
width: 140rpx; width: 140rpx;
@@ -501,15 +631,18 @@ Token: ${currentToken || '无'}
display: flex; display: flex;
align-items: center; align-items: center;
padding: 40rpx 30rpx; padding: 40rpx 30rpx;
background: #fff; background: rgba(255, 255, 255, 0.9);
border: 2rpx solid rgba(138, 43, 226, 0.3);
border-radius: 20rpx; border-radius: 20rpx;
margin-bottom: 24rpx; 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 { .card-icon {
font-size: 48rpx; font-size: 48rpx;
color: #ff9800; color: #8a2be2;
margin-right: 24rpx; margin-right: 24rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(138, 43, 226, 0.3));
} }
.card-title { .card-title {
font-size: 32rpx; font-size: 32rpx;
@@ -518,11 +651,14 @@ Token: ${currentToken || '无'}
/* 区块样式 */ /* 区块样式 */
.section-block { .section-block {
background: #fff; background: rgba(255, 255, 255, 0.9);
border: 2rpx solid rgba(138, 43, 226, 0.3);
border-radius: 20rpx; border-radius: 20rpx;
margin: 24rpx; margin: 24rpx;
padding: 30rpx; padding: 30rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
box-shadow: 0 6rpx 25rpx rgba(138, 43, 226, 0.15);
backdrop-filter: blur(8px);
} }
.section-header { .section-header {
display: flex; display: flex;
@@ -544,9 +680,17 @@ Token: ${currentToken || '无'}
} }
.logout-btn { .logout-btn {
width: 100%; width: 100%;
background-color: #f5f5f5; background: rgba(255, 255, 255, 0.9);
color: #666; color: #8a2be2;
font-size: 30rpx; 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> </style>

View 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>

View 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>

View File

@@ -101,7 +101,7 @@ export const useUserStore = defineStore('user', () => {
function wxLogin(code, userInfo) { function wxLogin(code, userInfo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.request({ uni.request({
url: 'http://8.145.52.111:8091/app/login', url: 'http://localhost:8091/app/login',
method: 'POST', method: 'POST',
data: { data: {
code code
@@ -148,11 +148,11 @@ export const useUserStore = defineStore('user', () => {
// 尝试调用登出接口 // 尝试调用登出接口
uni.request({ uni.request({
url: 'http://8.145.52.111:8091/app/logout', url: 'https://www.aixsy.com.cn/app/logout',
method: 'POST', method: 'POST',
header: { header: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': currentToken || '' 'Authorization': currentToken ? (currentToken.startsWith('Bearer ') ? currentToken : 'Bearer ' + currentToken) : ''
}, },
data: { data: {
token: currentToken || '' token: currentToken || ''

View File

@@ -1,11 +1,11 @@
// AI角色配置文件 // AI角色配置文件
export const aiCharacters = [ export const aiCharacters = [
{ {
id: 5, id: 9,
name: '萌妹小甜', name: '萌妹陪你聊天',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '可爱活泼,喜欢撒娇', personality: '可爱活泼,喜欢撒娇',
greeting: '你好呀~我是小甜,今天想聊什么呢?', greeting: '你好呀~我是萌妹,今天想聊什么呢?',
voiceStyle: '甜美可爱', voiceStyle: '甜美可爱',
responseStyle: '活泼俏皮,喜欢用表情符号', responseStyle: '活泼俏皮,喜欢用表情符号',
interests: ['美食', '宠物', '音乐', '旅行'], interests: ['美食', '宠物', '音乐', '旅行'],
@@ -18,11 +18,11 @@ export const aiCharacters = [
] ]
}, },
{ {
id: 6, id: 10,
name: '御姐温柔', name: '御姐温柔夜话',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '知性优雅,温柔体贴', personality: '知性优雅,温柔体贴',
greeting: '你好,我是温柔,有什么心事可以和我分享。', greeting: '你好,我是御姐,有什么心事可以和我分享。',
voiceStyle: '温柔知性', voiceStyle: '温柔知性',
responseStyle: '成熟稳重,善解人意', responseStyle: '成熟稳重,善解人意',
interests: ['阅读', '艺术', '哲学', '心理学'], interests: ['阅读', '艺术', '哲学', '心理学'],
@@ -35,11 +35,11 @@ export const aiCharacters = [
] ]
}, },
{ {
id: 7, id: 11,
name: '童真小天使', name: '童真世界探索',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '纯真可爱,充满好奇心', personality: '纯真可爱,充满好奇心',
greeting: '嗨!我是小天使,我们一起探索有趣的世界吧!', greeting: '嗨!我是童真小天使,我们一起探索有趣的世界吧!',
voiceStyle: '天真烂漫', voiceStyle: '天真烂漫',
responseStyle: '充满好奇,喜欢提问', responseStyle: '充满好奇,喜欢提问',
interests: ['游戏', '动画', '童话', '科学'], interests: ['游戏', '动画', '童话', '科学'],
@@ -52,8 +52,8 @@ export const aiCharacters = [
] ]
}, },
{ {
id: 8, id: 12,
name: '贴心男友', name: '温暖男友陪伴',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '温暖体贴,善解人意', personality: '温暖体贴,善解人意',
greeting: '宝贝,今天过得怎么样?有什么想聊的吗?', greeting: '宝贝,今天过得怎么样?有什么想聊的吗?',
@@ -70,7 +70,7 @@ export const aiCharacters = [
}, },
{ {
id: 13, id: 13,
name: '搞笑达人', name: '搞笑达人的日常',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '幽默风趣,善于调节气氛', personality: '幽默风趣,善于调节气氛',
greeting: '哈哈,我是搞笑达人!准备好笑到肚子疼了吗?', greeting: '哈哈,我是搞笑达人!准备好笑到肚子疼了吗?',
@@ -87,10 +87,10 @@ export const aiCharacters = [
}, },
{ {
id: 14, id: 14,
name: '博学者', name: '博学者的深度对话',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '知识渊博,思维深刻', personality: '知识渊博,思维深刻',
greeting: '你好,我是博学者,让我们进行一场深度对话吧。', greeting: '你好,我是博学者,让我们进行一场深度对话吧。',
voiceStyle: '沉稳睿智', voiceStyle: '沉稳睿智',
responseStyle: '引经据典,富有哲理', responseStyle: '引经据典,富有哲理',
interests: ['历史', '文学', '科学', '哲学'], interests: ['历史', '文学', '科学', '哲学'],
@@ -104,10 +104,10 @@ export const aiCharacters = [
}, },
{ {
id: 15, id: 15,
name: '活力健将', name: '运动健将的正能量',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '充满活力,积极向上', personality: '充满活力,积极向上',
greeting: '嘿!我是活力健将,让我们一起充满正能量!', greeting: '嘿!我是运动健将,让我们一起充满正能量!',
voiceStyle: '充满活力', voiceStyle: '充满活力',
responseStyle: '积极向上,充满正能量', responseStyle: '积极向上,充满正能量',
interests: ['运动', '健身', '户外', '挑战'], interests: ['运动', '健身', '户外', '挑战'],
@@ -121,7 +121,7 @@ export const aiCharacters = [
}, },
{ {
id: 16, id: 16,
name: '文艺青年', name: '文艺青年的灵感分享',
avatar: '/static/logo.png', avatar: '/static/logo.png',
personality: '文艺浪漫,富有想象力', personality: '文艺浪漫,富有想象力',
greeting: '你好,我是文艺青年,让我们一起感受生活的美好。', greeting: '你好,我是文艺青年,让我们一起感受生活的美好。',
@@ -145,6 +145,12 @@ export const getCharacterById = (id) => {
// 根据角色性格生成回复 // 根据角色性格生成回复
export const generateResponse = (character, userMessage) => { export const generateResponse = (character, userMessage) => {
// 检查character和sampleResponses是否存在
if (!character || !character.sampleResponses || !Array.isArray(character.sampleResponses)) {
console.warn('角色信息不完整,使用默认回复');
return '抱歉,我现在有点困惑,能再说一遍吗?';
}
const responses = character.sampleResponses; const responses = character.sampleResponses;
const randomIndex = Math.floor(Math.random() * responses.length); const randomIndex = Math.floor(Math.random() * responses.length);
return responses[randomIndex]; return responses[randomIndex];
@@ -152,6 +158,17 @@ export const generateResponse = (character, userMessage) => {
// 根据用户消息内容智能选择回复 // 根据用户消息内容智能选择回复
export const getSmartResponse = (character, userMessage) => { export const getSmartResponse = (character, userMessage) => {
// 检查参数有效性
if (!character) {
console.warn('角色信息为空,使用默认回复');
return '抱歉,我现在有点困惑,能再说一遍吗?';
}
if (!userMessage || typeof userMessage !== 'string') {
console.warn('用户消息无效,使用默认回复');
return '抱歉,我没有听清楚,能再说一遍吗?';
}
const message = userMessage.toLowerCase(); const message = userMessage.toLowerCase();
// 根据关键词匹配不同的回复风格 // 根据关键词匹配不同的回复风格

View File

@@ -1,8 +1,55 @@
// API服务文件 // API服务文件
import { useUserStore } from '@/stores/user.js'; 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')
// 去除图片标记 ![alt](url)
.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 = () => { const checkLoginStatus = () => {
@@ -43,7 +90,7 @@ const request = (options) => {
method: options.method || 'GET', method: options.method || 'GET',
header: { header: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': loginStatus.token || '', 'Authorization': loginStatus.token ? (loginStatus.token.startsWith('Bearer ') ? loginStatus.token : 'Bearer ' + loginStatus.token) : '',
...options.header ...options.header
}, },
data: options.data || {}, data: options.data || {},
@@ -98,15 +145,20 @@ export const chatAPI = {
} }
try { 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({ const response = await request({
url: '/api/chat/sync', url: '/api/chat/sync',
method: 'POST', method: 'POST',
data: { data: requestData
message: params.message,
useFunctionCall: false,
modelId: null, // 使用默认模型
templateId: params.characterId // 使用角色模板ID
}
}); });
console.log('API原始响应:', response); console.log('API原始响应:', response);
@@ -120,8 +172,13 @@ export const chatAPI = {
} }
// 如果响应是对象尝试提取AI回复 // 如果响应是对象尝试提取AI回复
else if (typeof response === 'object' && response !== null) { else if (typeof response === 'object' && response !== null) {
// 优先处理嵌套结构res.data.data.response // 优先处理根级别的response字段根据实际后端响应结构
if (response.data && response.data.data && response.data.data.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; processedResponse = response.data.data.response;
console.log('从嵌套结构 data.data.response 提取回复:', processedResponse); console.log('从嵌套结构 data.data.response 提取回复:', processedResponse);
} }
@@ -131,6 +188,17 @@ export const chatAPI = {
console.log('从字段 data.response 提取回复:', processedResponse); 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) { else if (response.data && response.data.message) {
if (response.data.message === '对话成功') { if (response.data.message === '对话成功') {
// 后端返回成功消息,使用默认回复 // 后端返回成功消息,使用默认回复
@@ -172,9 +240,14 @@ export const chatAPI = {
console.log('未找到有效回复,使用默认回复'); console.log('未找到有效回复,使用默认回复');
} }
// 清理文本,只保留文字和标点符号
const cleanedResponse = cleanText(processedResponse);
console.log('原始回复:', processedResponse);
console.log('清理后回复:', cleanedResponse);
return { return {
success: true, success: true,
data: processedResponse, data: cleanedResponse,
originalResponse: response originalResponse: response
}; };
} catch (error) { } catch (error) {
@@ -369,7 +442,7 @@ export const voiceAPI = {
} }
}, },
// 3. 语音对话 - 完整语音交互流程 // 3. 语音对话 - 完整语音交互流程前端发送aac格式后端转换为wav处理
voiceChat: async (filePath, options = {}) => { voiceChat: async (filePath, options = {}) => {
try { try {
const loginStatus = checkLoginStatus(); const loginStatus = checkLoginStatus();
@@ -384,6 +457,7 @@ export const voiceAPI = {
} }
console.log('开始语音对话,文件路径:', filePath); console.log('开始语音对话,文件路径:', filePath);
console.log('注意前端发送aac格式音频后端需要转换为wav格式进行处理');
// 构建认证头 // 构建认证头
let authHeader = ''; let authHeader = '';
@@ -411,13 +485,55 @@ export const voiceAPI = {
const data = JSON.parse(res.data); const data = JSON.parse(res.data);
console.log('语音对话响应数据:', 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({ resolve({
success: true, success: true,
data: { data: {
userText: data.data.userText || data.data.text, userText: userText,
aiResponse: data.data.aiResponse || data.data.response, aiResponse: cleanedAiResponse,
audioUrl: data.data.audioUrl || data.data.url audioUrl: audioUrl
} }
}); });
} else { } else {
@@ -452,7 +568,7 @@ export const voiceAPI = {
} }
}, },
// 4. 音频文件上传语音对话 // 4. 音频文件上传语音对话前端发送aac格式后端转换为wav处理
uploadVoiceChat: async (filePath, options = {}) => { uploadVoiceChat: async (filePath, options = {}) => {
try { try {
const loginStatus = checkLoginStatus(); const loginStatus = checkLoginStatus();
@@ -467,6 +583,7 @@ export const voiceAPI = {
} }
console.log('开始上传音频文件语音对话,文件路径:', filePath); console.log('开始上传音频文件语音对话,文件路径:', filePath);
console.log('注意前端发送aac格式音频后端需要转换为wav格式进行处理');
// 构建认证头 // 构建认证头
let authHeader = ''; let authHeader = '';
@@ -494,13 +611,55 @@ export const voiceAPI = {
const data = JSON.parse(res.data); const data = JSON.parse(res.data);
console.log('上传音频文件语音对话响应数据:', 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({ resolve({
success: true, success: true,
data: { data: {
userText: data.data.userText || data.data.text, userText: userText,
aiResponse: data.data.aiResponse || data.data.response, aiResponse: cleanedAiResponse,
audioUrl: data.data.audioUrl || data.data.url audioUrl: audioUrl
} }
}); });
} else { } 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; export default request;

211
前端交接文档.md Normal file
View 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
View 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
View 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
View 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>