feat: init
This commit is contained in:
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:x.ant.design)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:antd-design-x-vue.netlify.app)",
|
||||
"Bash(npm install:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
221
CLAUDE.md
Normal file
221
CLAUDE.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **小智AI (Xiaozhi AI)** voice assistant front-end application built with **uni-app (Vue 3)** targeting multiple platforms (H5, WeChat Mini Program, and other mini-program platforms). The app provides AI conversation, voice interaction, script management, voice cloning, and IoT device configuration capabilities.
|
||||
|
||||
**Core Technologies:**
|
||||
- uni-app 3.x with Vue 3 Composition API
|
||||
- Vite 5.2.8 for build tooling
|
||||
- Pinia 2.1.7 for state management
|
||||
- vue-i18n 9.1.9 for internationalization
|
||||
|
||||
**Backend Integration:**
|
||||
- Java Spring Boot backend at `http://localhost:8091` (configurable in `src/utils/api.js`)
|
||||
- Alibaba Cloud voice services for TTS/ASR/voice cloning
|
||||
- WeChat OAuth 2.0 for user authentication
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# H5 development (web browser)
|
||||
npm run dev:h5
|
||||
|
||||
# WeChat Mini Program development
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# Build for H5 production
|
||||
npm run build:h5
|
||||
|
||||
# Build for WeChat Mini Program
|
||||
npm run build:mp-weixin
|
||||
|
||||
# Other platforms (Alipay, Baidu, etc.)
|
||||
npm run dev:mp-[platform] # development
|
||||
npm run build:mp-[platform] # production
|
||||
```
|
||||
|
||||
Build outputs go to `dist/build/h5/` or `dist/build/mp-weixin/` respectively.
|
||||
|
||||
## Architecture & Code Structure
|
||||
|
||||
### Entry Points
|
||||
- **`src/main.js`**: Application bootstrap, creates SSR app and registers Pinia
|
||||
- **`src/App.vue`**: Root component with global lifecycle hooks
|
||||
- **`src/pages.json`**: Page routing, tabBar configuration, and navigation styles (all pages use custom navigation)
|
||||
- **`src/manifest.json`**: Platform-specific configurations (WeChat AppID: `wxff56c34ef9aceb62`)
|
||||
|
||||
### State Management (Pinia)
|
||||
- **`src/stores/user.js`**: User authentication state
|
||||
- Manages `token`, `nickName`, `avatarUrl`, `openid`, `isLoggedIn`, `hasAgreedToTerms`, `hasVisitedMinePage`
|
||||
- Provides `login()`, `logout()`, `setUserInfo()`, `clearFakeLoginData()` methods
|
||||
- Persists state to uni-app local storage (`userInfo`, `custom_token`, `user_token`)
|
||||
- **Important**: Token is checked from multiple sources (store, `custom_token`, `user_token`, `userInfo.token`)
|
||||
|
||||
### API Layer (`src/utils/api.js`)
|
||||
- **Request interceptor**: Automatically adds `Authorization: Bearer <token>` header from multiple token sources
|
||||
- **Response handling**: Smart extraction of backend responses from various nested structures
|
||||
- **Text cleaning**: `cleanText()` function strips HTML/Markdown and keeps only text and punctuation
|
||||
|
||||
**API Modules:**
|
||||
- **`chatAPI`**:
|
||||
- `syncChat()` - Synchronous chat with fallback for anonymous users
|
||||
- `asyncChat()` - Asynchronous chat (if needed)
|
||||
- `getChatHistory()`, `createConversation()` - Conversation management
|
||||
- **Note**: When user is not logged in, returns `isAnonymous: true` to trigger local simulation
|
||||
- **`voiceAPI`**:
|
||||
- `textToSpeech()` - TTS (Text-to-Speech)
|
||||
- `chatWithTTS()` - AI chat + voice synthesis
|
||||
- `voiceChat()` - Complete voice interaction (uploads AAC, backend converts to WAV)
|
||||
- `uploadVoiceChat()` - Audio file upload for voice chat
|
||||
- **Important**: Frontend sends AAC format, backend must convert to WAV for processing
|
||||
- Response parsing handles complex nested structures: `data.llmResult.response`, `data.sttResult.text`, `data.ttsResult.audioPath`
|
||||
- **`rechargeAPI`**: `getUserBalance()`, `createRechargeOrder()`, `getOrderStatus()`, `getRechargeHistory()`
|
||||
- **`roleAPI`**: `getRoles()`, `getRoleById()` - AI character management
|
||||
- **`configAPI`**: `getAllConfigs()`, `getModels()`, `getSTTConfigs()`, `getTemplates()`, `getTTSConfigs()`
|
||||
|
||||
### Page Structure
|
||||
Pages are organized by feature under `src/pages/`:
|
||||
|
||||
**Core Pages:**
|
||||
- `splash/splash` - Launch screen with agreement flow
|
||||
- `index/index` - Homepage with drama characters and AI voices (has tabBar)
|
||||
- `mine/mine` - User profile and settings (has tabBar)
|
||||
- `chat/chat` - AI conversation with text and voice input
|
||||
- `create/create` - Script creation entry
|
||||
- `script/editor` - Script editing interface
|
||||
- `drama/index` - Drama character details
|
||||
- `voice/clone` - Voice cloning feature
|
||||
- `agreement/agreement` - User agreement display
|
||||
- `recharge/recharge` - Recharge/payment
|
||||
- `recharge/history` - Recharge history (with pull-down refresh enabled)
|
||||
|
||||
All pages use `navigationStyle: "custom"` for custom navigation bars.
|
||||
|
||||
### Global Styles
|
||||
- **`src/uni.scss`**: Global SCSS variables and mixins
|
||||
- **`src/styles/`**: Common stylesheets
|
||||
- **`src/static/`**: Static assets (images, icons, etc.)
|
||||
|
||||
### Components
|
||||
- **`src/components/UserAgreement.vue`**: User agreement component
|
||||
- **`src/components/UserAuth.vue`**: User authentication component
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### Authentication Flow
|
||||
1. User triggers login from UI
|
||||
2. `useUserStore.login()` orchestrates:
|
||||
- Call `uni.getUserProfile()` for WeChat user info
|
||||
- Call `uni.login()` to get WeChat `code`
|
||||
- POST to `/app/login` with `code`
|
||||
- Save `token`, `nickName`, `avatarUrl`, `openid` to store and local storage
|
||||
3. All subsequent API calls auto-inject token from store/storage via request interceptor
|
||||
|
||||
### Anonymous User Handling
|
||||
- When not logged in, `chatAPI.syncChat()` returns `isAnonymous: true`
|
||||
- UI layer should implement local simulation/fallback responses
|
||||
- Voice APIs skip backend calls and use degradation handling
|
||||
|
||||
### Voice Interaction Flow
|
||||
1. **Record**: User records voice (AAC format) using `uni.getRecorderManager()`
|
||||
2. **Upload**: Frontend uploads AAC to backend via `voiceAPI.voiceChat()` or `uploadVoiceChat()`
|
||||
3. **Backend Processing**:
|
||||
- Convert AAC to WAV
|
||||
- STT (Speech-to-Text) via Alibaba Cloud ASR
|
||||
- LLM processing for AI response
|
||||
- TTS (Text-to-Speech) via Alibaba Cloud TTS
|
||||
4. **Response**: Backend returns structured response with `sttResult.text`, `llmResult.response`, `ttsResult.audioPath`
|
||||
5. **Playback**: Frontend plays audio using `uni.createInnerAudioContext()`
|
||||
|
||||
### Response Data Extraction Pattern
|
||||
The API layer uses a flexible extraction strategy to handle varying backend response structures:
|
||||
- Try multiple field names: `response`, `message`, `content`, `reply`, `answer`, `text`
|
||||
- Check nested paths: `data.data.response`, `data.response`, root-level `response`
|
||||
- For voice APIs, specifically look for: `data.llmResult.response`, `data.sttResult.text`, `data.ttsResult.audioPath`
|
||||
- Always clean responses with `cleanText()` to remove markup
|
||||
|
||||
## Important Configuration Files
|
||||
|
||||
### `src/pages.json`
|
||||
Defines all pages, tabBar, and global styles. Key settings:
|
||||
- All pages have `navigationStyle: "custom"` (custom nav bars)
|
||||
- TabBar: Homepage (`pages/index/index`) and Mine (`pages/mine/mine`)
|
||||
- `lazyCodeLoading: "requiredComponents"` for performance
|
||||
- Splash and agreement pages have `disableScroll: true` and `popGesture: "none"` (prevent back gesture)
|
||||
|
||||
### `src/manifest.json`
|
||||
Platform-specific configurations:
|
||||
- WeChat Mini Program AppID: `wxff56c34ef9aceb62`
|
||||
- App permissions and capabilities
|
||||
- Platform-specific optimizations
|
||||
|
||||
### `vite.config.js`
|
||||
Minimal Vite config using `@dcloudio/vite-plugin-uni` plugin.
|
||||
|
||||
## Documentation References
|
||||
|
||||
Refer to these files for detailed specifications:
|
||||
- **`API接口文档.md`**: Complete backend API documentation with request/response examples
|
||||
- **`产品设计需求文档.md`**: Product requirements, functional specifications, AI service integration details, IoT device configuration requirements
|
||||
- **`前端交接文档.md`**: Project handoff document with module descriptions and development notes
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Working with uni-app
|
||||
- Use `uni.*` APIs for cross-platform compatibility (e.g., `uni.request`, `uni.showToast`, `uni.navigateTo`)
|
||||
- Test on both H5 and WeChat Mini Program to ensure compatibility
|
||||
- WeChat Mini Program has stricter restrictions (e.g., requires HTTPS for production, domain whitelist)
|
||||
|
||||
### State Management
|
||||
- Always use Pinia store for shared state
|
||||
- Persist critical state (user info, tokens) to local storage via `uni.setStorageSync()`
|
||||
- Initialize stores on app launch by calling store's `init()` method
|
||||
|
||||
### API Integration
|
||||
- Never hardcode API endpoints; use `BASE_URL` in `src/utils/api.js`
|
||||
- Handle both logged-in and anonymous states gracefully
|
||||
- Always check for multiple possible response structures when parsing backend data
|
||||
- Use `cleanText()` for AI-generated text to strip formatting
|
||||
|
||||
### Audio Handling
|
||||
- Record audio using `uni.getRecorderManager()` with AAC format
|
||||
- Play audio using `uni.createInnerAudioContext()`
|
||||
- Always inform backend that AAC will be sent and needs WAV conversion
|
||||
- Cache frequently used audio responses for better performance
|
||||
|
||||
### Error Handling
|
||||
- Display user-friendly error messages using `uni.showToast()`
|
||||
- Log errors to console for debugging
|
||||
- Implement fallback mechanisms for non-critical features when backend is unavailable
|
||||
|
||||
### Performance
|
||||
- Use `lazyCodeLoading: "requiredComponents"` (already configured)
|
||||
- Optimize images and static assets
|
||||
- Implement code splitting for large pages
|
||||
- Cache API responses where appropriate (e.g., config data, role lists)
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### WeChat Mini Program
|
||||
- Requires domain whitelist configuration in WeChat Developer Console
|
||||
- OAuth login flow uses `uni.login({ provider: 'weixin' })`
|
||||
- Recording and playback permissions must be requested from user
|
||||
- Payment integration uses WeChat Pay API
|
||||
|
||||
### H5 (Web)
|
||||
- Can be deployed to any web server
|
||||
- Use browser DevTools for debugging
|
||||
- May need CORS configuration on backend for local development
|
||||
|
||||
## Git Workflow
|
||||
|
||||
Main branch: `master`
|
||||
Current branch: `test`
|
||||
|
||||
When creating PRs, target the `master` branch.
|
||||
@@ -52,6 +52,8 @@
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4060420250429001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4060420250429001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4060420250429001",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"ant-design-x-vue": "^1.3.2",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9"
|
||||
|
||||
24
src/App.vue
24
src/App.vue
@@ -2,18 +2,26 @@
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch');
|
||||
|
||||
// 应用启动时的全局初始化逻辑
|
||||
// 启动页会自动处理倒计时和页面跳转
|
||||
|
||||
// 可以在这里添加其他全局初始化逻辑
|
||||
// 例如:设置全局变量、注册事件监听器等
|
||||
|
||||
// 检查是否是首次启动或需要显示启动页
|
||||
const hasShownSplash = uni.getStorageSync('hasShownSplash');
|
||||
const hasAgreedToTerms = uni.getStorageSync('hasAgreedToTerms');
|
||||
|
||||
// 如果未显示过启动页或未同意协议,则跳转到启动页
|
||||
if (!hasShownSplash || hasAgreedToTerms !== 'true') {
|
||||
// 延迟一下确保页面已加载
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/splash/splash'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onShow: function () {
|
||||
console.log('App Show')
|
||||
},
|
||||
|
||||
|
||||
onHide: function () {
|
||||
console.log('App Hide')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
{
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"pages": [
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/drama/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/chat",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/splash/splash",
|
||||
"style": {
|
||||
@@ -19,12 +42,6 @@
|
||||
"disableScroll": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/create/create",
|
||||
"style": {
|
||||
@@ -37,12 +54,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/drama/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/voice/clone",
|
||||
"style": {
|
||||
@@ -55,16 +66,10 @@
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true,
|
||||
"app-plus": {
|
||||
"popGesture": "none"
|
||||
"popGesture": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/chat",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recharge/recharge",
|
||||
"style": {
|
||||
@@ -92,8 +97,16 @@
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
"pagePath": "pages/drama/index",
|
||||
"text": "发现"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/device/index",
|
||||
"text": "设备"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/chat/chat",
|
||||
"text": "专属"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/mine",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1790
src/pages/chat/chat.vue.backup
Normal file
1790
src/pages/chat/chat.vue.backup
Normal file
File diff suppressed because it is too large
Load Diff
584
src/pages/device/index.vue
Normal file
584
src/pages/device/index.vue
Normal file
@@ -0,0 +1,584 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-navbar fixed-navbar">
|
||||
<view class="navbar-stars">
|
||||
<view class="navbar-star star-left"></view>
|
||||
<view class="navbar-star star-right"></view>
|
||||
</view>
|
||||
<text class="navbar-title">我的设备</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备信息卡片 -->
|
||||
<view class="device-card">
|
||||
<view class="device-header">
|
||||
<view class="device-icon" :class="{'connected': deviceConnected}">
|
||||
{{ deviceConnected ? '🌟' : '⭕' }}
|
||||
</view>
|
||||
<view class="device-status">
|
||||
<text class="status-text">{{ deviceConnected ? '已连接' : '未连接' }}</text>
|
||||
<text class="status-indicator" :class="{'active': deviceConnected}"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="device-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备名称</text>
|
||||
<text class="info-value">{{ deviceName }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备ID</text>
|
||||
<text class="info-value">{{ deviceId }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备SN</text>
|
||||
<text class="info-value">{{ deviceSN }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="deviceConnected">
|
||||
<text class="info-label">连接时间</text>
|
||||
<text class="info-value">{{ connectTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="device-actions">
|
||||
<button
|
||||
class="floral-btn"
|
||||
:class="{'disconnect-btn': deviceConnected}"
|
||||
@click="toggleConnection"
|
||||
>
|
||||
{{ deviceConnected ? '断开连接' : '连接设备' }}
|
||||
</button>
|
||||
<button
|
||||
class="floral-btn outline refresh-btn"
|
||||
@click="refreshDeviceStatus"
|
||||
>
|
||||
🔄 刷新状态
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 未登录提示 -->
|
||||
<view class="login-tip" v-if="!userStore.isLoggedIn">
|
||||
<view class="tip-icon">🔐</view>
|
||||
<text class="tip-text">请先登录以查看设备信息</text>
|
||||
<button class="floral-btn small-btn" @click="goToLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<!-- 设备功能列表 -->
|
||||
<view class="feature-list" v-if="deviceConnected && userStore.isLoggedIn">
|
||||
<view class="feature-title">设备功能</view>
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon">📡</view>
|
||||
<view class="feature-content">
|
||||
<text class="feature-name">远程控制</text>
|
||||
<text class="feature-desc">通过小程序远程控制设备</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon">🎤</view>
|
||||
<view class="feature-content">
|
||||
<text class="feature-name">语音交互</text>
|
||||
<text class="feature-desc">与AI语音助手对话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon">🔔</view>
|
||||
<view class="feature-content">
|
||||
<text class="feature-name">消息通知</text>
|
||||
<text class="feature-desc">接收设备状态通知</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
// 状态管理
|
||||
const userStore = useUserStore();
|
||||
const deviceConnected = ref(false);
|
||||
const deviceName = ref('小智AI设备');
|
||||
const deviceSN = ref('未连接');
|
||||
const deviceId = ref('30:ed:a0:12:99:60');
|
||||
const connectTime = ref('');
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
userStore.init();
|
||||
// refreshDeviceStatus(); // API已注释,不需要自动刷新
|
||||
});
|
||||
|
||||
// 刷新设备状态
|
||||
const refreshDeviceStatus = () => {
|
||||
if (!userStore.isLoggedIn) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 暂时注释掉API请求,使用本地模拟数据
|
||||
uni.showToast({
|
||||
title: '刷新成功(本地模拟)',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
/* API请求已注释
|
||||
uni.showLoading({
|
||||
title: '获取设备状态...'
|
||||
});
|
||||
|
||||
uni.request({
|
||||
url: 'https://www.aixsy.com.cn/api/device/status',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Authorization': userStore.token ? (userStore.token.startsWith('Bearer ') ? userStore.token : 'Bearer ' + userStore.token) : ''
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('设备状态响应:', res);
|
||||
if (res.statusCode === 200 && res.data.data) {
|
||||
const data = res.data.data;
|
||||
deviceConnected.value = data.connected || false;
|
||||
deviceSN.value = data.sn || '未知SN';
|
||||
if (deviceConnected.value) {
|
||||
connectTime.value = data.connectTime || new Date().toLocaleString();
|
||||
}
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取设备状态失败:', err);
|
||||
uni.showToast({
|
||||
title: '刷新失败',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
uni.hideLoading();
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
// 连接/断开设备
|
||||
const toggleConnection = () => {
|
||||
if (!userStore.isLoggedIn) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 暂时注释掉API请求,使用本地模拟数据
|
||||
deviceConnected.value = !deviceConnected.value;
|
||||
|
||||
if (deviceConnected.value) {
|
||||
connectTime.value = new Date().toLocaleString();
|
||||
deviceSN.value = 'XZ' + Math.random().toString(36).substr(2, 8).toUpperCase();
|
||||
uni.showToast({
|
||||
title: '连接成功(本地模拟)',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
connectTime.value = '';
|
||||
deviceSN.value = '未连接';
|
||||
uni.showToast({
|
||||
title: '已断开连接(本地模拟)',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
/* API请求已注释
|
||||
uni.showLoading({
|
||||
title: deviceConnected.value ? '断开连接中...' : '连接设备中...'
|
||||
});
|
||||
|
||||
const token = userStore.token;
|
||||
const requestData = {
|
||||
deviceId: deviceId.value
|
||||
};
|
||||
|
||||
uni.request({
|
||||
url: `https://www.aixsy.com.cn/api/device/${deviceConnected.value ? 'disconnect' : 'connect'}`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? (token.startsWith('Bearer ') ? token : 'Bearer ' + token) : ''
|
||||
},
|
||||
data: requestData,
|
||||
success: (res) => {
|
||||
console.log('设备操作成功:', res);
|
||||
if (res.statusCode === 200) {
|
||||
deviceConnected.value = !deviceConnected.value;
|
||||
|
||||
if (deviceConnected.value) {
|
||||
connectTime.value = new Date().toLocaleString();
|
||||
deviceSN.value = res.data.data?.sn || 'XZ' + Math.random().toString(36).substr(2, 8).toUpperCase();
|
||||
uni.showToast({
|
||||
title: '连接成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
connectTime.value = '';
|
||||
deviceSN.value = '未连接';
|
||||
uni.showToast({
|
||||
title: '已断开连接',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('设备操作失败:', err);
|
||||
uni.showToast({
|
||||
title: deviceConnected.value ? '断开失败' : '连接失败',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
uni.hideLoading();
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
// 去登录
|
||||
const goToLogin = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/mine/mine'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.floral-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
|
||||
padding-bottom: 100rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 夜空装饰 */
|
||||
.night-sky-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 2s infinite;
|
||||
box-shadow: 0 0 10rpx #f9e076;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.star-1 { top: 10%; left: 15%; animation-delay: 0s; }
|
||||
.star-2 { top: 20%; left: 80%; animation-delay: 0.3s; }
|
||||
.star-3 { top: 30%; left: 45%; animation-delay: 0.6s; }
|
||||
.star-4 { top: 50%; left: 25%; animation-delay: 0.9s; }
|
||||
.star-5 { top: 60%; left: 70%; animation-delay: 1.2s; }
|
||||
.star-6 { top: 70%; left: 40%; animation-delay: 1.5s; }
|
||||
.star-7 { top: 15%; left: 60%; animation-delay: 0.4s; }
|
||||
.star-8 { top: 85%; left: 55%; animation-delay: 1.8s; }
|
||||
|
||||
/* 导航栏 */
|
||||
.custom-navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 88rpx;
|
||||
background: rgba(26, 11, 46, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 30rpx;
|
||||
z-index: 1000;
|
||||
border-bottom: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||
}
|
||||
|
||||
.navbar-stars {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navbar-star {
|
||||
position: absolute;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 15rpx #f9e076;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.star-left {
|
||||
top: 50%;
|
||||
left: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.star-right {
|
||||
top: 50%;
|
||||
right: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
text-shadow: 0 0 20rpx rgba(249, 224, 118, 0.5);
|
||||
}
|
||||
|
||||
/* 设备卡片 */
|
||||
.device-card {
|
||||
margin: 40rpx 30rpx;
|
||||
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
|
||||
border-radius: 30rpx;
|
||||
padding: 40rpx;
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.2);
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.device-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 30rpx;
|
||||
border-bottom: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
font-size: 80rpx;
|
||||
margin-right: 30rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.device-icon.connected {
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.device-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: #4caf50;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.device-info {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(249, 224, 118, 0.05);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(249, 224, 118, 0.6);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: #f9e076;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.floral-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 20rpx rgba(249, 224, 118, 0.3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.floral-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 10rpx rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
color: white;
|
||||
box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.outline {
|
||||
background: transparent;
|
||||
color: #f9e076;
|
||||
border: 2rpx solid #f9e076;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
flex: 0 0 auto;
|
||||
width: 180rpx;
|
||||
}
|
||||
|
||||
/* 未登录提示 */
|
||||
.login-tip {
|
||||
margin: 40rpx 30rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
background: rgba(249, 224, 118, 0.05);
|
||||
border-radius: 30rpx;
|
||||
border: 2rpx dashed rgba(249, 224, 118, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(249, 224, 118, 0.8);
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
width: 200rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* 功能列表 */
|
||||
.feature-list {
|
||||
margin: 40rpx 30rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
margin-bottom: 30rpx;
|
||||
padding-left: 20rpx;
|
||||
border-left: 6rpx solid #f9e076;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: linear-gradient(135deg, rgba(249, 224, 118, 0.08) 0%, rgba(249, 224, 118, 0.03) 100%);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.feature-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(249, 224, 118, 0.6);
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<view class="floral-container">
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-navbar fixed-navbar">
|
||||
<view class="navbar-left" @tap="goBack">← 返回</view>
|
||||
<text class="navbar-title">选择剧情</text>
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<view class="navbar-title">剧情角色</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 可滚动的两列卡片区 -->
|
||||
<scroll-view
|
||||
class="scroll-container"
|
||||
:style="{ marginTop: navBarHeight + 'px' }"
|
||||
scroll-y="true"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
@@ -94,6 +96,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
import { roleAPI } from '@/utils/api.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const showLoginModal = ref(false);
|
||||
@@ -102,84 +105,283 @@ const showLoginModal = ref(false);
|
||||
const showDetailModal = ref(false);
|
||||
const selectedItem = ref(null);
|
||||
|
||||
// 数据:剧情列表
|
||||
// 状态栏高度适配
|
||||
const statusBarHeight = ref(0);
|
||||
const navBarHeight = ref(0);
|
||||
|
||||
// 获取系统状态栏高度
|
||||
const getSystemInfo = () => {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
||||
// 导航栏总高度 = 状态栏高度 + 导航栏内容高度(44px)
|
||||
navBarHeight.value = statusBarHeight.value + 44;
|
||||
console.log('状态栏高度:', statusBarHeight.value, '导航栏总高度:', navBarHeight.value);
|
||||
};
|
||||
|
||||
// 数据:剧情列表(使用API请求获取真实数据)
|
||||
const dramaList = ref([]);
|
||||
|
||||
/* ====== 以下为假数据(已注释,保留作为参考) ======
|
||||
const dramaList = ref([
|
||||
{
|
||||
id: 1,
|
||||
cover: '/static/bailing.jpg',
|
||||
tag: '浪漫',
|
||||
title: '白领女友的温柔早安',
|
||||
{
|
||||
id: 1,
|
||||
roleId: 1,
|
||||
roleName: '小何',
|
||||
roleDesc: '台湾女孩,高情商智能助手',
|
||||
avatar: '/static/bailing.jpg',
|
||||
greeting: '你好~我是小何,一个高情商的台湾女孩,很高兴认识你!有什么想聊的吗?'
|
||||
cover: '/static/characters/xiaozhi.png',
|
||||
avatar: '/static/characters/xiaozhi.png',
|
||||
tag: '浪漫',
|
||||
title: '小智AI助手',
|
||||
roleName: '小智AI助手',
|
||||
roleDesc: '你好!我是小智,你的专属AI助手。我可以帮你解答问题、提供建议、陪你聊天。',
|
||||
greeting: '你好!我是小智,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-1',
|
||||
ttsId: 'tts-1',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.7,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cover: '/static/haomen.jpg',
|
||||
tag: '优雅',
|
||||
title: '豪门女主的秘密生活',
|
||||
{
|
||||
id: 2,
|
||||
roleId: 2,
|
||||
roleName: '小于',
|
||||
roleDesc: '咖啡店兼职店员,热情友好',
|
||||
avatar: '/static/haomen.jpg',
|
||||
greeting: '你好!我是小于,咖啡店的新店员,今天想喝点什么吗?'
|
||||
cover: '/static/characters/sakura.png',
|
||||
avatar: '/static/characters/sakura.png',
|
||||
tag: '优雅',
|
||||
title: '元气少女小樱',
|
||||
roleName: '元气少女小樱',
|
||||
roleDesc: '嘿!我是小樱,超级喜欢和大家聊天的元气少女!每天都充满活力。',
|
||||
greeting: '你好!我是小樱,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-2',
|
||||
ttsId: 'tts-2',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.8,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cover: '/static/waimai.jpg',
|
||||
tag: '活泼',
|
||||
title: '外卖小姐姐的贴心问候',
|
||||
{
|
||||
id: 3,
|
||||
roleId: 3,
|
||||
roleName: '李老师',
|
||||
roleDesc: '中学图书馆管理员,温文尔雅',
|
||||
avatar: '/static/waimai.jpg',
|
||||
greeting: '你好,我是李老师,图书馆管理员。有什么想了解的书吗?'
|
||||
cover: '/static/characters/lina.png',
|
||||
avatar: '/static/characters/lina.png',
|
||||
tag: '活泼',
|
||||
title: '温柔大姐姐琳娜',
|
||||
roleName: '温柔大姐姐琳娜',
|
||||
roleDesc: '你好呀,我是琳娜。如果你需要倾诉或者想要一些温暖的陪伴,随时都可以找我。',
|
||||
greeting: '你好!我是琳娜,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-3',
|
||||
ttsId: 'tts-3',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.7,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cover: '/static/tunvlang.jpg',
|
||||
tag: '深情',
|
||||
title: '因爱执著的少女',
|
||||
{
|
||||
id: 4,
|
||||
roleId: 4,
|
||||
roleName: '小何',
|
||||
roleDesc: '另一个小何角色',
|
||||
avatar: '/static/tunvlang.jpg',
|
||||
greeting: '你好~我是小何,很高兴为你服务!'
|
||||
cover: '/static/characters/allen.png',
|
||||
avatar: '/static/characters/allen.png',
|
||||
tag: '深情',
|
||||
title: '知识博士艾伦',
|
||||
roleName: '知识博士艾伦',
|
||||
roleDesc: '大家好,我是艾伦博士。拥有丰富的知识储备,擅长科学、历史、文学等各个领域。',
|
||||
greeting: '你好!我是艾伦博士,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-4',
|
||||
ttsId: 'tts-4',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.6,
|
||||
topP: 0.9
|
||||
},
|
||||
// {
|
||||
// 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: '校园里的美好回忆' },
|
||||
{
|
||||
id: 5,
|
||||
roleId: 5,
|
||||
cover: '/static/characters/ajie.png',
|
||||
avatar: '/static/characters/ajie.png',
|
||||
tag: '温暖',
|
||||
title: '搞笑达人阿杰',
|
||||
roleName: '搞笑达人阿杰',
|
||||
roleDesc: '哈喽!我是阿杰,专业逗乐20年!生活已经够累了,让我用段子和笑话给你的一天增添点乐趣吧。',
|
||||
greeting: '你好!我是阿杰,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-5',
|
||||
ttsId: 'tts-5',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.9,
|
||||
topP: 0.95
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
roleId: 6,
|
||||
cover: '/static/characters/mike.png',
|
||||
avatar: '/static/characters/mike.png',
|
||||
tag: '梦幻',
|
||||
title: '运动教练米克',
|
||||
roleName: '运动教练米克',
|
||||
roleDesc: '嘿!我是米克,你的私人运动教练。想要保持健康、塑造好身材吗?',
|
||||
greeting: '你好!我是米克,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-6',
|
||||
ttsId: 'tts-6',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.7,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
roleId: 7,
|
||||
cover: '/static/characters/lily.png',
|
||||
avatar: '/static/characters/lily.png',
|
||||
tag: '治愈',
|
||||
title: '心理咨询师莉莉',
|
||||
roleName: '心理咨询师莉莉',
|
||||
roleDesc: '你好,我是莉莉。作为一名心理咨询师,我擅长倾听和理解你的内心世界。',
|
||||
greeting: '你好!我是莉莉,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-7',
|
||||
ttsId: 'tts-7',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.7,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
roleId: 8,
|
||||
cover: '/static/characters/meimei.png',
|
||||
avatar: '/static/characters/meimei.png',
|
||||
tag: '青春',
|
||||
title: '美食家小美',
|
||||
roleName: '美食家小美',
|
||||
roleDesc: '嗨!我是小美,一个热爱美食的吃货!从家常菜到高级料理,从中餐到西餐,我都很在行哦。',
|
||||
greeting: '你好!我是小美,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-8',
|
||||
ttsId: 'tts-8',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.8,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
roleId: 9,
|
||||
cover: '/static/characters/andy.png',
|
||||
avatar: '/static/characters/andy.png',
|
||||
tag: '浪漫',
|
||||
title: '旅行达人安迪',
|
||||
roleName: '旅行达人安迪',
|
||||
roleDesc: '你好!我是安迪,走遍世界各地的旅行达人。我可以为你推荐最美的风景。',
|
||||
greeting: '你好!我是安迪,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-9',
|
||||
ttsId: 'tts-9',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.7,
|
||||
topP: 0.9
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
roleId: 10,
|
||||
cover: '/static/characters/code.png',
|
||||
avatar: '/static/characters/code.png',
|
||||
tag: '优雅',
|
||||
title: '程序员小码',
|
||||
roleName: '程序员小码',
|
||||
roleDesc: 'Hello World!我是小码,一名资深程序员。擅长各种编程语言和技术栈。',
|
||||
greeting: 'Hello!我是小码,很高兴认识你!',
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
templateId: 'template-10',
|
||||
ttsId: 'tts-10',
|
||||
sttId: 'stt-1',
|
||||
temperature: 0.6,
|
||||
topP: 0.9
|
||||
}
|
||||
]);
|
||||
====== 假数据结束 ====== */
|
||||
|
||||
// 列数据拆分
|
||||
const leftColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 0));
|
||||
const rightColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 1));
|
||||
|
||||
// 加载角色列表数据
|
||||
const loadDramaList = async () => {
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' });
|
||||
console.log('开始加载角色列表...');
|
||||
|
||||
const result = await roleAPI.getRoles();
|
||||
console.log('API返回结果:', result);
|
||||
|
||||
if (result.success) {
|
||||
// 处理响应数据,兼容多种返回格式
|
||||
let roles = [];
|
||||
|
||||
// 尝试从不同的路径提取角色列表数据
|
||||
if (result.data && result.data.data && result.data.data.list) {
|
||||
// 格式:{ data: { data: { list: [...] } } }
|
||||
roles = result.data.data.list;
|
||||
} else if (result.data && result.data.list) {
|
||||
// 格式:{ data: { list: [...] } }
|
||||
roles = result.data.list;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
// 格式:{ data: [...] }
|
||||
roles = result.data;
|
||||
} else if (result.data && result.data.data && Array.isArray(result.data.data)) {
|
||||
// 格式:{ data: { data: [...] } }
|
||||
roles = result.data.data;
|
||||
}
|
||||
|
||||
console.log('提取的角色数据:', roles);
|
||||
|
||||
// 映射数据到前端需要的格式
|
||||
dramaList.value = roles.map(role => ({
|
||||
id: role.roleId,
|
||||
roleId: role.roleId,
|
||||
cover: role.avatar || '/static/default-avatar.png',
|
||||
avatar: role.avatar || '/static/default-avatar.png',
|
||||
tag: role.tag || '角色',
|
||||
title: role.roleName || '未命名角色',
|
||||
roleName: role.roleName || '未命名角色',
|
||||
roleDesc: role.roleDesc || '',
|
||||
greeting: role.greeting || '你好!很高兴认识你!',
|
||||
modelId: role.modelId || 'gpt-3.5-turbo',
|
||||
modelName: role.modelName || '',
|
||||
modelProvider: role.modelProvider || '',
|
||||
templateId: role.templateId || role.roleId,
|
||||
ttsId: role.ttsId || '',
|
||||
ttsProvider: role.ttsProvider || '',
|
||||
sttId: role.sttId || '',
|
||||
temperature: role.temperature || 0.7,
|
||||
topP: role.topP || 0.9,
|
||||
voiceName: role.voiceName || '',
|
||||
vadEnergyTh: role.vadEnergyTh || 0.01,
|
||||
vadSilenceMs: role.vadSilenceMs || 1200,
|
||||
vadSilenceTh: role.vadSilenceTh || 0.3,
|
||||
vadSpeechTh: role.vadSpeechTh || 0.5
|
||||
}));
|
||||
|
||||
console.log('角色列表加载成功,共', dramaList.value.length, '个角色');
|
||||
} else {
|
||||
console.error('获取角色列表失败:', result.error);
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载角色列表异常:', error);
|
||||
uni.showToast({
|
||||
title: '加载异常,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo(); // 获取系统信息
|
||||
userStore.init();
|
||||
|
||||
// 调用 API 加载角色列表
|
||||
loadDramaList();
|
||||
});
|
||||
|
||||
// 方法
|
||||
@@ -189,22 +391,29 @@ const handleUse = (item) => {
|
||||
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 params = {
|
||||
characterId: item.id,
|
||||
roleId: item.roleId,
|
||||
roleName: item.roleName || item.title,
|
||||
roleDesc: item.roleDesc,
|
||||
avatar: item.avatar || item.cover,
|
||||
greeting: item.greeting,
|
||||
modelId: item.modelId || '', // 模型ID
|
||||
templateId: item.templateId || '', // 模板ID(使用roleId作为templateId)
|
||||
ttsId: item.ttsId || '', // TTS服务ID
|
||||
sttId: item.sttId || '', // STT服务ID
|
||||
temperature: item.temperature || '',
|
||||
topP: item.topP || ''
|
||||
};
|
||||
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key] || '')}`)
|
||||
.join('&');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.navigateTo({ url: `/pages/chat/chat?${queryString}` });
|
||||
};
|
||||
|
||||
const showLoginTip = () => { showLoginModal.value = true; };
|
||||
@@ -212,7 +421,6 @@ const showDetail = (item) => { selectedItem.value = item; showDetailModal.value
|
||||
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>
|
||||
@@ -223,47 +431,41 @@ const goBack = () => { uni.navigateBack(); };
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 120rpx;
|
||||
padding-top: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fixed-navbar {
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 1002;
|
||||
width: 100%;
|
||||
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);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
height: 100rpx;
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
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;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
text-shadow: 0 0 10px rgba(249, 224, 118, 0.5);
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,20 +110,17 @@ const skipToHome = () => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
// 尝试使用reLaunch跳转
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index',
|
||||
|
||||
// 标记已显示过启动页
|
||||
uni.setStorageSync('hasShownSplash', 'true');
|
||||
|
||||
// 跳转到 tabBar 首页(发现页)
|
||||
uni.switchTab({
|
||||
url: '/pages/drama/index',
|
||||
fail: () => {
|
||||
// 如果reLaunch失败,尝试switchTab
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index',
|
||||
fail: () => {
|
||||
// 如果switchTab也失败,使用redirectTo
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
}
|
||||
// 如果 switchTab 失败,使用 reLaunch
|
||||
uni.reLaunch({
|
||||
url: '/pages/drama/index'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,14 +42,14 @@ export const cleanText = (text) => {
|
||||
// 去除行首行尾空格
|
||||
.trim();
|
||||
|
||||
// 去除特殊字符(保留中文、英文、数字、标点符号)
|
||||
cleaned = cleaned.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s\.,;:!?()()【】""''""''、,。!?;:]/g, '');
|
||||
// 去除特殊字符(保留中文、英文、数字、标点符号,以及分隔符 &)
|
||||
cleaned = cleaned.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s\.,;:!?()()【】""''""''、,。!?;:&]/g, '');
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
// 基础配置
|
||||
const BASE_URL = 'http://localhost:8091'; // 根据后端地址调整
|
||||
const BASE_URL = 'http://192.168.3.243:8091'; // 根据后端地址调整
|
||||
|
||||
// 检查用户登录状态
|
||||
const checkLoginStatus = () => {
|
||||
@@ -813,7 +813,7 @@ export const roleAPI = {
|
||||
getRoles: async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/role/query',
|
||||
url: '/app/role/query',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user