首次提交
This commit is contained in:
574
API接口文档.md
Normal file
574
API接口文档.md
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
# 后端API接口文档
|
||||||
|
|
||||||
|
本文档整理了前端项目所需的所有后端API接口。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [用户认证接口](#用户认证接口)
|
||||||
|
2. [剧本管理接口](#剧本管理接口)
|
||||||
|
3. [声音克隆接口](#声音克隆接口)
|
||||||
|
4. [首页内容接口](#首页内容接口)
|
||||||
|
|
||||||
|
## 用户认证接口
|
||||||
|
|
||||||
|
### 1.1 微信登录接口
|
||||||
|
|
||||||
|
**接口说明**: 用于小程序环境下,通过微信授权登录
|
||||||
|
|
||||||
|
**请求URL**: `/api/login/wechat`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| code | string | 是 | 微信登录返回的code |
|
||||||
|
| userInfo | object | 是 | 用户信息对象 |
|
||||||
|
| userInfo.nickName | string | 是 | 用户昵称 |
|
||||||
|
| userInfo.avatarUrl | string | 是 | 用户头像地址 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "xxxxxxx",
|
||||||
|
"openid": "xxxxxxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 用户信息获取接口
|
||||||
|
|
||||||
|
**接口说明**: 获取当前登录用户的详细信息
|
||||||
|
|
||||||
|
**请求URL**: `/api/user/info`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"nickName": "用户昵称",
|
||||||
|
"avatarUrl": "头像URL",
|
||||||
|
"openid": "用户openid",
|
||||||
|
"balance": 100,
|
||||||
|
"vipStatus": true,
|
||||||
|
"vipExpireDate": "2024-12-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 退出登录接口
|
||||||
|
|
||||||
|
**接口说明**: 用户退出登录
|
||||||
|
|
||||||
|
**请求URL**: `/api/user/logout`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "退出成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 剧本管理接口
|
||||||
|
|
||||||
|
### 2.1 保存剧本接口
|
||||||
|
|
||||||
|
**接口说明**: 创建或更新用户剧本
|
||||||
|
|
||||||
|
**请求URL**: `/api/script/save`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | number | 否 | 剧本ID,不传则为新建 |
|
||||||
|
| title | string | 是 | 剧本标题 |
|
||||||
|
| type | string | 是 | 剧本类型 |
|
||||||
|
| content | string | 是 | 剧本内容 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "保存成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "剧本标题",
|
||||||
|
"type": "剧本类型",
|
||||||
|
"createTime": "2023-01-01 12:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 获取剧本列表接口
|
||||||
|
|
||||||
|
**接口说明**: 获取用户创建的剧本列表
|
||||||
|
|
||||||
|
**请求URL**: `/api/script/list`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| page | number | 否 | 页码,默认1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"total": 100,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "剧本标题1",
|
||||||
|
"type": "台词腔",
|
||||||
|
"createTime": "2023-01-01 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "剧本标题2",
|
||||||
|
"type": "人妻",
|
||||||
|
"createTime": "2023-01-02 12:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 获取剧本详情接口
|
||||||
|
|
||||||
|
**接口说明**: 获取单个剧本的详细信息
|
||||||
|
|
||||||
|
**请求URL**: `/api/script/detail`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | number | 是 | 剧本ID |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "剧本标题",
|
||||||
|
"type": "剧本类型",
|
||||||
|
"content": "剧本内容",
|
||||||
|
"createTime": "2023-01-01 12:00:00",
|
||||||
|
"updateTime": "2023-01-01 12:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 删除剧本接口
|
||||||
|
|
||||||
|
**接口说明**: 删除用户创建的剧本
|
||||||
|
|
||||||
|
**请求URL**: `/api/script/delete`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | number | 是 | 剧本ID |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "删除成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 声音克隆接口
|
||||||
|
|
||||||
|
### 3.1 上传声音样本接口
|
||||||
|
|
||||||
|
**接口说明**: 上传用户录制的声音样本
|
||||||
|
|
||||||
|
**请求URL**: `/api/voice/upload`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
| Content-Type | string | 是 | multipart/form-data |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| file | file | 是 | 声音文件,支持mp3、wav格式 |
|
||||||
|
| duration | number | 是 | 音频时长(秒) |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "上传成功",
|
||||||
|
"data": {
|
||||||
|
"sampleId": "sample123",
|
||||||
|
"url": "https://example.com/samples/sample123.mp3",
|
||||||
|
"duration": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 开始声音克隆接口
|
||||||
|
|
||||||
|
**接口说明**: 根据上传的样本开始声音克隆
|
||||||
|
|
||||||
|
**请求URL**: `/api/voice/clone`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| sampleIds | array | 是 | 样本ID数组 |
|
||||||
|
| name | string | 是 | 克隆声音名称 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "克隆任务已提交",
|
||||||
|
"data": {
|
||||||
|
"taskId": "task123",
|
||||||
|
"status": "processing",
|
||||||
|
"estimatedTime": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 获取声音克隆状态接口
|
||||||
|
|
||||||
|
**接口说明**: 查询声音克隆任务状态
|
||||||
|
|
||||||
|
**请求URL**: `/api/voice/status`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| taskId | string | 是 | 克隆任务ID |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"taskId": "task123",
|
||||||
|
"status": "completed", // processing, completed, failed
|
||||||
|
"progress": 100,
|
||||||
|
"result": {
|
||||||
|
"voiceId": "voice123",
|
||||||
|
"name": "我的声音",
|
||||||
|
"previewUrl": "https://example.com/preview/voice123.mp3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 获取克隆声音列表接口
|
||||||
|
|
||||||
|
**接口说明**: 获取用户的声音克隆列表
|
||||||
|
|
||||||
|
**请求URL**: `/api/voice/list`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| page | number | 否 | 页码,默认1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"total": 5,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"voiceId": "voice123",
|
||||||
|
"name": "我的声音1",
|
||||||
|
"previewUrl": "https://example.com/preview/voice123.mp3",
|
||||||
|
"createTime": "2023-01-01 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"voiceId": "voice124",
|
||||||
|
"name": "我的声音2",
|
||||||
|
"previewUrl": "https://example.com/preview/voice124.mp3",
|
||||||
|
"createTime": "2023-01-02 12:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 首页内容接口
|
||||||
|
|
||||||
|
### 4.1 获取剧情角色列表接口
|
||||||
|
|
||||||
|
**接口说明**: 获取首页剧情角色列表
|
||||||
|
|
||||||
|
**请求URL**: `/api/home/drama`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| page | number | 否 | 页码,默认1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"total": 10,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "白领女友的温柔早安",
|
||||||
|
"tag": "台词腔",
|
||||||
|
"cover": "https://example.com/images/drama1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "豪门女主的秘密生活",
|
||||||
|
"tag": "人妻",
|
||||||
|
"cover": "https://example.com/images/drama2.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 获取AI声音列表接口
|
||||||
|
|
||||||
|
**接口说明**: 获取首页AI声音列表
|
||||||
|
|
||||||
|
**请求URL**: `/api/home/voice`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| page | number | 否 | 页码,默认1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"total": 8,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "甜美少女音",
|
||||||
|
"tag": "少女",
|
||||||
|
"cover": "https://example.com/images/voice1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "性感成熟音",
|
||||||
|
"tag": "成熟",
|
||||||
|
"cover": "https://example.com/images/voice2.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 获取角色详情接口
|
||||||
|
|
||||||
|
**接口说明**: 获取角色详细信息
|
||||||
|
|
||||||
|
**请求URL**: `/api/role/detail`
|
||||||
|
|
||||||
|
**请求方式**: GET
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | number | 是 | 角色ID |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "白领女友的温柔早安",
|
||||||
|
"tag": "台词腔",
|
||||||
|
"cover": "https://example.com/images/drama1.jpg",
|
||||||
|
"description": "角色描述信息",
|
||||||
|
"previewUrl": "https://example.com/preview/drama1.mp3",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"title": "脚本1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"title": "脚本2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 使用角色接口
|
||||||
|
|
||||||
|
**接口说明**: 用户使用特定角色
|
||||||
|
|
||||||
|
**请求URL**: `/api/role/use`
|
||||||
|
|
||||||
|
**请求方式**: POST
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Authorization | string | 是 | 用户token,格式为"Bearer {token}" |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| roleId | number | 是 | 角色ID |
|
||||||
|
| scriptId | number | 是 | 脚本ID |
|
||||||
|
|
||||||
|
**响应结果**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"taskId": "task456",
|
||||||
|
"status": "processing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"@dcloudio/uni-mp-weixin": "3.0.0-4060420250429001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-4060420250429001",
|
||||||
"@dcloudio/uni-mp-xhs": "3.0.0-4060420250429001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-4060420250429001",
|
||||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4060420250429001",
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4060420250429001",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
},
|
},
|
||||||
|
|||||||
22
src/App.vue
22
src/App.vue
@@ -1,14 +1,32 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
onLaunch: function () {
|
onLaunch: function () {
|
||||||
console.log('App Launch')
|
console.log('App Launch');
|
||||||
|
|
||||||
|
// 检查本地存储中是否有同意协议的记录
|
||||||
|
try {
|
||||||
|
const hasAgreed = uni.getStorageSync('hasAgreedToTerms');
|
||||||
|
// 如果没有同意过,显示协议页面
|
||||||
|
if (hasAgreed !== 'true') {
|
||||||
|
// 使用setTimeout避免可能的导航冲突
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/agreement/agreement'
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Check agreement status error:', e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow: function () {
|
onShow: function () {
|
||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
},
|
},
|
||||||
|
|
||||||
onHide: function () {
|
onHide: function () {
|
||||||
console.log('App Hide')
|
console.log('App Hide')
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
168
src/components/UserAgreement.vue
Normal file
168
src/components/UserAgreement.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<view class="agreement-modal" v-if="visible">
|
||||||
|
<view class="agreement-content">
|
||||||
|
<view class="agreement-header">
|
||||||
|
<text class="agreement-title">用户须知</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="agreement-body" scroll-y show-scrollbar="true">
|
||||||
|
<view class="agreement-inner">
|
||||||
|
<view class="agreement-section">
|
||||||
|
<view class="section-title">1 年龄与身份</view>
|
||||||
|
<text class="section-item">1.1 本人已年满 18 周岁,具备完全民事行为能力;</text>
|
||||||
|
<text class="section-item">1.2 本人承诺不得让未成年人接触或使用本平台内容及成人用品;</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<view class="section-title">2 AIGC 生成规范</view>
|
||||||
|
<text class="section-item">2.1 不得生成、发布或传播淫秽色情(含未成年人或性交易)、赌博、暴力恐怖、歧视仇恨、危害国家安全、违法诈骗等信息;</text>
|
||||||
|
<text class="section-item">2.2 不得输出侵犯知识产权、肖像权、隐私权或其他合法权益的内容;</text>
|
||||||
|
<text class="section-item">2.3 应自行甄别生成内容的真实性与准确性,不得用生成内容冒充官方信息或误导他人;</text>
|
||||||
|
<text class="section-item">2.4 不得上传或指令模型生成他人身份证号、联系方式、人脸图像等可识别个人信息,除非已获得合法授权;</text>
|
||||||
|
<text class="section-item highlight-item">2.5 不得尝试逆向工程、导出模型权重或利用注入攻击规避审查;</text>
|
||||||
|
<text class="section-item highlight-item">2.6 发现输出违规或存在安全隐患时,应立即停止使用并通过平台渠道反馈;</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<view class="section-title">3 其他遵守事项</view>
|
||||||
|
<text class="section-item">3.1 成人用品仅供个人私密场景使用,应按说明书清洁、消毒并适度使用,如有疾病史应先咨询医生;</text>
|
||||||
|
<text class="section-item">3.2 违反本须知或《AIGC 平台及成人用品中国境内用户使用规范》将导致警告、限制功能、封禁账号或配合监管调查;</text>
|
||||||
|
<text class="section-item">3.3 若不同意本须知,请立即退出并停止使用本平台及相关产品。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="agreement-footer">
|
||||||
|
<button class="btn-disagree" @click="handleDisagree">暂不进入</button>
|
||||||
|
<button class="btn-agree" @click="handleAgree">确定</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['agree', 'disagree']);
|
||||||
|
|
||||||
|
// 处理同意事件
|
||||||
|
const handleAgree = () => {
|
||||||
|
emit('agree');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理不同意事件
|
||||||
|
const handleDisagree = () => {
|
||||||
|
emit('disagree');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.agreement-modal {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 600rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 88vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-header {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1rpx solid #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-body {
|
||||||
|
max-height: 70vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-inner {
|
||||||
|
padding: 20rpx 35rpx 20rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-section {
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
color: #333333;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
letter-spacing: 3.5rpx;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
text-justify: inter-character;
|
||||||
|
max-width: 520rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-item {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-footer {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1rpx solid #EEEEEE;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disagree, .btn-agree {
|
||||||
|
flex: 1;
|
||||||
|
height: 86rpx;
|
||||||
|
line-height: 86rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disagree {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #999999;
|
||||||
|
border-right: 1rpx solid #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-agree {
|
||||||
|
background: #FF9800;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
src/components/UserAuth.vue
Normal file
90
src/components/UserAuth.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<view class="user-auth">
|
||||||
|
<button
|
||||||
|
v-if="!userStore.isLoggedIn"
|
||||||
|
class="auth-btn"
|
||||||
|
:class="[type]"
|
||||||
|
open-type="getUserProfile"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
<slot>{{ btnText }}</slot>
|
||||||
|
</button>
|
||||||
|
<slot v-else name="logged"></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
// Props 定义
|
||||||
|
const props = defineProps({
|
||||||
|
// 按钮类型: primary, default, plain
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary'
|
||||||
|
},
|
||||||
|
// 按钮文本
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '微信一键登录'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事件
|
||||||
|
const emits = defineEmits(['login-success', 'login-fail']);
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const btnText = computed(() => props.text);
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await userStore.login();
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
emits('login-success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
emits('login-fail', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-auth {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
border: none;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
line-height: 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plain {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #07c160;
|
||||||
|
border: 1px solid #07c160;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
createSSRApp
|
createSSRApp
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app = createSSRApp(App);
|
const app = createSSRApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
app.use(pinia);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
app,
|
app,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
{
|
{
|
||||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "uni-app"
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/mine",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/create/create",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/script/editor",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/voice/clone",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/agreement/agreement",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"app-plus": {
|
||||||
|
"popGesture": "none"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -12,5 +46,25 @@
|
|||||||
"navigationBarTitleText": "uni-app",
|
"navigationBarTitleText": "uni-app",
|
||||||
"navigationBarBackgroundColor": "#F8F8F8",
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
|
},
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#999999",
|
||||||
|
"selectedColor": "#ff9800",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"iconPath": "static/tabbar/home.png",
|
||||||
|
"selectedIconPath": "static/tabbar/home_selected.png",
|
||||||
|
"text": "首页"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/mine/mine",
|
||||||
|
"iconPath": "static/tabbar/mine.png",
|
||||||
|
"selectedIconPath": "static/tabbar/mine_selected.png",
|
||||||
|
"text": "我的"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/pages/agreement/agreement.vue
Normal file
59
src/pages/agreement/agreement.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<user-agreement
|
||||||
|
:visible="true"
|
||||||
|
@agree="handleAgree"
|
||||||
|
@disagree="handleDisagree"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import UserAgreement from '@/components/UserAgreement.vue';
|
||||||
|
|
||||||
|
// 处理同意事件
|
||||||
|
const handleAgree = () => {
|
||||||
|
try {
|
||||||
|
// 保存到本地存储
|
||||||
|
uni.setStorageSync('hasAgreedToTerms', 'true');
|
||||||
|
|
||||||
|
// 返回上一页或首页
|
||||||
|
uni.navigateBack({
|
||||||
|
fail: () => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save agreement status error:', e);
|
||||||
|
// 发生错误时也返回首页
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理不同意事件
|
||||||
|
const handleDisagree = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '您需要同意用户须知才能继续使用本应用',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
// H5环境下返回首页
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
155
src/pages/create/create.vue
Normal file
155
src/pages/create/create.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 顶部自定义导航栏 -->
|
||||||
|
<view class="custom-navbar fixed-navbar">
|
||||||
|
<view class="navbar-left" @click="goBack">
|
||||||
|
<text class="back-icon">〈</text>
|
||||||
|
</view>
|
||||||
|
<text class="navbar-title">创建</text>
|
||||||
|
<view class="navbar-right"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 创建选项区域 -->
|
||||||
|
<view class="create-options">
|
||||||
|
<view class="option-card" @click="goToScriptEditor">
|
||||||
|
<view class="option-icon">📝</view>
|
||||||
|
<view class="option-content">
|
||||||
|
<text class="option-title">新建剧本</text>
|
||||||
|
<text class="option-desc">创建自定义剧本和对话</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="option-card" @click="goToVoiceClone">
|
||||||
|
<view class="option-icon">🎙️</view>
|
||||||
|
<view class="option-content">
|
||||||
|
<text class="option-title">克隆声音</text>
|
||||||
|
<text class="option-desc">定制个性化语音</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
// 初始化用户状态
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到剧本编辑页
|
||||||
|
const goToScriptEditor = () => {
|
||||||
|
// 检查是否已登录
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
showLoginTip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/script/editor'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到声音克隆页
|
||||||
|
const goToVoiceClone = () => {
|
||||||
|
// 检查是否已登录
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
showLoginTip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/voice/clone'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示登录提示
|
||||||
|
const showLoginTip = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先登录后再操作',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mine/mine'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fixed-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: #fff9f2;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 100rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.custom-navbar {
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
.navbar-left, .navbar-right {
|
||||||
|
width: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.navbar-title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 38rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 创建选项区域 */
|
||||||
|
.create-options {
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
}
|
||||||
|
.option-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.option-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
margin-right: 30rpx;
|
||||||
|
}
|
||||||
|
.option-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.option-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
.option-desc {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,48 +1,320 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="content">
|
<view class="container">
|
||||||
<image class="logo" src="/static/logo.png"></image>
|
<!-- 顶部自定义导航栏,固定在最上方 -->
|
||||||
<view class="text-area">
|
<view class="custom-navbar fixed-navbar">
|
||||||
<text class="title">{{ title }}</text>
|
<text class="navbar-title">TEST玩具</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 卡片区 -->
|
||||||
|
<view class="card-row">
|
||||||
|
<view class="card create-card" @click="goToCreate">
|
||||||
|
<view class="card-icon create-icon">+</view>
|
||||||
|
<text class="card-title">创建</text>
|
||||||
|
</view>
|
||||||
|
<view class="card connect-card">
|
||||||
|
<view class="card-icon connect-icon">🌐</view>
|
||||||
|
<text class="card-title bold">连接设备</text>
|
||||||
|
<text class="card-sn">SN: xxxxxxxx</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 二级Tab -->
|
||||||
|
<view class="tab-row">
|
||||||
|
<view :class="['tab-btn', tabActive === 'drama' ? 'active' : '']" @tap="tabActive = 'drama'">剧情</view>
|
||||||
|
<view :class="['tab-btn', tabActive === 'chat' ? 'active' : '']" @tap="tabActive = 'chat'">聊天</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 瀑布流卡片区 -->
|
||||||
|
<view class="waterfall">
|
||||||
|
<block v-for="(item, idx) in currentList" :key="item.id">
|
||||||
|
<view class="waterfall-card">
|
||||||
|
<image class="cover" :src="item.cover" mode="aspectFill" />
|
||||||
|
<view class="tag">{{ item.tag }}</view>
|
||||||
|
<view class="content-area">
|
||||||
|
<view class="title">{{ item.title }}</view>
|
||||||
|
<view class="card-bottom">
|
||||||
|
<button v-if="userStore.isLoggedIn" class="use-btn" @click="handleUse(item)">去使用</button>
|
||||||
|
<button v-else class="use-btn login-required" @click="showLoginTip">去使用</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录提示弹窗 -->
|
||||||
|
<view class="login-modal" v-if="showLoginModal">
|
||||||
|
<view class="login-modal-content">
|
||||||
|
<view class="login-modal-title">提示</view>
|
||||||
|
<view class="login-modal-text">请先登录后再操作</view>
|
||||||
|
<view class="login-modal-btns">
|
||||||
|
<button class="modal-btn cancel" @click="showLoginModal = false">取消</button>
|
||||||
|
<button class="modal-btn confirm" @click="goToLogin">去登录</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, computed, onMounted } from 'vue';
|
||||||
data() {
|
import { useUserStore } from '@/stores/user.js';
|
||||||
return {
|
|
||||||
title: 'Hello',
|
// 状态管理
|
||||||
}
|
const userStore = useUserStore();
|
||||||
},
|
const tabActive = ref('drama');
|
||||||
onLoad() {},
|
const showLoginModal = ref(false);
|
||||||
methods: {},
|
|
||||||
}
|
// 数据
|
||||||
|
const dramaList = ref([
|
||||||
|
{ id: 1, cover: '/static/drama1.jpg', tag: '台词腔', title: '白领女友的温柔早安' },
|
||||||
|
{ id: 2, cover: '/static/drama2.jpg', tag: '人妻', title: '豪门女主的秘密生活' },
|
||||||
|
{ id: 3, cover: '/static/drama3.jpg', tag: '外卖媛', title: '外卖小姐姐的贴心问候' },
|
||||||
|
{ id: 4, cover: '/static/drama4.jpg', tag: '病娇', title: '因爱执仗的少女' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chatList = ref([
|
||||||
|
{ id: 5, cover: '/static/chat1.jpg', tag: '萌妹', title: '可爱萌妹陪你聊' },
|
||||||
|
{ id: 6, cover: '/static/chat2.jpg', tag: '御姐', title: '知性御姐的温柔夜话' },
|
||||||
|
{ id: 7, cover: '/static/chat3.jpg', tag: '萝莉', title: '萝莉的童真世界' },
|
||||||
|
{ id: 8, cover: '/static/chat4.jpg', tag: '男友', title: '贴心男友的陪伴' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const currentList = computed(() => {
|
||||||
|
return tabActive.value === 'drama' ? dramaList.value : chatList.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
userStore.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const handleUse = (item) => {
|
||||||
|
console.log('使用角色:', item);
|
||||||
|
uni.showToast({
|
||||||
|
title: `正在加载: ${item.title}`,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
// 这里添加跳转到详情页或使用页面的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLoginTip = () => {
|
||||||
|
showLoginModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
showLoginModal.value = false;
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mine/mine'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToCreate = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/create/create'
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.content {
|
.fixed-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
padding-top: 100rpx;
|
||||||
|
}
|
||||||
|
.custom-navbar {
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 38rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
.navbar-title {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.card-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 32rpx 24rpx 0 24rpx;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx #f5e6d6;
|
||||||
|
margin: 0 8rpx;
|
||||||
|
padding: 24rpx 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
.create-icon {
|
||||||
.logo {
|
font-size: 48rpx;
|
||||||
height: 200rpx;
|
color: #ff9800;
|
||||||
width: 200rpx;
|
|
||||||
margin-top: 200rpx;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 50rpx;
|
|
||||||
}
|
}
|
||||||
|
.connect-icon {
|
||||||
.text-area {
|
font-size: 40rpx;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.card-sn {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #bbb;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
.tab-row {
|
||||||
|
display: flex;
|
||||||
|
margin: 32rpx 24rpx 0 24rpx;
|
||||||
|
border-bottom: 2rpx solid #f5e6d6;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
color: #bbb;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 4rpx solid transparent;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #ff9800;
|
||||||
|
border-bottom: 4rpx solid #ff9800;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.waterfall {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 24rpx 12rpx 0 12rpx;
|
||||||
|
}
|
||||||
|
.waterfall-card {
|
||||||
|
width: 46%;
|
||||||
|
margin: 2%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx #f5e6d6;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 220rpx;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 18rpx 18rpx 0 0;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
left: 12rpx;
|
||||||
|
background: #ff9800;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.content-area {
|
||||||
|
padding: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.card-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
.use-btn {
|
||||||
|
background: #ff9800;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 8rpx 36rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.login-required {
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
/* 登录提示弹窗 */
|
||||||
font-size: 36rpx;
|
.login-modal {
|
||||||
color: #8f8f94;
|
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: 999;
|
||||||
|
}
|
||||||
|
.login-modal-content {
|
||||||
|
width: 580rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 40rpx 0 0 0;
|
||||||
|
}
|
||||||
|
.login-modal-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
.login-modal-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
}
|
||||||
|
.login-modal-btns {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
.modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.modal-btn.cancel {
|
||||||
|
color: #999;
|
||||||
|
border-right: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
.modal-btn.confirm {
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
368
src/pages/mine/mine.vue
Normal file
368
src/pages/mine/mine.vue
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 顶部自定义导航栏 -->
|
||||||
|
<view class="custom-navbar fixed-navbar">
|
||||||
|
<text class="navbar-title">我的</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户信息区域 -->
|
||||||
|
<view class="user-info">
|
||||||
|
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
|
||||||
|
<view class="user-details" v-if="isLoggedIn">
|
||||||
|
<text class="login-text">{{ nickName }}</text>
|
||||||
|
<text class="user-id">ID: {{ openid }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="user-details" v-else>
|
||||||
|
<text class="login-text">请登录</text>
|
||||||
|
<text class="user-id">ID:</text>
|
||||||
|
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 菜单卡片 -->
|
||||||
|
<view class="menu-cards">
|
||||||
|
<!-- 用户须知 -->
|
||||||
|
<view class="menu-card" @click="showAgreement">
|
||||||
|
<view class="card-icon notice-icon">🔊</view>
|
||||||
|
<text class="card-title">用户须知</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户设备 -->
|
||||||
|
<view class="menu-card">
|
||||||
|
<view class="card-icon device-icon">📦</view>
|
||||||
|
<text class="card-title">用户设备</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 剧情角色区块 -->
|
||||||
|
<view class="section-block">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">剧情角色</text>
|
||||||
|
<text class="view-all">查看全部</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 克隆声音区块 -->
|
||||||
|
<view class="section-block">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">克隆声音</text>
|
||||||
|
<text class="view-all">查看全部</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录按钮,仅登录后显示 -->
|
||||||
|
<view class="logout-section" v-if="isLoggedIn">
|
||||||
|
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户须知弹窗 -->
|
||||||
|
<user-agreement
|
||||||
|
:visible="showAgreementModal"
|
||||||
|
@agree="handleAgreeTerms"
|
||||||
|
@disagree="handleDisagreeTerms"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
import UserAgreement from '@/components/UserAgreement.vue';
|
||||||
|
|
||||||
|
// 状态管理 - 避免直接依赖store的响应式
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const showAgreementModal = ref(false);
|
||||||
|
|
||||||
|
// 本地状态,避免直接引用store中的响应式状态
|
||||||
|
const isLoggedIn = ref(false);
|
||||||
|
const nickName = ref('');
|
||||||
|
const openid = ref('');
|
||||||
|
const avatarUrl = ref('/static/default-avatar.png');
|
||||||
|
|
||||||
|
// 登录按钮文本
|
||||||
|
const loginButtonText = computed(() => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return '微信一键登录';
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
return '立即登录';
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return '一键登录';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
const initUserInfo = () => {
|
||||||
|
try {
|
||||||
|
// 先尝试加载本地存储的用户信息
|
||||||
|
const userInfo = uni.getStorageSync('userInfo');
|
||||||
|
if (userInfo) {
|
||||||
|
try {
|
||||||
|
const parsedInfo = JSON.parse(userInfo);
|
||||||
|
nickName.value = parsedInfo.nickName || '';
|
||||||
|
avatarUrl.value = parsedInfo.avatarUrl || '/static/default-avatar.png';
|
||||||
|
openid.value = parsedInfo.openid || '';
|
||||||
|
isLoggedIn.value = !!parsedInfo.token;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse user info error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Load user info error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化用户信息
|
||||||
|
initUserInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = () => {
|
||||||
|
// H5环境
|
||||||
|
// #ifdef H5
|
||||||
|
nickName.value = 'H5测试用户';
|
||||||
|
avatarUrl.value = '/static/default-avatar.png';
|
||||||
|
openid.value = 'h5-mock-openid';
|
||||||
|
isLoggedIn.value = true;
|
||||||
|
|
||||||
|
// 保存到本地
|
||||||
|
const userInfo = {
|
||||||
|
token: 'h5-mock-token',
|
||||||
|
nickName: nickName.value,
|
||||||
|
avatarUrl: avatarUrl.value,
|
||||||
|
openid: openid.value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.setStorageSync('userInfo', JSON.stringify(userInfo));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save user info error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 微信环境
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.getUserProfile({
|
||||||
|
desc: '用于完善用户资料',
|
||||||
|
success: (res) => {
|
||||||
|
const userInfo = res.userInfo;
|
||||||
|
nickName.value = userInfo.nickName;
|
||||||
|
avatarUrl.value = userInfo.avatarUrl;
|
||||||
|
openid.value = 'wx-' + Date.now();
|
||||||
|
isLoggedIn.value = true;
|
||||||
|
|
||||||
|
// 保存到本地
|
||||||
|
const storeInfo = {
|
||||||
|
token: 'wx-mock-token',
|
||||||
|
nickName: nickName.value,
|
||||||
|
avatarUrl: avatarUrl.value,
|
||||||
|
openid: openid.value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.setStorageSync('userInfo', JSON.stringify(storeInfo));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save user info error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('Login failed:', err);
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync('userInfo');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Remove user info error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
nickName.value = '';
|
||||||
|
avatarUrl.value = '/static/default-avatar.png';
|
||||||
|
openid.value = '';
|
||||||
|
isLoggedIn.value = false;
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已退出登录',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示用户须知
|
||||||
|
const showAgreement = () => {
|
||||||
|
showAgreementModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理同意用户须知
|
||||||
|
const handleAgreeTerms = () => {
|
||||||
|
showAgreementModal.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置本地存储
|
||||||
|
uni.setStorageSync('hasAgreedToTerms', 'true');
|
||||||
|
uni.setStorageSync('hasVisitedMinePage', 'true');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save agreement status error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理不同意用户须知
|
||||||
|
const handleDisagreeTerms = () => {
|
||||||
|
showAgreementModal.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fixed-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: #fff9f2;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 100rpx;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
}
|
||||||
|
.custom-navbar {
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff9f2;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 38rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
.navbar-title {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息区域 */
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: #fff9f2;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.user-details {
|
||||||
|
margin-left: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.login-text {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
.user-id {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.login-btn {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
width: 240rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
line-height: 70rpx;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单卡片 */
|
||||||
|
.menu-cards {
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
.menu-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #ff9800;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 区块样式 */
|
||||||
|
.section-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin: 24rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.view-all {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出登录按钮 */
|
||||||
|
.logout-section {
|
||||||
|
padding: 40rpx 24rpx;
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
font-size: 30rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
180
src/pages/script/editor.vue
Normal file
180
src/pages/script/editor.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 顶部自定义导航栏 -->
|
||||||
|
<view class="custom-navbar fixed-navbar">
|
||||||
|
<view class="navbar-left" @click="goBack">
|
||||||
|
<text class="back-icon">〈</text>
|
||||||
|
</view>
|
||||||
|
<text class="navbar-title">新建剧本</text>
|
||||||
|
<view class="navbar-right"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 脚本编辑器区域 -->
|
||||||
|
<view class="editor-content">
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">剧本标题</text>
|
||||||
|
<input type="text" class="form-input" v-model="scriptTitle" placeholder="请输入剧本标题" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">剧本类型</text>
|
||||||
|
<picker mode="selector" :range="scriptTypes" @change="handleTypeChange" class="form-picker">
|
||||||
|
<view class="picker-value">{{ scriptTypes[scriptTypeIndex] }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">剧本内容</text>
|
||||||
|
<textarea class="form-textarea" v-model="scriptContent" placeholder="请输入剧本内容" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="save-btn" @click="saveScript">保存剧本</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const scriptTitle = ref('');
|
||||||
|
const scriptTypeIndex = ref(0);
|
||||||
|
const scriptTypes = ['台词腔', '人妻', '外卖媛', '病娇', '其他'];
|
||||||
|
const scriptContent = ref('');
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理类型选择
|
||||||
|
const handleTypeChange = (e) => {
|
||||||
|
scriptTypeIndex.value = e.detail.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存剧本
|
||||||
|
const saveScript = () => {
|
||||||
|
// 验证输入
|
||||||
|
if (!scriptTitle.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入剧本标题',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptContent.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入剧本内容',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里添加保存逻辑
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟返回
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fixed-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: #fff9f2;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 100rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.custom-navbar {
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
.navbar-left, .navbar-right {
|
||||||
|
width: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.navbar-title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 38rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器样式 */
|
||||||
|
.editor-content {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.form-picker {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.picker-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 400rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
margin-top: 50rpx;
|
||||||
|
background: #ff9800;
|
||||||
|
color: #fff;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
border-radius: 45rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
338
src/pages/voice/clone.vue
Normal file
338
src/pages/voice/clone.vue
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 顶部自定义导航栏 -->
|
||||||
|
<view class="custom-navbar fixed-navbar">
|
||||||
|
<view class="navbar-left" @click="goBack">
|
||||||
|
<text class="back-icon">〈</text>
|
||||||
|
</view>
|
||||||
|
<text class="navbar-title">克隆声音</text>
|
||||||
|
<view class="navbar-right"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 声音克隆内容 -->
|
||||||
|
<view class="voice-content">
|
||||||
|
<view class="section-title">上传声音样本</view>
|
||||||
|
|
||||||
|
<!-- 录音区域 -->
|
||||||
|
<view class="voice-card">
|
||||||
|
<view class="voice-action">
|
||||||
|
<view class="action-icon" @click="toggleRecord">
|
||||||
|
<text v-if="!isRecording">🎙️</text>
|
||||||
|
<text v-else>⏹️</text>
|
||||||
|
</view>
|
||||||
|
<text class="action-text">{{ isRecording ? '停止录音' : '开始录音' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="voice-status">
|
||||||
|
<text>{{ recordStatus }}</text>
|
||||||
|
<text v-if="recordDuration > 0" class="duration">{{ recordDuration }}s</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 上传录音文件 -->
|
||||||
|
<view class="upload-card">
|
||||||
|
<view class="upload-icon" @click="uploadVoiceFile">+</view>
|
||||||
|
<text class="upload-text">上传音频文件</text>
|
||||||
|
<text class="upload-desc">.mp3, .wav, 最大 10MB</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 声音样本列表 -->
|
||||||
|
<view class="sample-list" v-if="voiceSamples.length > 0">
|
||||||
|
<view class="section-title">已上传的样本</view>
|
||||||
|
<view class="sample-item" v-for="(sample, index) in voiceSamples" :key="index">
|
||||||
|
<view class="sample-info">
|
||||||
|
<text class="sample-name">样本{{ index + 1 }}</text>
|
||||||
|
<text class="sample-duration">{{ sample.duration }}s</text>
|
||||||
|
</view>
|
||||||
|
<view class="sample-actions">
|
||||||
|
<text class="play-icon" @click="playSample(index)">▶️</text>
|
||||||
|
<text class="delete-icon" @click="deleteSample(index)">🗑️</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<button class="submit-btn" @click="submitVoiceClone" :disabled="voiceSamples.length === 0">
|
||||||
|
{{ voiceSamples.length === 0 ? '请先上传声音样本' : '开始克隆声音' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const isRecording = ref(false);
|
||||||
|
const recordStatus = ref('准备录音');
|
||||||
|
const recordDuration = ref(0);
|
||||||
|
const voiceSamples = ref([]);
|
||||||
|
let recordTimer = null;
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换录音状态
|
||||||
|
const toggleRecord = () => {
|
||||||
|
if (isRecording.value) {
|
||||||
|
stopRecord();
|
||||||
|
} else {
|
||||||
|
startRecord();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始录音
|
||||||
|
const startRecord = () => {
|
||||||
|
isRecording.value = true;
|
||||||
|
recordStatus.value = '正在录音...';
|
||||||
|
recordDuration.value = 0;
|
||||||
|
|
||||||
|
// 模拟录音计时
|
||||||
|
recordTimer = setInterval(() => {
|
||||||
|
recordDuration.value++;
|
||||||
|
// 最多录制60秒
|
||||||
|
if (recordDuration.value >= 60) {
|
||||||
|
stopRecord();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 这里添加实际录音的API调用
|
||||||
|
uni.showToast({
|
||||||
|
title: '开始录音',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止录音
|
||||||
|
const stopRecord = () => {
|
||||||
|
isRecording.value = false;
|
||||||
|
recordStatus.value = '录音已完成';
|
||||||
|
|
||||||
|
clearInterval(recordTimer);
|
||||||
|
|
||||||
|
// 模拟添加录音样本
|
||||||
|
voiceSamples.value.push({
|
||||||
|
duration: recordDuration.value,
|
||||||
|
url: 'sample.mp3'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 这里添加实际停止录音API调用
|
||||||
|
uni.showToast({
|
||||||
|
title: '录音已保存',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传音频文件
|
||||||
|
const uploadVoiceFile = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '上传功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟添加上传样本
|
||||||
|
setTimeout(() => {
|
||||||
|
voiceSamples.value.push({
|
||||||
|
duration: 25,
|
||||||
|
url: 'upload.mp3'
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 播放样本
|
||||||
|
const playSample = (index) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: `播放样本${index + 1}`,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除样本
|
||||||
|
const deleteSample = (index) => {
|
||||||
|
voiceSamples.value.splice(index, 1);
|
||||||
|
uni.showToast({
|
||||||
|
title: '样本已删除',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交声音克隆
|
||||||
|
const submitVoiceClone = () => {
|
||||||
|
if (voiceSamples.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '处理中...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟处理时间
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({
|
||||||
|
title: '声音克隆成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟返回
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fixed-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: #fff9f2;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 100rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.custom-navbar {
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
.navbar-left, .navbar-right {
|
||||||
|
width: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.navbar-title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 38rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 声音克隆样式 */
|
||||||
|
.voice-content {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
}
|
||||||
|
.voice-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.voice-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.action-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.action-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.voice-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
.duration {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
.upload-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.upload-icon {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 60rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.upload-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
.upload-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.sample-list {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
.sample-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.sample-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 5rpx;
|
||||||
|
}
|
||||||
|
.sample-duration {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.sample-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
.play-icon, .delete-icon {
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
margin-top: 50rpx;
|
||||||
|
background: #ff9800;
|
||||||
|
color: #fff;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
border-radius: 45rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
button[disabled] {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/static/default-avatar.png
Normal file
1
src/static/default-avatar.png
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
src/static/tabbar/.keep
Normal file
1
src/static/tabbar/.keep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 保持 tabbar 目录存在,用于存放底部导航图标。
|
||||||
214
src/stores/user.js
Normal file
214
src/stores/user.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// 状态
|
||||||
|
const token = ref('');
|
||||||
|
const nickName = ref('');
|
||||||
|
const avatarUrl = ref('');
|
||||||
|
const openid = ref('');
|
||||||
|
const isLoggedIn = ref(false);
|
||||||
|
const hasAgreedToTerms = ref(false);
|
||||||
|
const hasVisitedMinePage = ref(false);
|
||||||
|
|
||||||
|
// 初始化:从本地存储加载用户信息
|
||||||
|
function init() {
|
||||||
|
try {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo');
|
||||||
|
if (userInfo) {
|
||||||
|
const parsedInfo = JSON.parse(userInfo);
|
||||||
|
token.value = parsedInfo.token || '';
|
||||||
|
nickName.value = parsedInfo.nickName || '';
|
||||||
|
avatarUrl.value = parsedInfo.avatarUrl || '';
|
||||||
|
openid.value = parsedInfo.openid || '';
|
||||||
|
isLoggedIn.value = !!parsedInfo.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agreedStatus = uni.getStorageSync('hasAgreedToTerms');
|
||||||
|
hasAgreedToTerms.value = agreedStatus === 'true';
|
||||||
|
|
||||||
|
const visitedMine = uni.getStorageSync('hasVisitedMinePage');
|
||||||
|
hasVisitedMinePage.value = visitedMine === 'true';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load user info from storage', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
// 1. 获取用户信息
|
||||||
|
const userProfileRes = await getUserProfile();
|
||||||
|
|
||||||
|
// 2. 获取微信登录凭证
|
||||||
|
const loginRes = await getWxLogin();
|
||||||
|
|
||||||
|
// 3. 调用后端登录接口
|
||||||
|
const loginData = await wxLogin(loginRes.code, userProfileRes.userInfo);
|
||||||
|
|
||||||
|
// 4. 保存用户信息
|
||||||
|
setUserInfo({
|
||||||
|
token: loginData.token,
|
||||||
|
nickName: userProfileRes.userInfo.nickName,
|
||||||
|
avatarUrl: userProfileRes.userInfo.avatarUrl,
|
||||||
|
openid: loginData.openid
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(loginData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
function getUserProfile() {
|
||||||
|
// H5环境下模拟用户信息
|
||||||
|
// #ifdef H5
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
userInfo: {
|
||||||
|
nickName: 'H5测试用户',
|
||||||
|
avatarUrl: '/static/default-avatar.png'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 非H5环境下获取真实用户信息
|
||||||
|
// #ifndef H5
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.getUserProfile({
|
||||||
|
desc: '用于完善用户资料',
|
||||||
|
success: (res) => {
|
||||||
|
resolve(res);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信登录凭证
|
||||||
|
function getWxLogin() {
|
||||||
|
// H5环境下模拟登录凭证
|
||||||
|
// #ifdef H5
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({ code: 'mock-code-for-h5' });
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 非H5环境下获取真实登录凭证
|
||||||
|
// #ifndef H5
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (res) => {
|
||||||
|
resolve(res);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端登录接口
|
||||||
|
function wxLogin(code, userInfo) {
|
||||||
|
// H5环境下模拟登录,避免API超时
|
||||||
|
// #ifdef H5
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
token: 'mock-token-for-h5',
|
||||||
|
openid: 'mock-openid-for-h5'
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 非H5环境下调用实际接口
|
||||||
|
// #ifndef H5
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url: 'https://your-api.com/api/login/wechat',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
code,
|
||||||
|
userInfo
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200 && res.data.token) {
|
||||||
|
resolve(res.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Login failed: ' + JSON.stringify(res.data)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
// 设置超时时间
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
function setUserInfo(userInfo) {
|
||||||
|
token.value = userInfo.token || '';
|
||||||
|
nickName.value = userInfo.nickName || '';
|
||||||
|
avatarUrl.value = userInfo.avatarUrl || '';
|
||||||
|
openid.value = userInfo.openid || '';
|
||||||
|
isLoggedIn.value = !!userInfo.token;
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
uni.setStorageSync('userInfo', JSON.stringify(userInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
function logout() {
|
||||||
|
token.value = '';
|
||||||
|
nickName.value = '';
|
||||||
|
avatarUrl.value = '';
|
||||||
|
openid.value = '';
|
||||||
|
isLoggedIn.value = false;
|
||||||
|
|
||||||
|
// 清除本地存储
|
||||||
|
uni.removeStorageSync('userInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:设置用户同意须知状态
|
||||||
|
function setAgreedToTerms(status) {
|
||||||
|
hasAgreedToTerms.value = status;
|
||||||
|
uni.setStorageSync('hasAgreedToTerms', status.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:设置用户已访问我的页面
|
||||||
|
function setVisitedMinePage(status) {
|
||||||
|
hasVisitedMinePage.value = status;
|
||||||
|
uni.setStorageSync('hasVisitedMinePage', status.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
token,
|
||||||
|
nickName,
|
||||||
|
avatarUrl,
|
||||||
|
openid,
|
||||||
|
isLoggedIn,
|
||||||
|
hasAgreedToTerms,
|
||||||
|
hasVisitedMinePage,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
init,
|
||||||
|
login,
|
||||||
|
setUserInfo,
|
||||||
|
logout,
|
||||||
|
setAgreedToTerms,
|
||||||
|
setVisitedMinePage
|
||||||
|
};
|
||||||
|
});
|
||||||
427
产品设计需求文档.md
Normal file
427
产品设计需求文档.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
# 小智AI产品设计需求文档
|
||||||
|
|
||||||
|
## 1. 产品概述
|
||||||
|
|
||||||
|
### 1.1 产品背景
|
||||||
|
|
||||||
|
本产品是小智AI的用户操作界面,基于微信小程序开发,为用户提供AI语音交互、剧本创作和智能对话等功能。产品通过整合阿里云语音技术,提供高质量的TTS(文本转语音)、ASR(语音识别)和声音克隆服务,结合小智AI强大的自然语言处理能力,为用户创造沉浸式的AI交互体验。
|
||||||
|
|
||||||
|
小程序作为前端界面,所有核心AI功能均由部署在服务器端的小智AI Java后端提供支持。产品不仅满足用户对AI语音交互的需求,还通过配网中心功能,为ESP32等IoT设备提供便捷的网络配置服务,构建完整的智能生态系统。
|
||||||
|
|
||||||
|
### 1.2 产品目标
|
||||||
|
|
||||||
|
- **核心目标**:打造小智AI的标准用户界面,提供流畅的AI语音交互体验
|
||||||
|
- **技术集成**:深度集成阿里云语音服务,确保语音识别和合成的高质量表现
|
||||||
|
- **用户价值**:为用户提供直观易用的AI对话界面,支持多种交互模式和个性化设置
|
||||||
|
- **生态构建**:通过ESP32配网功能,连接智能硬件设备,构建完整的AIoT生态
|
||||||
|
- **平台优势**:充分利用微信生态,降低用户使用门槛,提升产品推广效率
|
||||||
|
|
||||||
|
### 1.3 产品定位
|
||||||
|
|
||||||
|
小智AI的官方微信小程序界面,专注于提供AI语音交互、智能对话和设备配网服务,是连接用户与小智AI生态系统的重要桥梁。
|
||||||
|
|
||||||
|
## 2. 后端功能需求
|
||||||
|
|
||||||
|
### 2.1 用户认证系统
|
||||||
|
|
||||||
|
#### 2.1.1 微信授权登录
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
作为小智AI的用户入口,实现与微信生态的无缝集成,为用户提供便捷的登录体验。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **微信OAuth 2.0集成**:处理微信小程序登录流程,获取用户openid和基本信息
|
||||||
|
- **小智AI账户关联**:将微信用户与小智AI后端用户系统进行关联绑定
|
||||||
|
- **统一身份认证**:生成小智AI系统的用户token,实现跨服务的身份验证
|
||||||
|
- **设备绑定管理**:支持用户绑定多个智能设备,统一管理设备权限
|
||||||
|
- **会话保持**:维护用户登录状态,支持长期免登录使用
|
||||||
|
- **多端同步**:支持用户在不同设备间的数据同步和会话延续
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 与小智AI后端用户系统深度集成
|
||||||
|
- 实现微信小程序与服务器的安全通信
|
||||||
|
- 支持设备授权和权限管理
|
||||||
|
|
||||||
|
#### 2.1.2 用户信息管理
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
管理用户在小智AI系统中的个人资料和偏好设置。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **用户档案系统**:存储用户基本信息、使用偏好和历史数据
|
||||||
|
- **AI交互偏好**:管理用户对AI助手的个性化设置(语音类型、回复风格等)
|
||||||
|
- **设备管理中心**:展示和管理用户绑定的所有智能设备
|
||||||
|
- **使用统计**:记录用户的AI交互次数、时长等使用数据
|
||||||
|
- **隐私设置**:提供细粒度的隐私控制选项
|
||||||
|
- **数据同步**:与小智AI后端保持用户数据的实时同步
|
||||||
|
|
||||||
|
**数据模型**:
|
||||||
|
```
|
||||||
|
用户表 (users)
|
||||||
|
- user_id: 小智AI用户ID
|
||||||
|
- openid: 微信唯一标识
|
||||||
|
- nickname: 用户昵称
|
||||||
|
- avatar_url: 头像地址
|
||||||
|
- voice_preference: 语音偏好设置
|
||||||
|
- ai_personality: AI助手个性设置
|
||||||
|
- device_count: 绑定设备数量
|
||||||
|
- last_active: 最后活跃时间
|
||||||
|
- created_at: 创建时间
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.1.3 权限与设备管理
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
实现用户权限控制和智能设备的统一管理。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **分级权限系统**:区分普通用户、高级用户和管理员权限
|
||||||
|
- **设备权限控制**:管理用户对不同设备的访问和控制权限
|
||||||
|
- **功能使用限制**:根据用户等级限制AI服务的使用频率和功能范围
|
||||||
|
- **设备分组管理**:支持用户对设备进行分组和场景化管理
|
||||||
|
- **权限审计**:记录用户权限变更和设备操作日志
|
||||||
|
|
||||||
|
**权限级别定义**:
|
||||||
|
- **普通用户**:基础AI对话和设备控制功能
|
||||||
|
- **高级用户**:高级AI功能、更多设备绑定、优先服务支持
|
||||||
|
- **管理员**:系统管理和用户支持权限
|
||||||
|
|
||||||
|
### 2.2 AI对话系统
|
||||||
|
|
||||||
|
#### 2.2.1 智能对话服务
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
基于小智AI后端提供的自然语言处理能力,实现智能对话功能。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **多轮对话管理**:支持上下文相关的连续对话,保持对话连贯性
|
||||||
|
- **意图识别与槽位填充**:准确理解用户意图,提取关键信息
|
||||||
|
- **个性化回复**:根据用户偏好和历史交互调整AI回复风格
|
||||||
|
- **多模态交互**:支持文本、语音、图片等多种输入输出方式
|
||||||
|
- **知识库查询**:集成小智AI的知识库,提供准确的信息回复
|
||||||
|
- **情感分析**:识别用户情感状态,提供相应的情感回应
|
||||||
|
- **对话历史管理**:保存对话记录,支持历史对话回顾和继续
|
||||||
|
|
||||||
|
**技术架构**:
|
||||||
|
- 前端采集用户输入,发送至小智AI后端处理
|
||||||
|
- 集成阿里云ASR服务进行语音识别
|
||||||
|
- 使用阿里云TTS服务进行语音合成
|
||||||
|
- 实现对话状态管理和上下文维护
|
||||||
|
|
||||||
|
**数据模型**:
|
||||||
|
```
|
||||||
|
对话会话表 (chat_sessions)
|
||||||
|
- session_id: 会话ID
|
||||||
|
- user_id: 用户ID
|
||||||
|
- start_time: 开始时间
|
||||||
|
- last_message_time: 最后消息时间
|
||||||
|
- context: 对话上下文JSON
|
||||||
|
- status: 会话状态
|
||||||
|
|
||||||
|
对话消息表 (chat_messages)
|
||||||
|
- message_id: 消息ID
|
||||||
|
- session_id: 会话ID
|
||||||
|
- sender_type: 发送者类型(user/ai)
|
||||||
|
- content_type: 内容类型(text/audio/image)
|
||||||
|
- content: 消息内容
|
||||||
|
- timestamp: 时间戳
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.2 语音服务集成
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
集成阿里云语音服务,提供高质量的语音识别和合成功能。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **实时语音识别**:使用阿里云ASR API进行实时语音转文本
|
||||||
|
- **智能语音合成**:使用阿里云TTS API将AI回复转换为自然语音
|
||||||
|
- **多语言支持**:支持中文、英文等多语言的语音识别和合成
|
||||||
|
- **语音参数调节**:支持语速、音调、音色等参数的个性化调节
|
||||||
|
- **噪音抑制**:集成语音增强算法,提升识别准确率
|
||||||
|
- **离线缓存**:对常用回复进行语音缓存,提升响应速度
|
||||||
|
- **音频格式优化**:支持多种音频格式,优化传输效率
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 集成阿里云语音服务SDK
|
||||||
|
- 实现音频数据的高效传输和处理
|
||||||
|
- 优化语音识别和合成的延迟
|
||||||
|
|
||||||
|
#### 2.2.3 声音克隆与个性化
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
基于阿里云语音技术,为用户提供个性化声音克隆服务。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **声音样本采集**:指导用户录制高质量的声音样本
|
||||||
|
- **阿里云声音克隆**:调用阿里云语音克隆API进行模型训练
|
||||||
|
- **声音质量评估**:评估克隆声音的质量和相似度
|
||||||
|
- **多场景适配**:为不同应用场景优化声音表现
|
||||||
|
- **声音管理**:管理用户的个人声音资产
|
||||||
|
- **隐私保护**:确保用户声音数据的安全和隐私
|
||||||
|
|
||||||
|
**数据模型**:
|
||||||
|
```
|
||||||
|
用户声音表 (user_voices)
|
||||||
|
- voice_id: 声音ID
|
||||||
|
- user_id: 用户ID
|
||||||
|
- voice_name: 声音名称
|
||||||
|
- aliyun_voice_id: 阿里云声音ID
|
||||||
|
- sample_urls: 样本音频URLs
|
||||||
|
- quality_score: 质量评分
|
||||||
|
- training_status: 训练状态
|
||||||
|
- created_at: 创建时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 设备配网中心
|
||||||
|
|
||||||
|
#### 2.3.1 ESP32设备配网
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
为ESP32等智能硬件设备提供便捷的Wi-Fi配网服务。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **设备发现**:自动发现附近的待配网ESP32设备
|
||||||
|
- **Wi-Fi配置**:通过小程序为设备配置Wi-Fi网络信息
|
||||||
|
- **配网协议支持**:支持SmartConfig、AP配网等多种配网方式
|
||||||
|
- **设备绑定**:配网成功后自动绑定设备到用户账户
|
||||||
|
- **配网状态监控**:实时监控配网进程和设备连接状态
|
||||||
|
- **错误处理**:提供配网失败的诊断和重试机制
|
||||||
|
- **批量配网**:支持同时为多个设备进行配网
|
||||||
|
|
||||||
|
**技术实现**:
|
||||||
|
- 集成微信小程序的设备配网API
|
||||||
|
- 实现与ESP32设备的通信协议
|
||||||
|
- 提供配网过程的用户指导界面
|
||||||
|
|
||||||
|
#### 2.3.2 设备管理与控制
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
提供已配网设备的统一管理和控制功能。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **设备列表管理**:展示用户绑定的所有智能设备
|
||||||
|
- **设备状态监控**:实时显示设备在线状态和运行参数
|
||||||
|
- **远程控制**:通过小程序远程控制设备功能
|
||||||
|
- **设备分组**:支持按房间、功能等维度对设备分组
|
||||||
|
- **场景联动**:创建智能场景,实现设备间的联动控制
|
||||||
|
- **设备信息管理**:管理设备名称、位置等基础信息
|
||||||
|
- **固件升级**:支持设备固件的OTA升级
|
||||||
|
|
||||||
|
**数据模型**:
|
||||||
|
```
|
||||||
|
设备表 (devices)
|
||||||
|
- device_id: 设备ID
|
||||||
|
- user_id: 用户ID
|
||||||
|
- device_type: 设备类型
|
||||||
|
- device_name: 设备名称
|
||||||
|
- mac_address: MAC地址
|
||||||
|
- ip_address: IP地址
|
||||||
|
- firmware_version: 固件版本
|
||||||
|
- online_status: 在线状态
|
||||||
|
- location: 设备位置
|
||||||
|
- group_id: 分组ID
|
||||||
|
- created_at: 创建时间
|
||||||
|
|
||||||
|
设备状态表 (device_status)
|
||||||
|
- status_id: 状态ID
|
||||||
|
- device_id: 设备ID
|
||||||
|
- status_data: 状态数据JSON
|
||||||
|
- timestamp: 时间戳
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.3 IoT场景管理
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
提供智能场景的创建和管理功能,实现设备间的智能联动。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **场景创建**:图形化界面创建智能场景
|
||||||
|
- **条件触发**:支持时间、传感器数据、语音指令等触发条件
|
||||||
|
- **动作执行**:定义场景触发后的设备动作序列
|
||||||
|
- **场景调试**:提供场景逻辑的测试和调试功能
|
||||||
|
- **场景分享**:支持场景模板的分享和导入
|
||||||
|
- **语音控制**:通过语音指令激活智能场景
|
||||||
|
|
||||||
|
### 2.4 内容管理系统
|
||||||
|
|
||||||
|
#### 2.4.1 AI角色管理
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
管理小智AI系统中的各种AI角色和其特性。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **角色库管理**:维护系统预设的AI角色库
|
||||||
|
- **角色个性化**:允许用户自定义AI角色的性格和回复风格
|
||||||
|
- **角色切换**:支持用户在不同场景下切换AI角色
|
||||||
|
- **角色训练**:基于用户反馈持续优化AI角色表现
|
||||||
|
- **角色分享**:支持用户创建和分享自定义AI角色
|
||||||
|
|
||||||
|
#### 2.4.2 知识库管理
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
管理小智AI的知识库内容,确保AI回复的准确性和时效性。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **知识内容更新**:定期更新AI知识库内容
|
||||||
|
- **领域知识管理**:按不同领域组织和管理知识内容
|
||||||
|
- **知识质量控制**:确保知识内容的准确性和可靠性
|
||||||
|
- **个性化知识**:根据用户需求定制专属知识内容
|
||||||
|
|
||||||
|
### 2.5 系统管理功能
|
||||||
|
|
||||||
|
#### 2.5.1 数据统计与分析
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
收集和分析用户使用数据,优化产品体验。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **用户行为分析**:分析用户的使用习惯和偏好
|
||||||
|
- **AI服务统计**:统计AI对话、语音服务的使用情况
|
||||||
|
- **设备使用统计**:分析设备的使用频率和模式
|
||||||
|
- **性能监控**:监控系统性能和服务质量
|
||||||
|
- **用户反馈收集**:收集用户对产品的反馈和建议
|
||||||
|
|
||||||
|
#### 2.5.2 系统监控与维护
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
确保小程序和后端服务的稳定运行。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- **服务健康监控**:监控小智AI后端服务的运行状态
|
||||||
|
- **API调用监控**:跟踪小程序与后端API的调用情况
|
||||||
|
- **阿里云服务监控**:监控语音服务API的调用状态和质量
|
||||||
|
- **错误日志管理**:收集和分析系统错误日志
|
||||||
|
- **性能优化**:持续优化系统性能和用户体验
|
||||||
|
|
||||||
|
## 3. 技术架构需求
|
||||||
|
|
||||||
|
### 3.1 整体架构设计
|
||||||
|
|
||||||
|
**架构原则**:
|
||||||
|
- 前后端分离:小程序作为纯前端界面,核心逻辑由小智AI后端处理
|
||||||
|
- 服务集成:深度集成阿里云语音服务和小智AI核心能力
|
||||||
|
- 高可用性:确保关键服务的稳定性和容错能力
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
- **微信小程序前端**:用户界面和交互逻辑
|
||||||
|
- **小智AI Java后端**:核心业务逻辑和AI服务
|
||||||
|
- **阿里云语音服务**:TTS、ASR、声音克隆API
|
||||||
|
- **设备配网服务**:ESP32等IoT设备的配网支持
|
||||||
|
- **数据存储层**:用户数据、设备数据、对话记录等
|
||||||
|
|
||||||
|
### 3.2 数据存储架构
|
||||||
|
|
||||||
|
**存储策略**:
|
||||||
|
- **关系型数据库**:MySQL存储用户信息、设备数据、对话记录
|
||||||
|
- **缓存系统**:Redis缓存热点数据,提升响应速度
|
||||||
|
- **文件存储**:阿里云OSS存储音频文件和媒体资源
|
||||||
|
- **时序数据库**:存储设备状态数据和监控指标
|
||||||
|
|
||||||
|
### 3.3 AI服务集成
|
||||||
|
|
||||||
|
**集成架构**:
|
||||||
|
- **小智AI核心**:自然语言理解、对话管理、知识问答
|
||||||
|
- **阿里云ASR**:语音识别服务,支持实时和批量识别
|
||||||
|
- **阿里云TTS**:语音合成服务,提供多种音色和语言
|
||||||
|
- **阿里云语音克隆**:个性化声音克隆和训练服务
|
||||||
|
- **API网关**:统一管理外部API调用和安全认证
|
||||||
|
|
||||||
|
### 3.4 安全与隐私
|
||||||
|
|
||||||
|
**安全措施**:
|
||||||
|
- **数据传输加密**:HTTPS/WSS加密传输
|
||||||
|
- **API安全认证**:OAuth 2.0和JWT token验证
|
||||||
|
- **隐私数据保护**:用户语音数据的加密存储和处理
|
||||||
|
- **设备安全**:ESP32设备的安全配网和通信加密
|
||||||
|
- **合规性保障**:符合数据保护法规要求
|
||||||
|
|
||||||
|
## 4. 功能特性需求
|
||||||
|
|
||||||
|
### 4.1 用户界面设计
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- 简洁直观:遵循微信小程序设计规范,界面简洁易用
|
||||||
|
- 响应迅速:优化交互响应时间,提升用户体验
|
||||||
|
- 适配性强:适配不同尺寸的手机屏幕
|
||||||
|
|
||||||
|
**关键界面**:
|
||||||
|
- **主对话界面**:AI对话的主要交互界面
|
||||||
|
- **设备管理界面**:设备列表、状态监控、控制面板
|
||||||
|
- **配网向导界面**:设备配网的步骤指导
|
||||||
|
- **设置界面**:用户偏好、语音设置、隐私控制
|
||||||
|
|
||||||
|
### 4.2 性能要求
|
||||||
|
|
||||||
|
**关键指标**:
|
||||||
|
- AI对话响应时间:<2秒
|
||||||
|
- 语音识别延迟:<1秒
|
||||||
|
- 语音合成延迟:<3秒
|
||||||
|
- 设备配网成功率:>95%
|
||||||
|
- 小程序启动时间:<3秒
|
||||||
|
|
||||||
|
### 4.3 兼容性要求
|
||||||
|
|
||||||
|
**支持范围**:
|
||||||
|
- 微信版本:7.0及以上
|
||||||
|
- iOS系统:12.0及以上
|
||||||
|
- Android系统:8.0及以上
|
||||||
|
- 设备类型:ESP32系列开发板
|
||||||
|
|
||||||
|
## 5. 部署与运维
|
||||||
|
|
||||||
|
### 5.1 部署架构
|
||||||
|
|
||||||
|
**部署方案**:
|
||||||
|
- **小程序发布**:通过微信开发者工具发布到微信平台
|
||||||
|
- **后端部署**:小智AI Java后端部署在云服务器
|
||||||
|
- **服务依赖**:阿里云语音服务、数据库、缓存等基础服务
|
||||||
|
- **CDN加速**:静态资源通过CDN加速分发
|
||||||
|
|
||||||
|
### 5.2 监控与维护
|
||||||
|
|
||||||
|
**监控体系**:
|
||||||
|
- **业务监控**:AI服务调用、用户活跃度、设备连接状态
|
||||||
|
- **性能监控**:响应时间、并发量、资源使用率
|
||||||
|
- **错误监控**:API调用失败、异常日志、用户反馈
|
||||||
|
- **第三方服务监控**:阿里云API调用状态和限额使用情况
|
||||||
|
|
||||||
|
## 6. 项目实施计划
|
||||||
|
|
||||||
|
### 6.1 开发阶段
|
||||||
|
|
||||||
|
**第一阶段(基础功能,2个月)**:
|
||||||
|
- 用户认证和基础AI对话功能
|
||||||
|
- 阿里云语音服务集成
|
||||||
|
- 基础设备管理功能
|
||||||
|
|
||||||
|
**第二阶段(设备配网,1.5个月)**:
|
||||||
|
- ESP32设备配网功能
|
||||||
|
- 设备控制和状态监控
|
||||||
|
- IoT场景管理
|
||||||
|
|
||||||
|
**第三阶段(优化完善,1个月)**:
|
||||||
|
- 用户体验优化
|
||||||
|
- 性能调优和稳定性提升
|
||||||
|
- 功能完善和Bug修复
|
||||||
|
|
||||||
|
### 6.2 技术选型
|
||||||
|
|
||||||
|
**主要技术栈**:
|
||||||
|
- 前端:微信小程序原生开发
|
||||||
|
- 后端:Java Spring Boot (小智AI现有架构)
|
||||||
|
- 数据库:MySQL + Redis
|
||||||
|
- 外部服务:阿里云语音服务
|
||||||
|
- 通信协议:HTTPS/WebSocket
|
||||||
|
|
||||||
|
## 7. 总结
|
||||||
|
|
||||||
|
本产品设计需求文档详细描述了小智AI微信小程序的完整功能需求和技术架构。作为小智AI的官方用户界面,小程序将提供AI对话、语音交互、设备配网等核心功能,通过集成阿里云语音技术和小智AI后端服务,为用户创造优质的AI交互体验。
|
||||||
|
|
||||||
|
关键成功因素:
|
||||||
|
1. 与小智AI后端的深度集成
|
||||||
|
2. 阿里云语音服务的高效利用
|
||||||
|
3. ESP32设备配网的稳定实现
|
||||||
|
4. 优秀的用户体验设计
|
||||||
|
5. 完善的监控和运维保障
|
||||||
|
|
||||||
|
通过本文档的实施,将建立一个功能完善、技术先进的小智AI用户界面,为用户提供便捷的AI交互和智能设备管理服务。
|
||||||
Reference in New Issue
Block a user