Compare commits

...

16 Commits

Author SHA1 Message Date
chantouRichard edde0bf3ed Merge remote-tracking branch 'origin/hot-fix' into hot-fix
2 months ago
chantouRichard 0379bf5114 前端修改
2 months ago
heiferleaf c4db02796b bug修复
2 months ago
chantouRichard df8586446e 小更新
2 months ago
chantouRichard d3e4ae3688 前端修改
2 months ago
heiferleaf 76c895033e 立论样式修改
2 months ago
chantouRichard a89d6bb9ec 前端轮数实现以及后端WxReviewServiceImpl修改
2 months ago
chantouRichard 3a814e1214 更新
2 months ago
chantouRichard 7f65bab280 前端页面历史记录列表实现
2 months ago
chantouRichard cc63675e71 前端页面历史记录列表实现
2 months ago
chantouRichard 14f6b2d661 Merge remote-tracking branch 'origin/hot-fix' into hot-fix
2 months ago
chantouRichard 6fae2e87af 前端页面历史记录列表实现
2 months ago
chantouRichard da3f1638c6 前端页面历史记录列表实现
2 months ago
heiferleaf 4c614647e9 复盘和辩论对话Id-1报错修复
2 months ago
heiferleaf da100df835 对话内容的content提取和辩论历史记录通过数据库获取
2 months ago
heiferleaf 16dccb1f00 新增所有功能后端实现_yhf6/3
2 months ago

@ -1,106 +1,416 @@
# 微信小程序登录Demo后端
## 项目介绍
本项目是一个微信小程序登录的后端Demo实现了微信小程序登录、获取用户信息等功能。
## 技术栈
- Spring Boot 3.0.12
- MyBatis 3.0.1
- MySQL 8.0
- JWT
## 项目结构
```
src/main/java/com/learning/newdemo
├── config // 配置类
├── controller // 控制器
├── entity // 实体类
├── mapper // 数据访问层
├── service // 服务层
│ └── impl // 服务实现
├── util // 工具类
└── common // 通用类
```
## 运行环境
- JDK 17+
- MySQL 8.0+
- Maven 3.6+
## 数据库配置
1. 创建数据库和表
```sql
# 执行src/main/resources/db/wx_miniapp.sql脚本
```
2. 修改数据库连接信息(`application.yml`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
```
## 微信小程序配置
修改`application.yml`中的微信小程序配置:
```yaml
wechat:
miniapp:
appid: 你的小程序APPID
secret: 你的小程序SECRET
```
## 启动项目
```bash
mvn spring-boot:run
```
## 接口说明
### 1. 登录接口
- URL: `/api/wx/login`
- Method: POST
- Body:
```json
{
"code": "微信临时登录凭证"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"token": "JWT令牌"
}
}
```
### 2. 获取用户信息接口
- URL: `/api/wx/user`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"id": 1,
"openid": "用户openid",
"nickname": "用户昵称",
"avatarUrl": "头像URL",
"gender": 1,
"country": "国家",
"province": "省份",
"city": "城市",
"language": "语言"
}
}
```
# 微信小程序辩论助手后端
## 项目介绍
本项目是一个微信小程序辩论助手的后端服务实现了微信小程序登录、获取用户信息、AI辅助立论、复盘分析、模拟辩论等功能并支持历史记录的保存与查询。
## 技术栈
- Spring Boot 3.0.12
- MyBatis 3.0.1
- MySQL 8.0
- JWT
- RestTemplate (AI接口调用)
## 项目结构
```
src/main/java/com/learning/newdemo
├── config // 配置类
├── controller // 控制器
├── entity // 实体类
├── mapper // 数据访问层
├── service // 服务层
│ └── impl // 服务实现
├── util // 工具类
└── common // 通用类
```
## 运行环境
- JDK 17+
- MySQL 8.0+
- Maven 3.6+
## 数据库配置
1. 创建数据库和表
```sql
# 执行src/main/resources/db/wx_miniapp.sql脚本
# 用户表
CREATE TABLE `wx_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(255) DEFAULT NULL COMMENT '头像',
`gender` tinyint DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';
# 对话活动表
CREATE TABLE `wx_conversation` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '用户ID',
`type` varchar(20) NOT NULL COMMENT '对话类型(debate/argument/review)',
`title` varchar(255) DEFAULT NULL COMMENT '对话标题',
`preview` varchar(255) DEFAULT NULL COMMENT '预览内容(最后一次AI回复的前10个字符)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话活动表';
# 辩论历史记录表
CREATE TABLE `wx_debate_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`conversation_id` bigint NOT NULL COMMENT '对话活动ID',
`content` text COMMENT 'AI回复内容',
`user_message` text COMMENT '用户消息',
`sequence` int NOT NULL COMMENT '消息序号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_conversation_id` (`conversation_id`),
KEY `idx_sequence` (`sequence`),
CONSTRAINT `fk_debate_conversation` FOREIGN KEY (`conversation_id`) REFERENCES `wx_conversation` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='辩论历史记录表';
# 立论历史记录表
CREATE TABLE `wx_argument_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`conversation_id` bigint NOT NULL COMMENT '对话活动ID',
`topic` varchar(255) DEFAULT NULL COMMENT '辩题',
`stance` varchar(50) DEFAULT NULL COMMENT '立场',
`content` text COMMENT 'AI回复内容',
`user_message` text COMMENT '用户消息',
`sequence` int NOT NULL COMMENT '消息序号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_conversation_id` (`conversation_id`),
KEY `idx_sequence` (`sequence`),
CONSTRAINT `fk_argument_conversation` FOREIGN KEY (`conversation_id`) REFERENCES `wx_conversation` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='立论历史记录表';
# 复盘历史记录表
CREATE TABLE `wx_review_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`conversation_id` bigint NOT NULL COMMENT '对话活动ID',
`content` text COMMENT 'AI回复内容',
`user_message` text COMMENT '用户消息',
`sequence` int NOT NULL COMMENT '消息序号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_conversation_id` (`conversation_id`),
KEY `idx_sequence` (`sequence`),
CONSTRAINT `fk_review_conversation` FOREIGN KEY (`conversation_id`) REFERENCES `wx_conversation` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='复盘历史记录表';
```
2. 修改数据库连接信息(`application.yml`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
```
## 微信小程序配置
修改`application.yml`中的微信小程序配置:
```yaml
wechat:
miniapp:
appid: 你的小程序APPID
secret: 你的小程序SECRET
```
## AI接口配置
修改`application.yml`中的AI接口配置
```yaml
ai:
api:
url: AI接口地址
key: API密钥(如果需要)
```
## 启动项目
```bash
mvn spring-boot:run
```
## 接口说明
### 1. 登录接口
- URL: `/api/wx/login`
- Method: POST
- Body:
```json
{
"code": "微信临时登录凭证"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"token": "JWT令牌"
}
}
```
### 2. 获取用户信息接口
- URL: `/api/wx/user`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"id": 1,
"openid": "用户openid",
"nickname": "用户昵称",
"avatarUrl": "头像URL",
"gender": 1,
"country": "国家",
"province": "省份",
"city": "城市",
"language": "语言"
}
}
```
### 3. AI接口
#### 3.1 立论接口
- URL: `/api/ai/argument`
- Method: POST
- Headers:
```
Authorization: 登录接口返回的token
```
- Body:
```json
{
"conversationId": "对话活动ID(可选,有则更新,无则新建)",
"topic": "辩题",
"stance": "立场(正方/反方)"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"content": "AI生成的立论内容",
"conversationId": "对话活动ID"
}
}
```
- 调用时机:用户在立论助手页面输入辩题和选择立场后,点击发送按钮时调用
- 数据库操作:
- 如果没有conversationId则在wx_conversation表中新建一条记录type为"argument"并返回新建的conversationId
- 在wx_argument_record表中插入一条记录包含用户消息和AI回复内容
- 更新wx_conversation表中对应记录的preview字段(取AI回复内容前10个字符)
#### 3.2 复盘接口
- URL: `/api/ai/review`
- Method: POST
- Headers:
```
Authorization: 登录接口返回的token
```
- Body:
```json
{
"conversationId": "对话活动ID(可选,有则更新,无则新建)",
"content": "用户输入的辩论内容或复盘需求"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"content": "AI生成的复盘分析内容",
"conversationId": "对话活动ID"
}
}
```
- 调用时机:用户在复盘分析页面输入辩论内容或复盘需求后,点击发送按钮时调用
- 数据库操作:
- 如果没有conversationId则在wx_conversation表中新建一条记录type为"review"并返回新建的conversationId
- 在wx_review_record表中插入一条记录包含用户消息和AI回复内容
- 更新wx_conversation表中对应记录的preview字段(取AI回复内容前10个字符)
#### 3.3 辩论接口
- URL: `/api/ai/debate`
- Method: POST
- Headers:
```
Authorization: 登录接口返回的token
```
- Body:
```json
{
"conversationId": "对话活动ID(可选,有则更新,无则新建)",
"userMessage": "用户当前输入的消息"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"content": "AI生成的辩论回复内容",
"conversationId": "对话活动ID"
}
}
```
- 调用时机:用户在模拟辩论页面输入消息后,点击发送按钮时调用
- 数据库操作:
- 如果没有conversationId则在wx_conversation表中新建一条记录type为"debate"并返回新建的conversationId
- 在wx_debate_record表中插入一条记录包含用户消息和AI回复内容
- 更新wx_conversation表中对应记录的preview字段(取AI回复内容前10个字符)
### 4. 历史记录接口
#### 4.1 获取对话活动列表
- URL: `/api/conversation/list`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": [
{
"id": 1,
"type": "debate",
"title": "对话标题",
"preview": "AI回复的前10个字符",
"createTime": "2023-01-01 12:00:00"
}
]
}
```
- 调用时机用户进入历史记录列表页面时调用根据当前所在模块传入不同的type参数
- 数据库操作查询当前用户在wx_conversation表中指定type的所有记录按创建时间倒序排列
#### 4.2 获取辩论历史记录详情
- URL: `/api/conversation/debate/{conversationId}`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data":
{
"id": 1,
"userMessage": "用户消息",
"content": "AI回复内容",
"sequence": 1,
"createTime": "2023-01-01 12:00:00"
}
}
```
- 调用时机:用户点击辩论历史记录列表中的某一项时调用
- 数据库操作:
- 查询wx_conversation表中指定id的记录
- 查询wx_debate_record表中conversation_id等于指定id的所有记录按sequence排序
#### 4.3 获取立论历史记录详情
- URL: `/api/conversation/argument/{conversationId}`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data":
[
{
"id": 1,
"userMessage": "用户消息",
"content": "AI回复内容",
"sequence": 1,
"createTime": "2023-01-01 12:00:00"
}
]
}
```
- 调用时机:用户点击立论历史记录列表中的某一项时调用
- 数据库操作:
- 查询wx_conversation表中指定id的记录
- 查询wx_argument_record表中conversation_id等于指定id的所有记录按sequence排序
#### 4.4 获取复盘历史记录详情
- URL: `/api/conversation/review/{conversationId}`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
[
{
"id": 1,
"userMessage": "用户消息",
"content": "AI回复内容",
"sequence": 1,
"createTime": "2023-01-01 12:00:00"
}
]
}
}
```
- 调用时机:用户点击复盘历史记录列表中的某一项时调用
- 数据库操作:
- 查询wx_conversation表中指定id的记录
- 查询wx_review_record表中conversation_id等于指定id的所有记录按sequence排序

@ -77,6 +77,12 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>

@ -0,0 +1,23 @@
package com.learning.newdemo.Dto;
import com.learning.newdemo.entity.WxArgument;
import lombok.Data;
import java.util.Date;
@Data
public class ArgumentDetailDTO {
private Integer id;
private String userMessage;
private String content;
private Integer sequence;
private Date createTime;
public ArgumentDetailDTO(WxArgument wxArgument) {
this.id = wxArgument.getId();
this.userMessage = wxArgument.getUserMessage();
this.content = wxArgument.getContent();
this.sequence = wxArgument.getSequence();
this.createTime = wxArgument.getCreateTime();
}
}

@ -0,0 +1,23 @@
package com.learning.newdemo.Dto;
import com.learning.newdemo.entity.WxConversation;
import lombok.Data;
import java.util.Date;
@Data
public class ConversationDTO {
private Long id;
private String type; // "debate"/"argument"/"review"
private String title;
private String preview; // AI回复的前10个字符
private Date updateTime;
public ConversationDTO(WxConversation wxConversation) {
this.id = wxConversation.getId();
this.type = wxConversation.getType();
this.title = wxConversation.getTitle();
this.preview = wxConversation.getPreview();
this.updateTime = wxConversation.getUpdateTime();
}
}

@ -0,0 +1,23 @@
package com.learning.newdemo.Dto;
import com.learning.newdemo.entity.WxDebate;
import lombok.Data;
import java.util.Date;
@Data
public class DebateDetailDTO {
private Long id;
private String userMessage;
private String content;
private Integer sequence;
private Date createTime;
public DebateDetailDTO(WxDebate wxDebate) {
this.id = wxDebate.getId();
this.userMessage = wxDebate.getUserMessage();
this.content = wxDebate.getContent();
this.sequence = wxDebate.getSequence();
this.createTime = wxDebate.getCreateTime();
}
}

@ -0,0 +1,23 @@
package com.learning.newdemo.Dto;
import com.learning.newdemo.entity.WxReview;
import lombok.Data;
import java.util.Date;
@Data
public class ReviewDetailDTO {
private Long id;
private String userMessage; // 用户消息
private String content; // AI回复内容
private Integer sequence; // 消息序号
private Date createTime;
public ReviewDetailDTO(WxReview wxReview) {
this.id = wxReview.getId();
this.userMessage = wxReview.getUserMessage();
this.content = wxReview.getContent();
this.sequence = wxReview.getSequence();
this.createTime = wxReview.getCreateTime();
}
}

@ -7,10 +7,7 @@ import com.learning.newdemo.service.WxDebateService;
import com.learning.newdemo.service.WxReviewService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@ -34,11 +31,13 @@ public class WxAIController {
private String content;
private Long conversationId;
@PostMapping("/argument")
public Result<Map<String, Object>> getArgument(@RequestBody Map<String, String> params){
public Result<Map<String, Object>> getArgument(@RequestHeader ("Authorization") String token, @RequestBody Map<String, String> params){
topic = params.get("topic");
stance = params.get("stance");
conversationId = Long.parseLong(params.get("conversationId"));
if(topic == null || stance == null){
return Result.error("立论主题或者内容为空");
@ -51,9 +50,12 @@ public class WxAIController {
if (argument == null) {
return Result.error("立论获取失败");
}
log.info("test",argument);
long relatedConversationId = wxArgumentService.UpdateArgument(conversationId, topic, stance, argument, topic + stance, token);
Map<String, Object> data = new HashMap<>();
data.put("argument", argument);
data.put("content", argument);
data.put("conversationId", relatedConversationId);
// 查看data
log.info("立论获取成功:{}", argument);
@ -65,10 +67,11 @@ public class WxAIController {
}
@PostMapping("/review")
public Result<Map<String, Object>> review(@RequestBody Map<String, String> params){
public Result<Map<String, Object>> review(@RequestHeader ("Authorization") String token,@RequestBody Map<String, String> params){
log.info("请求内容: {}", params);
content = params.get("content");
conversationId = Long.parseLong(params.get("conversationId"));
try {
String review = wxReviewService.GetReview(content);
@ -76,8 +79,11 @@ public class WxAIController {
return Result.error("复盘获取失败");
}
long relatedConversationId = wxReviewService.UpdateReview(conversationId, review, content, token);
Map<String, Object> data = new HashMap<>();
data.put("review", review);
data.put("conversationId", relatedConversationId);
// 查看data
log.info("复盘获取成功:{}", review);
@ -88,18 +94,22 @@ public class WxAIController {
}
}
@PostMapping("/debate")
public Result<Map<String, Object>> debate(@RequestBody Map<String, String> params){
public Result<Map<String, Object>> debate(@RequestHeader ("Authorization") String token, @RequestBody Map<String, String> params){
log.info("请求内容: {}", params);
String history = params.get("history");
String userMessage = params.get("userMessage");
conversationId = Long.parseLong(params.get("conversationId"));
try {
String debate = wxDebateService.GetDebate(history, userMessage);
String debate = wxDebateService.GetDebate(conversationId, userMessage);
if (debate == null) {
return Result.error("辩论获取失败");
}
long relatedConversationId = wxDebateService.UpdateDebate(conversationId, debate, userMessage, token);
Map<String, Object> data = new HashMap<>();
data.put("debate", debate);
data.put("conversationId", relatedConversationId);
// 查看data
log.info("辩论获取成功:{}", debate);

@ -0,0 +1,104 @@
package com.learning.newdemo.controller;
import com.learning.newdemo.Dto.ArgumentDetailDTO;
import com.learning.newdemo.Dto.ConversationDTO;
import com.learning.newdemo.Dto.DebateDetailDTO;
import com.learning.newdemo.Dto.ReviewDetailDTO;
import com.learning.newdemo.common.Result;
import com.learning.newdemo.entity.WxArgument;
import com.learning.newdemo.entity.WxConversation;
import com.learning.newdemo.entity.WxDebate;
import com.learning.newdemo.entity.WxReview;
import com.learning.newdemo.mapper.WxArgumentMapper;
import com.learning.newdemo.mapper.WxConversationMapper;
import com.learning.newdemo.mapper.WxDebateMapper;
import com.learning.newdemo.mapper.WxReviewMapper;
import com.learning.newdemo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("api/conversation")
public class WxConversationController {
private final WxConversationMapper wxConversationMapper;
private final WxDebateMapper wxDebateMapper;
private final WxArgumentMapper wxArgumentMapper;
private final WxReviewMapper wxReviewMapper;
private final JwtUtil jwtUtil;
public WxConversationController(WxConversationMapper wxConversationMapper, WxDebateMapper wxDebateMapper, WxArgumentMapper wxArgumentMapper, WxReviewMapper wxReviewMapper, JwtUtil jwtUtil) {
this.wxConversationMapper = wxConversationMapper;
this.wxDebateMapper = wxDebateMapper;
this.wxArgumentMapper = wxArgumentMapper;
this.wxReviewMapper = wxReviewMapper;
this.jwtUtil = jwtUtil;
}
@GetMapping("/list")
public Result<List<ConversationDTO>> getConversationList(@RequestHeader("Authorization") String token){
log.info("test111:",token);
try{
int userId = jwtUtil.getUserIdFromToken(token);
List<WxConversation> conversations = wxConversationMapper.selectByUserId(userId);
List<ConversationDTO> result = conversations.stream()
.map(conv -> new ConversationDTO(
conv))
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e){
log.error("获取对话失败");
return Result.error("获取对话失败:" + e.getMessage());
}
}
@GetMapping( "/debate/{conversation}")
public Result<List<DebateDetailDTO>> getDebate(@PathVariable("conversation") Long conversationId) {
try{
List<WxDebate> wxDebates = wxDebateMapper.selectByConversationId(conversationId);
List<DebateDetailDTO> debateDetailDTOS = wxDebates.stream().map(DebateDetailDTO::new).collect(Collectors.toList());
return Result.success(debateDetailDTOS);
} catch (Exception e) {
log.error("获取辩论失败", e);
return Result.error("获取辩论失败:" + e.getMessage());
}
}
@GetMapping("/argument/{conversationId}")
public Result<List<ArgumentDetailDTO>> getArgument(@PathVariable Long conversationId) {
try {
List <WxArgument> wxArguments = wxArgumentMapper.selectByConversationId(conversationId);
List <ArgumentDetailDTO> argumentDetailDTOS = wxArguments.stream().map(ArgumentDetailDTO::new).collect(Collectors.toList());
return Result.success(argumentDetailDTOS);
} catch ( Exception e) {
log.error("获取立论失败", e);
return Result.error("获取立论失败:" + e.getMessage());
}
}
@GetMapping("/review/{conversationId}")
public Result<List<ReviewDetailDTO>> getReview(@PathVariable Long conversationId) {
try {
List<WxReview> wxReviews = wxReviewMapper.selectByConversationId(conversationId);
List<ReviewDetailDTO> reviewDetailDTOS = wxReviews.stream().map(ReviewDetailDTO::new).collect(Collectors.toList());
return Result.success(reviewDetailDTOS);
} catch ( Exception e){
log.error("获取评论失败", e);
return Result.error("获取评论失败:" + e.getMessage());
}
}
}

@ -0,0 +1,25 @@
package com.learning.newdemo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class WxArgument {
// 主键
private Integer id;
// 对话活动Id外键
private long conversationId;
// 辩题
private String topic;
// 立场
private String stance;
// AI回复内容
private String content;
// 用户信息
private String userMessage;
// 消息序号
private Integer sequence;
// 创建时间
private Date createTime;
}

@ -0,0 +1,16 @@
package com.learning.newdemo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class WxConversation {
private Long id;
private int userId;
private String type;
private String title;
private String preview;
private Date createTime;
private Date updateTime;
}

@ -0,0 +1,15 @@
package com.learning.newdemo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class WxDebate {
private Long id;
private Long conversationId;
private String content;
private String userMessage;
private Integer sequence;
private Date createTime;
}

@ -0,0 +1,15 @@
package com.learning.newdemo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class WxReview {
private Long id;
private Long conversationId; // 关联的对话活动ID
private String content; // AI回复内容
private String userMessage; // 用户消息
private Integer sequence; // 消息序号
private Date createTime;
}

@ -0,0 +1,12 @@
package com.learning.newdemo.mapper;
import com.learning.newdemo.entity.WxArgument;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface WxArgumentMapper {
List<WxArgument> selectByConversationId(long conversationId);
int insert(WxArgument wxArgument);
}

@ -0,0 +1,13 @@
package com.learning.newdemo.mapper;
import com.learning.newdemo.entity.WxConversation;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface WxConversationMapper {
List<WxConversation> selectByUserId(Integer userId);
int insert(WxConversation wxConversation);
int updatePreview(Long conversationId, String preview);
}

@ -0,0 +1,12 @@
package com.learning.newdemo.mapper;
import com.learning.newdemo.entity.WxDebate;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface WxDebateMapper {
List<WxDebate> selectByConversationId(Long conversationId);
int insert(WxDebate wxDebate);
}

@ -0,0 +1,12 @@
package com.learning.newdemo.mapper;
import com.learning.newdemo.entity.WxReview;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface WxReviewMapper {
List<WxReview> selectByConversationId(Long conversationId);
int insert(WxReview wxReview);
}

@ -6,4 +6,5 @@ package com.learning.newdemo.service;
public interface WxArgumentService {
String GetArgument(String topic, String stance);
long UpdateArgument(Long conversationId, String topic, String stance, String content, String userMessage, String token);
}

@ -1,5 +1,6 @@
package com.learning.newdemo.service;
public interface WxDebateService {
String GetDebate(String history, String userMessage);
String GetDebate(Long conversationId, String userMessage);
long UpdateDebate(Long conversationId, String content, String userMessage, String token);
}

@ -2,4 +2,5 @@ package com.learning.newdemo.service;
public interface WxReviewService {
String GetReview(String content);
long UpdateReview(Long conversationId, String content, String userMessage, String token);
}

@ -1,33 +1,33 @@
package com.learning.newdemo.service;
import com.learning.newdemo.entity.WxUser;
/**
*
*/
public interface WxUserService {
/**
*
*
* @param code
* @return tokennull
*/
String login(String code);
/**
* openid
*
* @param openid
* @return
*/
WxUser getUserByOpenid(String openid);
/**
*
*
* @param wxUser
* @return
*/
boolean updateUser(WxUser wxUser);
package com.learning.newdemo.service;
import com.learning.newdemo.entity.WxUser;
/**
*
*/
public interface WxUserService {
/**
*
*
* @param code
* @return tokennull
*/
String login(String code);
/**
* openid
*
* @param openid
* @return Id
*/
int getUserByOpenid(String openid);
/**
*
*
* @param wxUser
* @return
*/
boolean updateUser(WxUser wxUser);
}

@ -1,21 +1,29 @@
package com.learning.newdemo.service.impl;
import org.json.JSONObject;
import org.json.JSONArray;
import com.learning.newdemo.entity.WxArgument;
import com.learning.newdemo.entity.WxConversation;
import com.learning.newdemo.mapper.WxArgumentMapper;
import com.learning.newdemo.mapper.WxConversationMapper;
import com.learning.newdemo.service.WxArgumentService;
import com.learning.newdemo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
@Service
@Slf4j
public class WxArgumentServiceImpl implements WxArgumentService {
private final RestTemplate restTemplate;
private final RestTemplate _restTemplate;
private final WxArgumentMapper _wxArgumentMapper;
private final WxConversationMapper _wxConversationMapper;
private final JwtUtil jwtUtil;
@Value("${ai.argument.header.Authorization}") private String authorizationHeader;
@Value("${ai.argument.body.message.role-sys}") private String roleSys;
@ -33,8 +41,11 @@ public class WxArgumentServiceImpl implements WxArgumentService {
@Value("${ai.argument.body.logprobs}") private boolean logprobs;
@Value("${ai.argument.url}") private String url;
public WxArgumentServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
public WxArgumentServiceImpl(RestTemplate restTemplate, WxArgumentMapper wxArgumentMapper, WxConversationMapper wxConversationMapper, JwtUtil jwtUtil) {
this._restTemplate = restTemplate;
this._wxArgumentMapper = wxArgumentMapper;
this._wxConversationMapper = wxConversationMapper;
this.jwtUtil = jwtUtil;
}
@Override
@ -75,8 +86,17 @@ public class WxArgumentServiceImpl implements WxArgumentService {
log.info("请求体:{}", requestBody);
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
ResponseEntity<String> response = _restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
JSONObject json = new JSONObject(response.getBody());
JSONArray choices = json.getJSONArray("choices");
if (!choices.isEmpty()) {
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
return message.getString("content");
}
}
return null;
} catch (Exception e) {
log.error("向AI获取立论失败", e);
return null;
@ -92,4 +112,37 @@ public class WxArgumentServiceImpl implements WxArgumentService {
.replace("\r", "\\r")
.replace("\t", "\\t");
}
@Override
public long UpdateArgument(Long conversationId, String topic, String stance, String content, String userMessage, String token) {
long relatedConversationId = conversationId;
if(conversationId == -1){
int userId = jwtUtil.getUserIdFromToken(token);
WxConversation wxConversation = new WxConversation();
wxConversation.setUserId(userId);
wxConversation.setType("argument");
wxConversation.setTitle(topic);
// 渲染preview的前10个字符
wxConversation.setPreview(content.substring(0, Math.min(content.length(), 10)));
wxConversation.setCreateTime(new Date());
wxConversation.setUpdateTime(new Date());
_wxConversationMapper.insert(wxConversation);
relatedConversationId = wxConversation.getId();
}
else{
_wxConversationMapper.updatePreview(conversationId, content.substring(0, Math.min(content.length(), 10)));
}
WxArgument wxArgument = new WxArgument();
wxArgument.setConversationId(relatedConversationId);
wxArgument.setTopic(topic);
wxArgument.setStance(stance);
wxArgument.setContent(content);
wxArgument.setUserMessage(userMessage);
wxArgument.setSequence(_wxArgumentMapper.selectByConversationId(relatedConversationId).size() + 1);
wxArgument.setCreateTime(new Date());
_wxArgumentMapper.insert(wxArgument);
return relatedConversationId;
}
}

@ -1,12 +1,20 @@
package com.learning.newdemo.service.impl;
import com.learning.newdemo.entity.WxDebate;
import com.learning.newdemo.entity.WxConversation;
import com.learning.newdemo.mapper.WxConversationMapper;
import com.learning.newdemo.mapper.WxDebateMapper;
import com.learning.newdemo.service.WxDebateService;
import com.learning.newdemo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
@Service
@ -14,6 +22,9 @@ import org.springframework.web.client.RestTemplate;
public class WxDebateServiceImpl implements WxDebateService {
// 通过构造函数从IOC容器中注入RestTemplate
private final RestTemplate restTemplate;
private final WxDebateMapper wxDebateMapper;
private final WxConversationMapper wxConversationMapper;
private final JwtUtil jwtUtil;
@Value("${ai.debate.body.message.content-sys}") private String contentSys;
@Value("${ai.debate.header.Authorization}") private String authorizationHeader;
@ -31,17 +42,30 @@ public class WxDebateServiceImpl implements WxDebateService {
@Value("${ai.debate.body.logprobs}") private boolean logprobs;
@Value("${ai.debate.url}") private String url;
public WxDebateServiceImpl(RestTemplate restTemplate) {
public WxDebateServiceImpl(RestTemplate restTemplate, WxDebateMapper wxDebateMapper, WxConversationMapper wxConversationMapper, JwtUtil jwtUtil) {
this.restTemplate = restTemplate;
this.wxDebateMapper = wxDebateMapper;
this.wxConversationMapper = wxConversationMapper;
this.jwtUtil = jwtUtil;
}
@Override
public String GetDebate(String history, String userMessage){
public String GetDebate(Long conversationId, String userMessage){
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", authorizationHeader);
String history;
if(conversationId == -1){
history = "";
}
else {
history = wxDebateMapper.selectByConversationId(conversationId).stream()
.map(WxDebate::getContent)
.reduce("", (a, b) -> a + b);
}
StringBuilder requestBodyBuilder = new StringBuilder();
requestBodyBuilder.append("{")
.append("\"messages\": [")
@ -74,7 +98,17 @@ public class WxDebateServiceImpl implements WxDebateService {
log.info("请求体:{}", requestBody);
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
JSONObject json = new JSONObject(response.getBody());
JSONArray choices = json.getJSONArray("choices");
if (!choices.isEmpty()) {
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
return message.getString("content");
}
}
return null;
} catch (Exception e){
log.error("模拟辩论获取失败", e);
return null;
@ -90,4 +124,34 @@ public class WxDebateServiceImpl implements WxDebateService {
.replace("\r", "\\r")
.replace("\t", "\\t");
}
@Override
public long UpdateDebate(Long conversationId, String content, String userMessage, String token) {
long relatedConversationId = conversationId;
if(conversationId == -1){
int userId = jwtUtil.getUserIdFromToken(token);
WxConversation wxConversation = new WxConversation();
wxConversation.setUserId(userId);
wxConversation.setType("debate");
wxConversation.setTitle("");
wxConversation.setPreview(content.substring(0, Math.min(content.length(), 10)));
wxConversation.setCreateTime(new Date());
wxConversation.setUpdateTime(new Date());
wxConversationMapper.insert(wxConversation);
relatedConversationId = wxConversation.getId();
}
else{
wxConversationMapper.updatePreview(conversationId, content.substring(0, Math.min(content.length(), 10)));
}
WxDebate wxDebate = new WxDebate();
wxDebate.setConversationId(relatedConversationId);
wxDebate.setContent(content);
wxDebate.setUserMessage(userMessage);
wxDebate.setSequence(wxDebateMapper.selectByConversationId(relatedConversationId).size() + 1);
wxDebate.setCreateTime(new Date());
wxDebateMapper.insert(wxDebate);
return relatedConversationId;
}
}

@ -1,17 +1,29 @@
package com.learning.newdemo.service.impl;
import com.learning.newdemo.entity.WxReview;
import com.learning.newdemo.entity.WxConversation;
import com.learning.newdemo.mapper.WxConversationMapper;
import com.learning.newdemo.mapper.WxReviewMapper;
import com.learning.newdemo.service.WxReviewService;
import com.learning.newdemo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
@Service
@Slf4j
public class WxReviewServiceImpl implements WxReviewService {
private final RestTemplate restTemplate;
private final WxReviewMapper wxReviewMapper;
private final WxConversationMapper wxConversationMapper;
private final JwtUtil jwtUtil;
@Value("${ai.argument.header.Authorization}") private String authorizationHeader;
@Value("${ai.argument.body.message.role-sys}") private String roleSys;
@ -29,8 +41,11 @@ public class WxReviewServiceImpl implements WxReviewService {
@Value("${ai.argument.body.logprobs}") private boolean logprobs;
@Value("${ai.argument.url}") private String url;
public WxReviewServiceImpl(RestTemplate restTemplate) {
public WxReviewServiceImpl(RestTemplate restTemplate, WxReviewMapper wxReviewMapper, WxConversationMapper wxConversationMapper, JwtUtil jwtUtil) {
this.restTemplate = restTemplate;
this.wxReviewMapper = wxReviewMapper;
this.wxConversationMapper = wxConversationMapper;
this.jwtUtil = jwtUtil;
}
@Override
@ -49,7 +64,7 @@ public class WxReviewServiceImpl implements WxReviewService {
+ "},"
+ "{"
+ "\"role\": \"" + roleUser + "\","
+ "\"content\": \"" + content + "\""
+ "\"content\": \"" + escapeJson(content) + "\""
+ "}"
+ "],"
+ "\"model\": \"" + model + "\","
@ -72,7 +87,16 @@ public class WxReviewServiceImpl implements WxReviewService {
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
JSONObject json = new JSONObject(response.getBody());
JSONArray choices = json.getJSONArray("choices");
if (!choices.isEmpty()) {
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
return message.getString("content");
}
}
return null;
} catch (Exception e) {
log.error("向AI获取立论失败", e);
return null;
@ -82,10 +106,53 @@ public class WxReviewServiceImpl implements WxReviewService {
// 工具方法:转义 JSON 字符串中的特殊字符
private String escapeJson(String input) {
if (input == null) return "";
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
StringBuilder escaped = new StringBuilder();
for (char c : input.toCharArray()) {
switch (c) {
case '"': escaped.append("\\\""); break;
case '\\': escaped.append("\\\\"); break;
case '\b': escaped.append("\\b"); break;
case '\f': escaped.append("\\f"); break;
case '\n': escaped.append("\\n"); break;
case '\r': escaped.append("\\r"); break;
case '\t': escaped.append("\\t"); break;
default:
if (c <= 0x1F) { // 处理其他控制字符
escaped.append(String.format("\\u%04x", (int) c));
} else {
escaped.append(c);
}
}
}
return escaped.toString();
}
@Override
public long UpdateReview(Long conversationId, String content, String userMessage, String token) {
long relatedConversationId = conversationId;
if(conversationId == -1){
int userId = jwtUtil.getUserIdFromToken(token);
WxConversation wxConversation = new WxConversation();
wxConversation.setUserId(userId);
wxConversation.setType("review");
wxConversation.setTitle("");
wxConversation.setPreview(content.substring(0, Math.min(content.length(), 10)));
wxConversation.setCreateTime(new Date());
wxConversation.setUpdateTime(new Date());
wxConversationMapper.insert(wxConversation);
relatedConversationId = wxConversation.getId();
}
else{
wxConversationMapper.updatePreview(conversationId, content.substring(0, Math.min(content.length(), 10)));
}
WxReview wxReview = new WxReview();
wxReview.setConversationId(relatedConversationId);
wxReview.setContent(content);
wxReview.setUserMessage(userMessage);
wxReview.setSequence(wxReviewMapper.selectByConversationId(relatedConversationId).size() + 1);
wxReview.setCreateTime(new Date());
wxReviewMapper.insert(wxReview);
return relatedConversationId;
}
}

@ -62,10 +62,10 @@ public class WxUserServiceImpl implements WxUserService {
}
// 根据openid获取或创建用户
getUserByOpenid(openid);
int userId = getUserByOpenid(openid);
// 生成jwt token
return jwtUtil.generateToken(openid);
return jwtUtil.generateToken(userId);
} catch (Exception e) {
log.error("微信登录异常", e);
return null;
@ -73,7 +73,7 @@ public class WxUserServiceImpl implements WxUserService {
}
@Override
public WxUser getUserByOpenid(String openid) {
public int getUserByOpenid(String openid) {
WxUser wxUser = wxUserMapper.selectByOpenid(openid);
if (wxUser == null) {
// 如果用户不存在,创建新用户
@ -82,7 +82,7 @@ public class WxUserServiceImpl implements WxUserService {
wxUserMapper.insert(wxUser);
wxUser = wxUserMapper.selectByOpenid(openid);
}
return wxUser;
return wxUser.getId().intValue();
}
@Override

@ -1,112 +1,126 @@
package com.learning.newdemo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT
*/
@Component
public class JwtUtil {
/**
*
*/
@Value("${jwt.secret}")
private String secret;
/**
*
*/
@Value("${jwt.expiration}")
private Long expiration;
/**
* token
*/
public String generateToken(String openid) {
Map<String, Object> claims = new HashMap<>();
claims.put("openid", openid);
return generateToken(claims);
}
/**
* tokenopenid
*/
public String getOpenidFromToken(String token) {
String openid;
try {
Claims claims = getClaimsFromToken(token);
openid = (String) claims.get("openid");
} catch (Exception e) {
openid = null;
}
return openid;
}
/**
* token
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* tokenJWT
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
// 使用正确的方法: parseClaimsJws而不是parseClaimsJwt
claims = Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* token
*/
private String generateToken(Map<String, Object> claims) {
Date createdDate = new Date();
Date expirationDate = new Date(createdDate.getTime() + expiration);
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256);
return builder.compact();
}
/**
*
*/
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
package com.learning.newdemo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT
*/
@Slf4j
@Component
public class JwtUtil {
/**
*
*/
@Value("${jwt.secret}")
private String secret;
/**
*
*/
@Value("${jwt.expiration}")
private Long expiration;
/**
* token
*/
public String generateToken(int userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
return generateToken(claims);
}
/**
* tokenopenid
*/
public int getUserIdFromToken(String bearerToken) {
log.info("test:",bearerToken);
int userId;
try {
String token = extractToken(bearerToken);
Claims claims = getClaimsFromToken(token);
userId = (int) claims.get("userId");
} catch (Exception e) {
userId = -1;
}
return userId;
}
/**
* token
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* tokenJWT
*/
private Claims getClaimsFromToken(String token) {
log.info("111111:",token);
Claims claims = null;
try {
// 使用正确的方法: parseClaimsJws而不是parseClaimsJwt
claims = Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
System.out.println("Token解析失败: " + e.getMessage());
}
return claims;
}
/**
* token
*/
private String generateToken(Map<String, Object> claims) {
Date createdDate = new Date();
Date expirationDate = new Date(createdDate.getTime() + expiration);
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256);
return builder.compact();
}
/**
*
*/
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* token
*/
public String extractToken(String bearerToken){
if(bearerToken != null && bearerToken.startsWith("Bearer "))
return bearerToken.substring(7);
return bearerToken;
}
}

@ -1,22 +0,0 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS wx_miniapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE wx_miniapp;
-- 创建微信用户表
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxArgumentMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxArgument">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="topic" property="topic" jdbcType="VARCHAR"/>
<result column="stance" property="stance" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 可复用的列名列表 -->
<sql id="Base_Column_List">
id, conversation_id, topic, stance, content, user_message, sequence, create_time
</sql>
<!-- 按 conversationId 查询并按 sequence 排序 -->
<select id="selectByConversationId" resultMap="BaseResultMap" parameterType="java.lang.Long">
SELECT
<include refid="Base_Column_List"/>
FROM wx_argument_record
WHERE conversation_id = #{conversationId,jdbcType=BIGINT}
ORDER BY sequence
</select>
<!-- 插入新记录 -->
<insert id="insert" parameterType="com.learning.newdemo.entity.WxArgument" useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_argument_record (
conversation_id, topic, stance, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{topic,jdbcType=VARCHAR},
#{stance,jdbcType=VARCHAR},
#{content,jdbcType=LONGVARCHAR},
#{userMessage,jdbcType=LONGVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime,jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learning.newdemo.mapper.WxConversationMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxConversation">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="type" property="type" jdbcType="VARCHAR"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="preview" property="preview" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 可复用的列名列表 -->
<sql id="Base_Column_List">
id, user_id, type, title, preview, create_time, update_time
</sql>
<!-- 按用户ID和类型查询对话活动 (用于接口4.1) -->
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM wx_conversation
WHERE user_id = #{userId}
ORDER BY update_time DESC
</select>
<!-- 插入新对话活动 (用于接口3.1/3.2/3.3) -->
<insert id="insert" parameterType="com.learning.newdemo.entity.WxConversation"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_conversation (
user_id, type, title, preview, create_time, update_time
)
VALUES (
#{userId}, #{type}, #{title}, #{preview}, #{createTime}, #{updateTime}
)
</insert>
<!-- 更新对话预览信息 (用于更新最后一次AI回复的预览) -->
<update id="updatePreview">
UPDATE wx_conversation
SET
preview = #{preview},
update_time = NOW()
WHERE id = #{conversationId}
</update>
</mapper>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxDebateMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxDebate">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, conversation_id, content, user_message, sequence, create_time
</sql>
<select id="selectByConversationId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from wx_debate_record
where conversation_id = #{conversationId}
ORDER BY sequence
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxDebate"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_debate_record (
conversation_id, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{content,jdbcType=LONGNVARCHAR},
#{userMessage,jdbcType=LONGNVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime, jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxReviewMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxReview">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, conversation_id, content, user_message, sequence, create_time
</sql>
<select id="selectByConversationId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from wx_review_record
where conversation_id = #{conversationId}
ORDER BY sequence
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxDebate"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_review_record (
conversation_id, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{content,jdbcType=LONGNVARCHAR},
#{userMessage,jdbcType=LONGNVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime, jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -1,22 +0,0 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS wx_miniapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE wx_miniapp;
-- 创建微信用户表
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxArgumentMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxArgument">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="topic" property="topic" jdbcType="VARCHAR"/>
<result column="stance" property="stance" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 可复用的列名列表 -->
<sql id="Base_Column_List">
id, conversation_id, topic, stance, content, user_message, sequence, create_time
</sql>
<!-- 按 conversationId 查询并按 sequence 排序 -->
<select id="selectByConversationId" resultMap="BaseResultMap" parameterType="java.lang.Long">
SELECT
<include refid="Base_Column_List"/>
FROM wx_argument_record
WHERE conversation_id = #{conversationId,jdbcType=BIGINT}
ORDER BY sequence
</select>
<!-- 插入新记录 -->
<insert id="insert" parameterType="com.learning.newdemo.entity.WxArgument" useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_argument_record (
conversation_id, topic, stance, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{topic,jdbcType=VARCHAR},
#{stance,jdbcType=VARCHAR},
#{content,jdbcType=LONGVARCHAR},
#{userMessage,jdbcType=LONGVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime,jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learning.newdemo.mapper.WxConversationMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxConversation">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="type" property="type" jdbcType="VARCHAR"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="preview" property="preview" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 可复用的列名列表 -->
<sql id="Base_Column_List">
id, user_id, type, title, preview, create_time, update_time
</sql>
<!-- 按用户ID和类型查询对话活动 (用于接口4.1) -->
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM wx_conversation
WHERE user_id = #{userId}
ORDER BY update_time DESC
</select>
<!-- 插入新对话活动 (用于接口3.1/3.2/3.3) -->
<insert id="insert" parameterType="com.learning.newdemo.entity.WxConversation"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_conversation (
user_id, type, title, preview, create_time, update_time
)
VALUES (
#{userId}, #{type}, #{title}, #{preview}, #{createTime}, #{updateTime}
)
</insert>
<!-- 更新对话预览信息 (用于更新最后一次AI回复的预览) -->
<update id="updatePreview">
UPDATE wx_conversation
SET
preview = #{preview},
update_time = NOW()
WHERE id = #{conversationId}
</update>
</mapper>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxDebateMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxDebate">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, conversation_id, content, user_message, sequence, create_time
</sql>
<select id="selectByConversationId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from wx_debate_record
where conversation_id = #{conversationId}
ORDER BY sequence
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxDebate"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_debate_record (
conversation_id, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{content,jdbcType=LONGNVARCHAR},
#{userMessage,jdbcType=LONGNVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime, jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxReviewMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxReview">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="conversation_id" property="conversationId" jdbcType="BIGINT"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="user_message" property="userMessage" jdbcType="LONGVARCHAR"/>
<result column="sequence" property="sequence" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, conversation_id, content, user_message, sequence, create_time
</sql>
<select id="selectByConversationId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from wx_review_record
where conversation_id = #{conversationId}
ORDER BY sequence
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxDebate"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wx_review_record (
conversation_id, content, user_message, sequence, create_time
)
VALUES (
#{conversationId,jdbcType=BIGINT},
#{content,jdbcType=LONGNVARCHAR},
#{userMessage,jdbcType=LONGNVARCHAR},
#{sequence,jdbcType=INTEGER},
#{createTime, jdbcType=TIMESTAMP}
)
</insert>
</mapper>

@ -1,58 +1,58 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxUserMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxUser">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="openid" property="openid" jdbcType="VARCHAR"/>
<result column="nickname" property="nickname" jdbcType="VARCHAR"/>
<result column="avatar_url" property="avatarUrl" jdbcType="VARCHAR"/>
<result column="gender" property="gender" jdbcType="INTEGER"/>
<result column="country" property="country" jdbcType="VARCHAR"/>
<result column="province" property="province" jdbcType="VARCHAR"/>
<result column="city" property="city" jdbcType="VARCHAR"/>
<result column="language" property="language" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, openid, nickname, avatar_url, gender, country, province, city, language, create_time, update_time
</sql>
<select id="selectByOpenid" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List"/>
from wx_user
where openid = #{openid,jdbcType=VARCHAR}
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxUser" useGeneratedKeys="true" keyProperty="id">
insert into wx_user (
openid, nickname, avatar_url, gender, country, province, city, language, create_time
)
values (
#{openid,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR},
#{avatarUrl,jdbcType=VARCHAR},
#{gender,jdbcType=INTEGER},
#{country,jdbcType=VARCHAR},
#{province,jdbcType=VARCHAR},
#{city,jdbcType=VARCHAR},
#{language,jdbcType=VARCHAR},
now()
)
</insert>
<update id="updateByPrimaryKey" parameterType="com.learning.newdemo.entity.WxUser">
update wx_user
set nickname = #{nickname,jdbcType=VARCHAR},
avatar_url = #{avatarUrl,jdbcType=VARCHAR},
gender = #{gender,jdbcType=INTEGER},
country = #{country,jdbcType=VARCHAR},
province = #{province,jdbcType=VARCHAR},
city = #{city,jdbcType=VARCHAR},
language = #{language,jdbcType=VARCHAR},
update_time = now()
where id = #{id,jdbcType=INTEGER}
</update>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxUserMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxUser">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="openid" property="openid" jdbcType="VARCHAR"/>
<result column="nickname" property="nickname" jdbcType="VARCHAR"/>
<result column="avatar_url" property="avatarUrl" jdbcType="VARCHAR"/>
<result column="gender" property="gender" jdbcType="INTEGER"/>
<result column="country" property="country" jdbcType="VARCHAR"/>
<result column="province" property="province" jdbcType="VARCHAR"/>
<result column="city" property="city" jdbcType="VARCHAR"/>
<result column="language" property="language" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, openid, nickname, avatar_url, gender, country, province, city, language, create_time, update_time
</sql>
<select id="selectByOpenid" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List"/>
from wx_user
where openid = #{openid,jdbcType=VARCHAR}
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxUser" useGeneratedKeys="true" keyProperty="id">
insert into wx_user (
openid, nickname, avatar_url, gender, country, province, city, language, create_time
)
values (
#{openid,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR},
#{avatarUrl,jdbcType=VARCHAR},
#{gender,jdbcType=INTEGER},
#{country,jdbcType=VARCHAR},
#{province,jdbcType=VARCHAR},
#{city,jdbcType=VARCHAR},
#{language,jdbcType=VARCHAR},
now()
)
</insert>
<update id="updateByPrimaryKey" parameterType="com.learning.newdemo.entity.WxUser">
update wx_user
set nickname = #{nickname,jdbcType=VARCHAR},
avatar_url = #{avatarUrl,jdbcType=VARCHAR},
gender = #{gender,jdbcType=INTEGER},
country = #{country,jdbcType=VARCHAR},
province = #{province,jdbcType=VARCHAR},
city = #{city,jdbcType=VARCHAR},
language = #{language,jdbcType=VARCHAR},
update_time = now()
where id = #{id,jdbcType=INTEGER}
</update>
</mapper>

@ -23,7 +23,11 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-xhs": "3.0.0-4040520250104002",
"@dcloudio/uni-quickapp-webview": "3.0.0-4040520250104002",
"markdown-it": "^14.1.0",
"marked": "^15.0.12",
"mini-html-parser2": "^0.3.0",
"pinia": "^3.0.2",
"towxml": "^3.0.6",
"vue": "^3.5.15",
"vue-i18n": "^9.1.9"
},
@ -5670,6 +5674,12 @@
"node_modules/dom-walk": {
"version": "0.1.2"
},
"node_modules/domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"license": "BSD-2-Clause"
},
"node_modules/domexception": {
"version": "2.0.1",
"dev": true,
@ -5691,6 +5701,15 @@
"node": ">=8"
}
},
"node_modules/domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"dev": true,
@ -6276,6 +6295,15 @@
"node": ">= 0.6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "5.1.1",
"dev": true,
@ -7995,6 +8023,15 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/load-bmfont": {
"version": "1.4.2",
"license": "MIT",
@ -8148,6 +8185,41 @@
"tmpl": "1.0.5"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"dev": true,
@ -8156,6 +8228,12 @@
"node": ">= 0.4"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/media-typer": {
"version": "0.3.0",
"dev": true,
@ -8271,6 +8349,22 @@
"dom-walk": "^0.1.0"
}
},
"node_modules/mini-html-parser2": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/mini-html-parser2/-/mini-html-parser2-0.3.0.tgz",
"integrity": "sha512-W4x1MCmtlnAH5M9qQ1WbRn+hTvv7bdrJx4VI+6SD0MUZatW/6K7v213Aidx7VDQmSKoRv+iAn5TswJnesOs71Q==",
"dependencies": {
"domhandler": "^2.4.2",
"entities": "^1.1.1",
"events": "^3.0.0"
}
},
"node_modules/mini-html-parser2/node_modules/entities": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"license": "BSD-2-Clause"
},
"node_modules/minimatch": {
"version": "3.1.2",
"dev": true,
@ -8965,6 +9059,15 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode-reader": {
"version": "1.0.4",
"license": "Apache-2.0"
@ -10137,6 +10240,47 @@
"node": ">= 4.0.0"
}
},
"node_modules/towxml": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/towxml/-/towxml-3.0.6.tgz",
"integrity": "sha512-AiEEyTYemk3xLlypfihR+vr/slyjvQZvZbGGXQdRklwYY8dbDEGUDopFwAtfW7tVkOc09rfZ7yu040vjJU84cw==",
"license": "MIT",
"dependencies": {
"fs-extra": "^8.1.0"
}
},
"node_modules/towxml/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/towxml/node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/towxml/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "2.1.0",
"dev": true,
@ -10191,6 +10335,12 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.5.4",
"license": "MIT"

@ -25,7 +25,11 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-xhs": "3.0.0-4040520250104002",
"@dcloudio/uni-quickapp-webview": "3.0.0-4040520250104002",
"markdown-it": "^14.1.0",
"marked": "^15.0.12",
"mini-html-parser2": "^0.3.0",
"pinia": "^3.0.2",
"towxml": "^3.0.6",
"vue": "^3.5.15",
"vue-i18n": "^9.1.9"
},

@ -1,13 +1,20 @@
{
"miniprogramRoot": "",
"miniprogramRoot": "./",
"libVersion": "3.0.2",
"appid": "wxdc5c8df2ec2453e4",
"compileType": "miniprogram",
"packOptions": {
"ignore": [],
"ignore": [{ "type": "folder", "value": "node_modules" }],
"include": []
},
"setting": {
"packNpmManmediately": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./"
}
],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
@ -19,4 +26,4 @@
"tabIndent": "insertSpaces",
"tabSize": 2
}
}
}

@ -24,6 +24,11 @@
>AI辅助构建辩论框架智能生成论点与论据</view
>
</view>
<view class="card-icon-wrapper">
<view class="card-icon" @click.stop="showHistory = true">
<image src="/static/icons/list.png" mode="aspectFit"></image>
</view>
</view>
<view class="collapse-icon">
<image
:src="
@ -43,6 +48,9 @@
scroll-y
:scroll-into-view="scrollToView"
scroll-with-animation
:class="{ 'fullscreen-chat': isChatFullscreen }"
@transitionend="handleTransitionEnd"
:style="{ height: chatAreaHeight }"
>
<view
v-for="(msg, index) in messages"
@ -62,30 +70,50 @@
</view>
</block>
<block v-else>
{{ msg.content }}
<text class="typewriter" v-if="msg.role === 'ai' && msg.typing">{{ msg.displayContent }}</text>
<rich-text
v-else
:nodes="parseMarkdown(msg.content)"
class="markdown-content"
></rich-text>
</block>
</view>
<view class="avatar user-avatar" v-if="msg.role === 'user'"></view>
</view>
<!-- 全屏按钮 -->
<view class="fullscreen-toggle-button" @click="toggleFullscreen">
<image
:src="
isChatFullscreen
? '/static/icons/fullscreen-exit-line.png'
: '/static/icons/fullscreen-line.png'
"
mode="aspectFit"
></image>
</view>
</scroll-view>
<!-- 输入框与发送按钮 -->
<view class="chat-input">
<view class="chat-input" :class="{ 'fullscreen-input': isChatFullscreen }" :style="{ bottom: inputBottom }">
<view class="input-group">
<input
class="input-box"
type="text"
v-model="position"
placeholder="请输入论方,例如:正方"
placeholder-style="color: #ffffff; font-size: 28rpx;"
/>
<view class="textarea-container">
<view class="textarea-wrapper">
<textarea
class="textarea-box"
v-model="question"
placeholder="请输入辩题,例如:应不应该取消作业"
placeholder-style="color: #ffffff; font-size: 28rpx;"
auto-height
@focus="onInputFocus"
@blur="onInputBlur"
/>
<button class="send-button-embedded" @click="sendMessage()">
<button class="send-button" @click="sendMessage()">
<image
src="/static/icons/send-plane-fill.png"
mode="aspectFit"
@ -104,54 +132,222 @@
@close="showSheet = false"
@click="handleSheetClick"
/>
<ConversationHistory
:visible="showHistory"
:history-list="chatHistory"
:type="2"
@update:visible="showHistory = $event"
@select="handleSelect"
@createNew="createNew"
/>
</view>
</template>
<script>
import { useArgumentStore } from "../stores/ArgumentStore";
import { useTokenStore } from "../stores/tokenStore";
import Popup from "./Popup.vue";
import ConversationHistory from "./ConversationHistory.vue";
import { marked } from "marked";
export default {
components: { Popup },
components: { Popup, ConversationHistory },
mounted() {
this.getHistoryList();
this.conversationId = useArgumentStore().conversationId;
console.log("conversationId:", this.conversationId);
this.messages = useArgumentStore().conversation;
this.calculateChatAreaHeight();
this.systemInfo = uni.getSystemInfoSync();
this.tabBarHeight = this.systemInfo.platform === 'ios' ? 100 : 80;
},
data() {
return {
conversationId: -1,
showSheet: false,
showHistory: false,
storeArg: "",
input: "",
messages: [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
displayContent: "",
typing: false,
},
],
scrollToView: "",
position: "",
question: "",
isCardCollapsed: false, //
isCardCollapsed: false,
isChatFullscreen: false,
isTransitioning: false,
inputBottom: '100rpx', // TabBar
keyboardHeight: 0,
chatAreaHeight: 'calc(100vh - 400rpx)',
systemInfo: null,
tabBarHeight: 100,
chatHistory: ["test1", "test2"],
};
},
onLoad() {
uni.onKeyboardHeightChange((res) => {
this.keyboardHeight = res.height;
this.adjustLayout();
});
},
onUnload() {
uni.offKeyboardHeightChange();
},
methods: {
calculateChatAreaHeight() {
//
const cardHeight = this.isCardCollapsed ? 120 : 180;
const inputHeight = 200;
const safeArea = this.systemInfo?.safeAreaInsets?.bottom || 0;
this.chatAreaHeight = `calc(100vh - ${cardHeight + inputHeight + safeArea}rpx)`;
},
adjustLayout() {
if (this.keyboardHeight > 0) {
//
this.inputBottom = `${this.keyboardHeight * 2 + this.tabBarHeight}rpx`;
this.chatAreaHeight = `calc(100vh - 300rpx - ${this.keyboardHeight * 2}rpx)`;
} else {
//
this.inputBottom = `${this.tabBarHeight}rpx`;
this.calculateChatAreaHeight();
}
this.scrollToBottom();
},
toggleFullscreen() {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.isChatFullscreen = !this.isChatFullscreen;
this.adjustLayout();
},
handleTransitionEnd() {
this.isTransitioning = false;
},
onInputFocus() {
this.scrollToBottom();
},
onInputBlur() {
//
setTimeout(() => {
if (!this.isChatFullscreen) {
this.adjustLayout();
}
}, 200);
},
async getHistoryList() {
return new Promise((resolve, reject) => {
const token = useTokenStore().token.content;
console.log("token:", token);
uni.request({
url: "http://localhost:8080/api/conversation/list",
method: "GET",
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
console.log("res:", res);
if (res.statusCode === 200 && res.data.code === 200) {
const argumentList = res.data.data.filter(
(item) => item.type === "argument"
);
this.chatHistory = argumentList;
if (argumentList.length > 0) {
resolve(argumentList);
} else {
resolve("没有找到类型为 argument 的对话");
}
} else {
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
createNew() {
this.messages = [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
displayContent: "",
typing: false,
},
];
console.log("createNew");
this.conversationId = -1;
useArgumentStore().setConversationId(-1);
useArgumentStore().setConversation(this.messages);
},
handleSelect(conversationId) {
console.log("选中了历史记录 id:", conversationId);
this.showHistory = false;
return new Promise((resolve, reject) => {
const token = useTokenStore().token.content;
console.log("token:", token);
uni.request({
url: `http://localhost:8080/api/conversation/argument/${conversationId}`,
method: "GET",
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
console.log("res:", res);
if (res.statusCode === 200 && res.data.code === 200) {
this.messages = [];
for (let item of res.data.data) {
this.messages.push({
role: "user",
content: item.userMessage,
displayContent: item.userMessage,
typing: false,
});
this.messages.push({
role: "ai",
content: item.content,
displayContent: item.content,
typing: false,
});
}
useArgumentStore().setConversation(this.messages);
this.conversationId = conversationId;
useArgumentStore().setConversationId(conversationId);
} else {
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
handleSheetClick() {
const pinia = this.$pinia;
const argumentStore = useArgumentStore(pinia);
argumentStore.setArgument(this.storeArg);
this.$emit("start-debate", "argument"); //
this.$emit("start-debate", "argument");
},
// /
toggleCard() {
this.isCardCollapsed = !this.isCardCollapsed;
this.calculateChatAreaHeight();
},
//
getRandomCircleStyle() {
const size = Math.random() * 300 + 100;
const x = Math.random() * 100;
const y = Math.random() * 100;
const delay = Math.random() * 5;
const duration = Math.random() * 10 + 15;
return {
width: `${size}rpx`,
height: `${size}rpx`,
@ -161,92 +357,191 @@ export default {
animationDuration: `${duration}s`,
};
},
async sendMessage() {
//
if (!this.position.trim() || !this.question.trim()) return;
this.messages.push({
role: "user",
content: this.position + " " + this.question,
displayContent: this.position + " " + this.question,
typing: false,
});
this.scrollToBottom();
// AI loading
const aiIndex = this.messages.length;
this.messages.push({ role: "ai", content: "", loading: true });
this.messages.push({ role: "ai", content: "", displayContent: "", loading: true, typing: false });
this.scrollToBottom();
// AI
const reply = await this.callAI(this.position, this.question);
// loading AI
this.messages.splice(aiIndex, 1, { role: "ai", content: reply });
this.messages.splice(aiIndex, 1, {
role: "ai",
content: reply,
displayContent: "",
typing: true,
});
this.scrollToBottom();
this.typeMessage(aiIndex, reply);
},
async callAI(topic, stance) {
this.position = "";
this.question = "";
return new Promise((resolve, reject) => {
console.log("callAI:", topic, stance);
const token = useTokenStore().token.content;
if (!token) {
reject("未登录,请先登录");
return;
}
uni.request({
url: "http://localhost:8080/api/ai/argument",
method: "POST",
header: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
data: {
conversationId: this.conversationId,
topic: topic,
stance: stance,
},
success: (res) => {
console.log("res:", res);
let reviewJson = JSON.parse(res.data.data.argument);
console.log("API响应:", res);
if (res.statusCode === 200 && res.data.code === 200) {
resolve(
reviewJson.choices[0].message.content || "AI 没有返回有效内容"
);
const responseData = res.data.data;
if (this.conversationId == -1) {
this.conversationId = responseData.conversationId;
console.log("更新会话ID为", responseData.conversationId);
this.getHistoryList();
useArgumentStore().setConversationId(this.conversationId);
}
// review
const cleanedArgument =
responseData.content?.replace(/`/g, "") || "";
resolve(cleanedArgument);
} else {
reject("请求失败:" + (res.data.msg || "未知错误"));
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
console.error("请求失败:", err);
reject("请求失败:" + err.errMsg);
},
});
});
},
typeMessage(index, fullText) {
let currentLength = 0;
const speed = 50;
const type = () => {
if (currentLength <= fullText.length) {
this.messages[index].displayContent = fullText.slice(0, currentLength);
currentLength++;
setTimeout(type, speed);
} else {
this.messages[index].typing = false;
this.scrollToBottom();
}
};
type();
},
parseMarkdown(markdown) {
//
let html = marked(markdown || "");
//
html = html.replace(/<script.*?>.*?<\/script>/gi, "");
return html;
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollToView = "msg" + (this.messages.length - 1);
});
},
onLongPress(msg, index) {
console.log("长按消息", msg, index);
if (msg.role !== "ai") return;
this.storeArg = msg;
this.showSheet = true;
// uni.showActionSheet({
// itemList: [""],
// success: (res) => {
// if (res.tapIndex === 0) {
// const pinia = this.$pinia;
// const argumentStore = useArgumentStore(pinia);
// argumentStore.setArgument(msg);
// this.$emit("start-debate", msg); //
// }
// },
// });
},
},
};
</script>
<style scoped>
/* 添加 Markdown 内容的基础样式 */
.markdown-content {
width: 100%; /* 确保宽度不超出容器 */
overflow: hidden; /* 隐藏溢出内容 */
overflow-wrap: break-word;
overflow-y: auto;
overflow-x: auto;
}
/* 标题样式 */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin: 16rpx 0;
font-weight: bold;
}
.markdown-content h1 {
font-size: 36rpx;
}
.markdown-content h2 {
font-size: 32rpx;
}
/* 列表样式 */
.markdown-content ul,
.markdown-content ol {
padding-left: 40rpx;
margin: 12rpx 0;
}
.markdown-content li {
margin: 8rpx 0;
}
/* 代码块样式 */
.markdown-content a {
word-break: break-all; /* 长链接强制换行 */
}
.markdown-content pre {
white-space: pre-wrap; /* 保留空格但换行 */
word-break: break-all; /* 允许任意字符换行 */
overflow-wrap: break-word; /* 优先在单词间换行 */
background: rgba(0, 0, 0, 1);
padding: 12rpx;
border-radius: 8rpx;
}
/* 引用样式 */
.markdown-content blockquote {
border-left: 4rpx solid #f59e0b;
padding-left: 20rpx;
margin: 16rpx 0;
color: rgba(255, 255, 255, 0.7);
}
/* 表格样式 */
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 16rpx 0;
}
.markdown-content th,
.markdown-content td {
border: 1rpx solid rgba(255, 255, 255, 0.2);
padding: 12rpx;
}
.markdown-content th {
background-color: rgba(255, 255, 255, 0.1);
}
.argument-component {
width: 100%;
height: 100vh;
@ -256,11 +551,9 @@ export default {
background: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
overflow-x: hidden;
box-sizing: border-box;
padding-bottom: 180rpx; /* 为底部TabBar留出空间 */
position: relative;
}
/* 动态背景 */
.animated-bg {
position: absolute;
top: 0;
@ -281,13 +574,11 @@ export default {
);
animation: float 20s infinite ease-in-out;
opacity: 0.4;
/* 添加过渡效果 */
transition: all 1.5s ease-in-out;
}
@keyframes float {
0%,
100% {
0%, 100% {
transform: translate(0, 0) scale(1);
}
25% {
@ -301,7 +592,6 @@ export default {
}
}
/* 顶部卡片 */
.content-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
@ -398,8 +688,7 @@ export default {
0% {
transform: translateX(-100%) rotate(45deg);
}
20%,
100% {
20%, 100% {
transform: translateX(100%) rotate(45deg);
}
}
@ -443,7 +732,6 @@ export default {
transition: all 0.3s ease;
}
/* 聊天区域 */
.chat-area {
flex: 1;
width: 100%;
@ -455,10 +743,82 @@ export default {
border: 1px solid rgba(255, 255, 255, 0.15);
position: relative;
z-index: 1;
transition: all 0.3s ease;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.chat-area.fullscreen-chat {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: calc(100vh - 300rpx - env(safe-area-inset-bottom));
padding: 30rpx;
border-radius: 0;
z-index: 999;
background: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
transform: scale(1);
animation: expand 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes expand {
0% {
transform: scale(0.95);
opacity: 0.8;
border-radius: 28rpx;
}
100% {
transform: scale(1);
opacity: 1;
border-radius: 0;
}
}
.chat-area:not(.fullscreen-chat) {
animation: collapse 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes collapse {
0% {
transform: scale(1);
opacity: 1;
border-radius: 0;
}
100% {
transform: scale(0.95);
opacity: 0.8;
border-radius: 28rpx;
}
}
.fullscreen-toggle-button {
position: fixed;
right: 10rpx;
top: 50%;
transform: translateY(-50%) scale(1);
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
z-index: 1000;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
.fullscreen-toggle-button:active {
transform: translateY(-50%) scale(0.9);
}
.fullscreen-toggle-button image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
}
/* 消息通用样式 */
.chat-message {
display: flex;
margin-bottom: 30rpx;
@ -485,7 +845,6 @@ export default {
justify-content: flex-start;
}
/* 头像样式 */
.avatar {
width: 70rpx;
height: 70rpx;
@ -507,7 +866,6 @@ export default {
margin-right: 0;
}
/* 气泡样式 */
.bubble {
max-width: 70%;
padding: 20rpx 24rpx;
@ -517,7 +875,7 @@ export default {
word-break: break-word;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
position: relative;
transition: all 0.3s ease; /* 添加过渡效果 */
transition: all 0.3s ease;
}
.from-user .bubble {
@ -536,7 +894,7 @@ export default {
height: 0;
border-left: 16rpx solid #f59e0b;
border-top: 16rpx solid transparent;
transition: all 0.3s ease; /* 添加过渡效果 */
transition: all 0.3s ease;
}
.from-ai .bubble {
@ -555,10 +913,15 @@ export default {
height: 0;
border-right: 16rpx solid rgba(255, 255, 255, 0.2);
border-top: 16rpx solid transparent;
transition: all 0.3s ease; /* 添加过渡效果 */
transition: all 0.3s ease;
}
.typewriter {
display: inline-block;
overflow: hidden;
white-space: pre-wrap;
}
/* 加载动画 */
.loading-animation {
display: flex;
align-items: center;
@ -584,9 +947,7 @@ export default {
}
@keyframes bounce {
0%,
80%,
100% {
0%, 80%, 100% {
transform: scale(0);
}
40% {
@ -594,7 +955,6 @@ export default {
}
}
/* 输入区域 */
.chat-input {
display: flex;
padding: 16rpx;
@ -602,35 +962,49 @@ export default {
border-radius: 28rpx;
align-items: flex-end;
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 1;
margin-bottom: 20rpx;
bottom: var(--input-bottom, 100rpx);
z-index: 100;
margin-bottom: 180rpx;
box-sizing: border-box;
transition: all 0.3s ease;
}
.send-button.left-send {
margin-right: 12rpx;
margin-left: 0;
background: #f59e0b;
width: 70rpx;
height: 70rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 15rpx rgba(245, 158, 11, 0.4);
transition: all 0.3s;
padding: 0;
.chat-input.fullscreen-input {
border-radius: 0;
padding: 20rpx;
background: rgba(67, 56, 202, 0.9);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
left: 0;
right: 0;
z-index: 1000;
position: fixed;
margin-bottom: 180rpx;
}
.send-button.left-send:active {
transform: scale(0.95) rotate(-15deg);
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
@keyframes slideUp {
0% {
transform: translateY(100rpx);
opacity: 0.8;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.send-button.left-send image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
.chat-input:not(.fullscreen-input) {
animation: slideDownInput 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes slideDownInput {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(100rpx);
opacity: 0.8;
}
}
.input-group {
@ -638,6 +1012,19 @@ export default {
display: flex;
flex-direction: column;
gap: 16rpx;
width: 100%;
box-sizing: border-box;
}
.textarea-wrapper {
display: flex;
align-items: flex-end;
width: 100%;
background: rgba(255, 255, 255, 0.2);
padding-right: 10rpx;
padding-bottom: 10rpx;
box-sizing: border-box;
border-radius: 20rpx;
}
.input-box {
@ -649,57 +1036,47 @@ export default {
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s;
height: 70rpx;
}
.textarea-container {
position: relative;
width: 100%;
box-sizing: border-box;
}
.textarea-box {
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 16rpx 20rpx 16rpx 90rpx; /* 增加左侧内边距 */
flex: 1;
padding: 16rpx 20rpx;
font-size: 28rpx;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s;
min-height: 70rpx;
max-height: 180rpx;
width: 100%;
margin-right: 16rpx;
box-sizing: border-box;
background: transparent;
border: none;
}
.send-button-embedded {
position: absolute;
bottom: 16rpx;
left: 16rpx;
background: transparent;
width: 60rpx;
height: 60rpx;
.send-button {
background: #f59e0b;
width: 70rpx;
height: 70rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 15rpx rgba(245, 158, 11, 0.4);
transition: all 0.3s;
padding: 0;
z-index: 2;
}
.send-button-embedded:active {
transform: scale(1.2) rotate(-15deg);
flex-shrink: 0;
}
.send-button-embedded image {
width: 40rpx;
height: 40rpx;
filter: invert(72%) sepia(87%) saturate(1242%) hue-rotate(325deg)
brightness(101%) contrast(96%);
.send-button:active {
transform: scale(0.95) rotate(-15deg);
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
}
.input-box::placeholder,
.textarea-box::placeholder {
color: burlywood;
.send-button image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
}
.input-box:focus,
@ -709,10 +1086,7 @@ export default {
background: rgba(255, 255, 255, 0.25);
}
/* 底部安全区域 */
.safe-area-bottom {
height: 20rpx;
height: calc(20rpx + constant(safe-area-inset-bottom)); /* iOS 11.0-11.2 */
height: calc(20rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
height: calc(100rpx + env(safe-area-inset-bottom));
}
</style>
</style>

@ -0,0 +1,403 @@
<template>
<div v-if="visible">
<div class="overlay" @click="close"></div>
<transition name="slide">
<div class="history-sidebar">
<div class="history-header">
<div class="title">💬对话历史</div>
</div>
<div class="create" v-if="type != 0" @click="createNew()">
创建新对话
</div>
<!-- 判断是否有历史记录 -->
<div v-if="historyList.length > 0" class="history-list-container">
<ul class="history-list">
<li
v-for="(item, index) in historyList"
:key="item.id || index"
class="history-item"
@click="onItemClick(item)"
>
<view class="activity-icon" :class="item.type">
<image
:src="getActivityIcon(item.type)"
mode="aspectFit"
></image>
</view>
<view v-if="type == 0" class="activity-content">
<view class="activity-title">{{
item.title ? getWords(item.title) : getWords(item.preview)
}}</view>
<view class="activity-time">{{
formatDate(item.updateTime)
}}</view>
</view>
<view v-if="type == 2" class="activity-content">
<view class="activity-title">{{ getWords(item.title) }}</view>
<view class="activity-time">{{
formatDate(item.updateTime)
}}</view>
</view>
<view v-if="type == 1 || type == 3" class="activity-content">
<view class="activity-title">{{ getWords(item.preview) }}</view>
<view class="activity-time">{{
formatDate(item.updateTime)
}}</view>
</view>
</li>
</ul>
</div>
<!-- 没有数据时显示提示信息 -->
<div v-else class="no-data">
<image
src="/static/icons/no-data.png"
mode="aspectFit"
class="no-data-icon"
/>
<p class="no-data-text">暂无数据</p>
</div>
</div>
</transition>
<view class="animated-bg">
<view
v-for="i in 6"
:key="i"
class="bg-circle"
:style="getRandomCircleStyle()"
></view>
</view>
</div>
</template>
<script>
export default {
name: "ConversationHistory",
props: {
visible: { type: Boolean, required: true },
historyList: { type: Array, default: () => [] },
type: { type: Number, default: 0 },
},
mounted() {
this.startCircleUpdate();
},
data() {
return {
circleStyle: this.getRandomCircleStyle(),
};
},
methods: {
formatDate(updateTime) {
// ISO Date
const updateDate = new Date(updateTime);
const now = new Date();
//
const diffTime = Math.abs(now - updateDate);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 1) {
return "今天";
}
if (diffDays < 3) {
return `${diffDays}天前`;
} else {
// 使 toLocaleDateString
return updateDate.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
}
},
close() {
this.$emit("update:visible", false);
},
onItemClick(item) {
this.$emit("select", item.id);
},
createNew() {
console.log("Creating new conversation...");
this.$emit("createNew");
this.close();
},
//
getRandomCircleStyle() {
const size = Math.random() * 300 + 100;
const x = Math.random() * 100;
const y = Math.random() * 100;
const delay = Math.random() * 5;
const duration = Math.random() * 10 + 15;
return {
width: `${size}rpx`,
height: `${size}rpx`,
left: `${x}%`,
top: `${y}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`,
};
},
startCircleUpdate() {
setInterval(() => {
this.circleStyle = this.getRandomCircleStyle();
}, 5000); // 5
},
getWords(text) {
return text.length > 18 ? text.substring(0, 18) + "..." : text;
// return text;
},
//
getActivityIcon(type) {
const icons = {
debate: "/static/icons/chat-1-line.png",
argument: "/static/icons/lightbulb-line.png",
review: "/static/icons/file-chart-line.png",
};
return icons[type] || icons.debate;
},
},
unmounted() {
clearInterval();
},
};
</script>
<style scoped>
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 50px;
}
.no-data-icon {
width: 200px;
height: 200px;
opacity: 0.6;
}
.no-data-text {
margin-top: 15px;
font-size: 24px;
color: #999;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.4s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateX(-100%);
opacity: 0;
}
.slide-enter-to,
.slide-leave-from {
transform: translateX(0);
opacity: 1;
}
.title {
font-size: 24px;
font-weight: bold;
justify-content: center;
align-content: center;
color: #ffffff;
margin-bottom: 10px;
}
.activity-icon {
width: 60rpx;
height: 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.activity-content {
flex: 1;
}
.activity-icon.debate {
background: rgba(245, 158, 11, 0.3);
}
.activity-icon.argument {
background: rgba(16, 185, 129, 0.3);
}
.activity-icon.review {
background: rgba(79, 70, 229, 0.3);
}
.activity-icon image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 28rpx;
color: #ffffff;
margin-bottom: 6rpx;
}
.activity-time {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.6);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.4);
z-index: 500;
backdrop-filter: blur(4px);
}
.history-sidebar {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 100vh;
background-color: #7951dd;
box-shadow: -6px 0 12px rgba(0, 0, 0, 0.2);
padding: 24px 20px;
overflow-y: auto;
z-index: 800;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.history-header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
font-weight: 600;
letter-spacing: 1px;
}
.create {
font-size: 18px;
font-weight: bold;
color: #ffffff;
background-color: #9269f9;
width: 120px;
height: 40px;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.2s ease; /* 平滑过渡 */
z-index: 999;
}
/* 添加点击时的动画效果 */
.create:active {
animation: clickEffect 0.5s ease forwards;
}
/* 定义动画:点击时缩小一点并加深背景颜色 */
@keyframes clickEffect {
0% {
transform: scale(1);
background-color: #9269f9;
}
50% {
transform: scale(0.95);
background-color: #7a54d8;
}
100% {
transform: scale(1);
background-color: #9269f9;
}
}
.history-list {
list-style: none;
padding: 0;
margin: 0;
}
.history-item {
display: flex;
margin-bottom: 15px;
padding: 12px 16px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
z-index: 1001;
will-change: background-color, transform;
}
.history-item:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: scale(1.02);
}
.message-text {
color: #ffffff;
margin: 0;
font-size: 15px;
word-break: break-word;
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.4s ease;
}
.slide-fade-enter {
transform: translateX(100%);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(100%);
opacity: 0;
}
/* 动态背景 */
.animated-bg {
position: absolute;
top: 0;
left: 0;
width: 340px;
height: 100%;
overflow: hidden;
z-index: 600;
}
.bg-circle {
position: absolute;
border-radius: 50%;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05),
rgba(255, 255, 255, 0.02)
);
animation: float 20s infinite ease-in-out;
opacity: 0.4;
/* 添加过渡效果 */
transition: all 1.5s ease-in-out;
}
</style>

File diff suppressed because it is too large Load Diff

@ -2,7 +2,12 @@
<view class="home-component">
<!-- 动态背景元素 -->
<view class="animated-bg">
<view v-for="i in 6" :key="i" class="bg-circle" :style="getRandomCircleStyle()"></view>
<view
v-for="i in 6"
:key="i"
class="bg-circle"
:style="getRandomCircleStyle()"
></view>
</view>
<!-- 顶部欢迎卡片 -->
@ -24,33 +29,50 @@
<view class="feature-desc">与AI进行实时辩论对练提升应变能力</view>
</view>
<view class="feature-arrow">
<image src="/static/icons/arrow-right-s-line.png" mode="aspectFit"></image>
<image
src="/static/icons/arrow-right-s-line.png"
mode="aspectFit"
></image>
</view>
</view>
<view class="feature-card" @click="switchTab(2)">
<view class="feature-icon">
<image src="/static/icons/lightbulb-line.png" mode="aspectFit"></image>
<image
src="/static/icons/lightbulb-line.png"
mode="aspectFit"
></image>
</view>
<view class="feature-text">
<view class="feature-title">立论助手</view>
<view class="feature-desc">AI辅助构建辩论框架智能生成论点与论据</view>
<view class="feature-desc"
>AI辅助构建辩论框架智能生成论点与论据</view
>
</view>
<view class="feature-arrow">
<image src="/static/icons/arrow-right-s-line.png" mode="aspectFit"></image>
<image
src="/static/icons/arrow-right-s-line.png"
mode="aspectFit"
></image>
</view>
</view>
<view class="feature-card" @click="switchTab(3)">
<view class="feature-icon">
<image src="/static/icons/file-chart-line.png" mode="aspectFit"></image>
<image
src="/static/icons/file-chart-line.png"
mode="aspectFit"
></image>
</view>
<view class="feature-text">
<view class="feature-title">复盘分析</view>
<view class="feature-desc">分析辩论过程提供改进建议与优化方向</view>
</view>
<view class="feature-arrow">
<image src="/static/icons/arrow-right-s-line.png" mode="aspectFit"></image>
<image
src="/static/icons/arrow-right-s-line.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
@ -61,55 +83,179 @@
<view class="section-title">最近活动</view>
<view class="section-more" @click="showMoreActivities">
<text>查看更多</text>
<image src="/static/icons/arrow-right-s-line.png" mode="aspectFit"></image>
<image
src="/static/icons/arrow-right-s-line.png"
mode="aspectFit"
></image>
</view>
</view>
<view class="activity-list">
<view class="activity-item" v-for="(activity, index) in recentActivities" :key="index">
<view class="activity-icon" :class="activity.type">
<image :src="getActivityIcon(activity.type)" mode="aspectFit"></image>
</view>
<view class="activity-content">
<view class="activity-title">{{ activity.title }}</view>
<view class="activity-time">{{ activity.time }}</view>
</view>
<view class="activity-arrow">
<image src="/static/icons/arrow-right-s-line.png" mode="aspectFit"></image>
<view v-if="recentActivities.length > 0">
<view
class="activity-item"
v-for="(activity, index) in recentActivities.slice(0, 3)"
:key="index"
>
<view class="activity-icon" :class="activity.type">
<image
:src="getActivityIcon(activity.type)"
mode="aspectFit"
></image>
</view>
<view class="activity-content">
<view class="activity-title">{{
activity.title ? activity.title : activity.preview
}}</view>
<view class="activity-time">{{
formatDate(activity.updateTime)
}}</view>
</view>
<view class="activity-arrow">
<image
src="/static/icons/arrow-right-s-line.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
<!-- 没有数据时显示提示信息 -->
<div v-else class="no-data">
<image
src="/static/icons/no-data.png"
mode="aspectFit"
class="no-data-icon"
/>
<p class="no-data-text">暂无数据</p>
</div>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-area-bottom"></view>
<transition name="slide">
<ConversationHistory
:visible="showHistory"
:history-list="recentActivities"
:type="0"
@update:visible="showHistory = $event"
/>
</transition>
</view>
</template>
<script>
import ConversationHistory from "./ConversationHistory.vue";
import { useTokenStore } from "../stores/tokenStore";
export default {
components: {
ConversationHistory,
},
mounted() {
this.getHistoryList();
},
data() {
return {
showHistory: false,
chatHistory: ["test1", "test2"],
recentActivities: [
{
type: 'debate',
type: "debate",
title: '关于"教育改革"的辩论',
time: '今天 14:30'
time: "今天 14:30",
},
{
type: 'argument',
type: "argument",
title: '关于"环境保护"的立论',
time: '昨天 09:15'
time: "昨天 09:15",
},
{
type: 'review',
type: "review",
title: '关于"人工智能"的辩论复盘',
time: '3天前'
}
]
time: "3天前",
},
],
};
},
methods: {
formatDate(updateTime) {
// ISO Date
const updateDate = new Date(updateTime);
const now = new Date();
//
const diffTime = Math.abs(now - updateDate);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 1) {
return "今天";
}
if (diffDays < 3) {
return `${diffDays}天前`;
} else {
// 使 toLocaleDateString
return updateDate.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
}
},
async getHistoryList() {
// this.recentActivities = [
// {
// type: 'debate',
// title: '""',
// time: ' 14:30'
// },
// {
// type: 'argument',
// title: '""',
// time: ' 09:15'
// },
// {
// type: 'review',
// title: '""',
// time: '3'
// }
// ]
return new Promise((resolve, reject) => {
const token = useTokenStore().token.content;
console.log("token:", token);
uni.request({
url: "http://localhost:8080/api/conversation/list", // URL
method: "GET", // GET
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
console.log("res:", res);
if (res.statusCode === 200 && res.data.code === 200) {
//
const conversation = res.data.data[0];
this.recentActivities = res.data.data;
resolve(conversation ? conversation : "没有可用的对话");
} else {
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
createNew() {
console.log("createNew");
},
handleSelect(id) {
console.log("选中了历史记录 id:", id);
},
//
getRandomCircleStyle() {
const size = Math.random() * 300 + 100;
@ -117,51 +263,88 @@ export default {
const y = Math.random() * 100;
const delay = Math.random() * 5;
const duration = Math.random() * 10 + 15;
return {
width: `${size}rpx`,
height: `${size}rpx`,
left: `${x}%`,
top: `${y}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`
animationDuration: `${duration}s`,
};
},
//
getActivityIcon(type) {
const icons = {
'debate': '/static/icons/chat-1-line.png',
'argument': '/static/icons/lightbulb-line.png',
'review': '/static/icons/file-chart-line.png'
debate: "/static/icons/chat-1-line.png",
argument: "/static/icons/lightbulb-line.png",
review: "/static/icons/file-chart-line.png",
};
return icons[type] || icons.debate;
},
//
switchTab(index) {
this.$emit('tab-change', index);
this.$emit("tab-change", index);
},
//
showMoreActivities() {
uni.showToast({
title: '功能开发中',
icon: 'none'
});
}
}
this.showHistory = true;
console.log("showMoreActivities:", this.showHistory);
// uni.showToast({
// title: '',
// icon: 'none'
// });
},
},
};
</script>
<style scoped>
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 50px;
}
.no-data-icon {
width: 200px;
height: 100px;
opacity: 0.6;
}
.no-data-text {
margin-top: 15px;
font-size: 24px;
color: #999;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease-in-out;
position: fixed;
width: 100%;
z-index: 9999;
}
.slide-enter,
.slide-leave-to {
transform: translateX(-100%);
opacity: 1;
}
.home-component {
width: 100%;
min-height: 100vh;
padding: 30rpx;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #4338CA 0%, #7C3AED 100%);
background: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
overflow-x: hidden;
box-sizing: border-box;
padding-bottom: 180rpx; /* 为底部TabBar留出空间 */
@ -182,14 +365,19 @@ export default {
.bg-circle {
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05),
rgba(255, 255, 255, 0.02)
);
animation: float 20s infinite ease-in-out;
opacity: 0.4;
transition: all 1.5s ease-in-out;
}
@keyframes float {
0%, 100% {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
@ -238,7 +426,7 @@ export default {
.welcome-title {
font-size: 48rpx;
color: #FFFFFF;
color: #ffffff;
font-weight: 700;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
@ -250,7 +438,6 @@ export default {
line-height: 1.5;
}
/* 功能卡片区域 */
.feature-grid {
display: flex;
@ -324,7 +511,7 @@ export default {
.feature-title {
font-size: 32rpx;
color: #FFFFFF;
color: #ffffff;
font-weight: 600;
margin-bottom: 8rpx;
}
@ -365,7 +552,7 @@ export default {
.section-title {
font-size: 36rpx;
color: #FFFFFF;
color: #ffffff;
font-weight: 600;
}
@ -438,7 +625,7 @@ export default {
.activity-title {
font-size: 28rpx;
color: #FFFFFF;
color: #ffffff;
margin-bottom: 6rpx;
}
@ -468,4 +655,4 @@ export default {
height: calc(20rpx + constant(safe-area-inset-bottom)); /* iOS 11.0-11.2 */
height: calc(20rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
}
</style>
</style>

@ -27,6 +27,11 @@
>分析辩论过程提供改进建议与优化方向</view
>
</view>
<view class="card-icon-wrapper">
<view class="card-icon" @click.stop="showHistory = true">
<image src="/static/icons/list.png" mode="aspectFit"></image>
</view>
</view>
<view class="collapse-icon">
<image
:src="
@ -46,6 +51,9 @@
scroll-y
:scroll-into-view="scrollToView"
scroll-with-animation
:class="{ 'fullscreen-chat': isChatFullscreen }"
@transitionend="handleTransitionEnd"
:style="{ height: chatAreaHeight }"
>
<view
v-for="(msg, index) in messages"
@ -65,25 +73,50 @@
</view>
</block>
<block v-else>
{{ msg.content }}
<text class="typewriter" v-if="msg.role === 'ai' && msg.typing">{{
msg.displayContent
}}</text>
<rich-text
v-else
:nodes="parseMarkdown(msg.content)"
class="markdown-content"
></rich-text>
</block>
</view>
<view class="avatar user-avatar" v-if="msg.role === 'user'"></view>
</view>
<!-- 全屏按钮 -->
<view class="fullscreen-toggle-button" @click="toggleFullscreen">
<image
:src="
isChatFullscreen
? '/static/icons/fullscreen-exit-line.png'
: '/static/icons/fullscreen-line.png'
"
mode="aspectFit"
></image>
</view>
</scroll-view>
<!-- 输入框与发送按钮 -->
<view class="chat-input">
<view
class="chat-input"
:class="{ 'fullscreen-input': isChatFullscreen }"
:style="{ bottom: inputBottom }"
>
<view class="input-group">
<view class="textarea-container">
<view class="textarea-wrapper">
<textarea
class="textarea-box"
v-model="content"
v-model="question"
placeholder="粘贴你的辩论记录AI将为你分析优缺点并提供改进建议"
placeholder-style="color: #ffffff; font-size: 28rpx;"
auto-height
maxlength="5000"
@focus="onInputFocus"
@blur="onInputBlur"
maxlength="50000"
/>
<button class="send-button-embedded" @click="sendMessage()">
<button class="send-button" @click="sendMessage()">
<image
src="/static/icons/send-plane-fill.png"
mode="aspectFit"
@ -103,15 +136,28 @@
@close="showSheet = false"
@click="handleSheetClick"
/>
<ConversationHistory
:visible="showHistory"
:history-list="chatHistory"
:type="3"
@update:visible="showHistory = $event"
@select="handleSelect"
@createNew="createNew"
/>
</view>
</template>
<script>
import { useDebateStore } from "../stores/DebateStore";
import Popup from "./Popup.vue";
import ConversationHistory from "./ConversationHistory.vue";
import { useTokenStore } from "../stores/tokenStore";
import { useReviewStore } from "../stores/reviewStore";
import { marked } from "marked";
export default {
components: { Popup },
components: { Popup, ConversationHistory },
props: {
debate: {
type: Object,
@ -121,10 +167,23 @@ export default {
mounted() {
const pinia = this.$pinia;
const store = useDebateStore(pinia);
this.content = store.selectedDebate.content;
if (store.selectedDebate) this.content = store.selectedDebate;
this.getHistoryList();
this.conversationId = useReviewStore().conversationId;
this.messages = useReviewStore().conversation;
this.calculateChatAreaHeight();
this.systemInfo = uni.getSystemInfoSync();
this.tabBarHeight = this.systemInfo.platform === "ios" ? 100 : 80;
},
data() {
return {
conversationId: -1,
showHistory: false,
chatHistory: ["test1", "test2"],
showSheet: false,
StoreReview: "",
input: "",
@ -133,14 +192,218 @@ export default {
role: "ai",
content:
"欢迎使用辩论复盘分析!请粘贴你的辩论记录,我将为你分析优缺点并提供改进建议。",
displayContent: "",
typing: false,
},
],
scrollToView: "",
content: "",
isCardCollapsed: false, //
tabBarHeight: 100, // tab bar 100rpx
isChatFullscreen: false,
isTransitioning: false,
inputBottom: "100rpx", // TabBar
keyboardHeight: 0,
chatAreaHeight: "calc(100vh - 400rpx)",
systemInfo: null,
tabBarHeight: 100,
};
},
onLoad() {
uni.onKeyboardHeightChange((res) => {
this.keyboardHeight = res.height;
this.adjustLayout();
});
},
onUnload() {
uni.offKeyboardHeightChange();
},
methods: {
calculateChatAreaHeight() {
//
const cardHeight = this.isCardCollapsed ? 120 : 180;
const inputHeight = 200;
const safeArea = this.systemInfo?.safeAreaInsets?.bottom || 0;
this.chatAreaHeight = `calc(100vh - ${
cardHeight + inputHeight + safeArea
}rpx)`;
},
adjustLayout() {
if (this.keyboardHeight > 0) {
//
this.inputBottom = `${this.keyboardHeight * 2 + this.tabBarHeight}rpx`;
this.chatAreaHeight = `calc(100vh - 300rpx - ${
this.keyboardHeight * 2
}rpx)`;
} else {
//
this.inputBottom = `${this.tabBarHeight}rpx`;
this.calculateChatAreaHeight();
}
this.scrollToBottom();
},
toggleFullscreen() {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.isChatFullscreen = !this.isChatFullscreen;
this.adjustLayout();
},
handleTransitionEnd() {
this.isTransitioning = false;
},
onInputFocus() {
this.scrollToBottom();
},
onInputBlur() {
//
setTimeout(() => {
if (!this.isChatFullscreen) {
this.adjustLayout();
}
}, 200);
},
toggleFullscreen() {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.isChatFullscreen = !this.isChatFullscreen;
},
handleTransitionEnd() {
this.isTransitioning = false;
},
onInputFocus() {
//
this.scrollToBottom();
},
onInputBlur() {
//
this.inputBottom = this.tabBarHeight;
},
typeMessage(index, fullText) {
let currentLength = 0;
const speed = 50;
const type = () => {
if (currentLength <= fullText.length) {
this.messages[index].displayContent = fullText.slice(
0,
currentLength
);
currentLength++;
setTimeout(type, speed);
} else {
this.messages[index].typing = false;
this.scrollToBottom();
}
};
type();
},
parseMarkdown(markdown) {
//
let html = marked(markdown || "");
//
html = html.replace(/<script.*?>.*?<\/script>/gi, "");
return html;
},
async getHistoryList() {
return new Promise((resolve, reject) => {
const token = useTokenStore().token.content;
console.log("token:", token);
uni.request({
url: "http://localhost:8080/api/conversation/list", // URL
method: "GET", // GET
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
console.log("res:", res);
if (res.statusCode === 200 && res.data.code === 200) {
// type === "argument"
const reviewList = res.data.data.filter(
(item) => item.type === "review"
);
// chatHistory
this.chatHistory = reviewList;
// resolve
if (reviewList.length > 0) {
resolve(reviewList); // resolve(argumentList[0])
} else {
resolve("没有找到类型为 argument 的对话");
}
} else {
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
createNew() {
this.messages = [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
displayContent: "",
typing: false,
},
];
console.log("createNew");
this.conversationId = -1;
useReviewStore().setConversationId(-1);
useReviewStore().setConversation(this.messages);
},
handleSelect(conversationId) {
console.log("选中了历史记录 id:", conversationId);
this.showHistory = false;
return new Promise((resolve, reject) => {
const token = useTokenStore().token.content;
console.log("token:", token);
uni.request({
url: `http://localhost:8080/api/conversation/review/${conversationId}`, // 使 conversationId
method: "GET",
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
console.log("res:", res);
if (res.statusCode === 200 && res.data.code === 200) {
this.messages = [];
for (let item of res.data.data) {
this.messages.push({
role: "user",
content: item.userMessage,
});
this.messages.push({
role: "ai",
content: item.content,
});
}
useReviewStore().setConversation(this.messages);
this.conversationId = conversationId;
useReviewStore().setConversationId(conversationId);
} else {
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
handleSheetClick() {
const content = this.StoreReview;
uni.setClipboardData({
@ -163,6 +426,7 @@ export default {
// /
toggleCard() {
this.isCardCollapsed = !this.isCardCollapsed;
this.calculateChatAreaHeight();
},
//
@ -212,25 +476,46 @@ export default {
return new Promise((resolve, reject) => {
console.log("callAI:", content);
const token = useTokenStore().token.content;
if (!token) {
reject("未登录,请先登录");
return;
}
uni.request({
url: "http://localhost:8080/api/ai/review",
method: "POST",
header: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
data: {
conversationId: this.conversationId,
content: content,
},
success: (res) => {
console.log("res:", res);
let reviewJson = JSON.parse(res.data.data.review);
this.scrollToBottom();
console.log("API响应:", res);
if (res.statusCode === 200 && res.data.code === 200) {
resolve(
reviewJson.choices[0].message.content || "AI 没有返回有效内容"
);
const responseData = res.data.data;
console.log("responseData:", responseData);
// ID
if (this.conversationId == -1) {
this.conversationId = responseData.conversationId;
console.log("更新会话ID为", responseData.id);
this.getHistoryList();
useReviewStore().setConversationId(this.conversationId);
}
// review
const cleanedReview =
responseData.review?.replace(/`/g, "") || "";
resolve(cleanedReview);
} else {
reject("请求失败:" + (res.data.msg || "未知错误"));
reject("请求失败:" + (res.data.message || "未知错误"));
}
},
fail: (err) => {
@ -250,6 +535,100 @@ export default {
</script>
<style scoped>
.fullscreen-toggle-button {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%) scale(1);
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
z-index: 1000;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
/* 添加 Markdown 内容的基础样式 */
.markdown-content {
width: 100%; /* 确保宽度不超出容器 */
overflow: hidden; /* 隐藏溢出内容 */
overflow-wrap: break-word;
overflow-y: auto;
overflow-x: auto;
}
/* 标题样式 */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin: 16rpx 0;
font-weight: bold;
}
.markdown-content h1 {
font-size: 36rpx;
}
.markdown-content h2 {
font-size: 32rpx;
}
/* 列表样式 */
.markdown-content ul,
.markdown-content ol {
padding-left: 40rpx;
margin: 12rpx 0;
}
.markdown-content li {
margin: 8rpx 0;
}
/* 代码块样式 */
.markdown-content a {
word-break: break-all; /* 长链接强制换行 */
}
.markdown-content pre {
white-space: pre-wrap; /* 保留空格但换行 */
word-break: break-all; /* 允许任意字符换行 */
overflow-wrap: break-word; /* 优先在单词间换行 */
background: rgba(0, 0, 0, 1);
padding: 12rpx;
border-radius: 8rpx;
}
/* 引用样式 */
.markdown-content blockquote {
border-left: 4rpx solid #f59e0b;
padding-left: 20rpx;
margin: 16rpx 0;
color: rgba(255, 255, 255, 0.7);
}
/* 表格样式 */
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 16rpx 0;
}
.markdown-content th,
.markdown-content td {
border: 1rpx solid rgba(255, 255, 255, 0.2);
padding: 12rpx;
}
.markdown-content th {
background-color: rgba(255, 255, 255, 0.1);
}
.review-component {
width: 100%;
height: 100vh;
@ -453,12 +832,85 @@ export default {
background: rgba(255, 255, 255, 0.1);
border-radius: 28rpx;
padding: 30rpx;
margin-bottom: 30rpx;
margin-bottom: -50rpx;
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.15);
position: relative;
z-index: 1;
transition: all 0.3s ease;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.chat-area.fullscreen-chat {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: calc(100vh - 300rpx - env(safe-area-inset-bottom));
padding: 30rpx;
border-radius: 0;
z-index: 999;
background: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
transform: scale(1);
animation: expand 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes expand {
0% {
transform: scale(0.95);
opacity: 0.8;
border-radius: 28rpx;
}
100% {
transform: scale(1);
opacity: 1;
border-radius: 0;
}
}
.chat-area:not(.fullscreen-chat) {
animation: collapse 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes collapse {
0% {
transform: scale(1);
opacity: 1;
border-radius: 0;
}
100% {
transform: scale(0.95);
opacity: 0.8;
border-radius: 28rpx;
}
}
.fullscreen-toggle-button {
position: fixed;
right: 10rpx;
top: 50%;
transform: translateY(-50%) scale(1);
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
z-index: 1000;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
.fullscreen-toggle-button:active {
transform: translateY(-50%) scale(0.9);
}
.fullscreen-toggle-button image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
}
/* 消息通用样式 */
@ -467,6 +919,8 @@ export default {
margin-bottom: 30rpx;
align-items: flex-start;
animation: fadeIn 0.5s ease-out;
overflow-x: auto;
}
@keyframes fadeIn {
@ -521,6 +975,7 @@ export default {
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
position: relative;
transition: all 0.3s ease; /* 添加过渡效果 */
overflow-wrap: break-word;
}
.from-user .bubble {
@ -560,7 +1015,11 @@ export default {
border-top: 16rpx solid transparent;
transition: all 0.3s ease; /* 添加过渡效果 */
}
.typewriter {
display: inline-block;
overflow: hidden;
white-space: pre-wrap;
}
/* 加载动画 */
.loading-animation {
display: flex;
@ -605,9 +1064,49 @@ export default {
border-radius: 28rpx;
align-items: flex-end;
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 1;
margin-bottom: 20rpx;
bottom: var(--input-bottom, 100rpx);
z-index: 100;
margin-bottom: 50rpx;
box-sizing: border-box;
transition: all 0.3s ease;
}
.chat-input.fullscreen-input {
border-radius: 0;
padding: 20rpx;
background: rgba(67, 56, 202, 0.9);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
left: 0;
right: 0;
z-index: 1000;
position: fixed;
margin-bottom: 180rpx;
}
@keyframes slideUp {
0% {
transform: translateY(100rpx);
opacity: 0.8;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.chat-input:not(.fullscreen-input) {
animation: slideDownInput 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes slideDownInput {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(100rpx);
opacity: 0.8;
}
}
.input-group {
@ -615,58 +1114,61 @@ export default {
display: flex;
flex-direction: column;
gap: 16rpx;
width: 100%;
box-sizing: border-box;
}
.textarea-container {
position: relative;
.textarea-wrapper {
display: flex;
align-items: flex-end;
width: 100%;
background: rgba(255, 255, 255, 0.2);
padding-right: 10rpx;
padding-bottom: 10rpx;
box-sizing: border-box;
border-radius: 20rpx;
}
.send-button-embedded {
position: absolute;
bottom: 16rpx;
left: 16rpx;
.textarea-box {
flex: 1;
padding: 16rpx 20rpx;
font-size: 28rpx;
color: #ffffff;
transition: all 0.3s;
min-height: 100rpx;
max-height: 180rpx;
margin-right: 16rpx;
box-sizing: border-box;
background: transparent;
width: 60rpx;
height: 60rpx;
border: none;
}
.send-button {
background: #f59e0b;
width: 70rpx;
height: 70rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 15rpx rgba(245, 158, 11, 0.4);
transition: all 0.3s;
padding: 0;
z-index: 2;
}
.send-button-embedded:active {
transform: scale(1.2) rotate(-15deg);
}
.send-button-embedded image {
width: 40rpx;
height: 40rpx;
filter: invert(72%) sepia(87%) saturate(1242%) hue-rotate(325deg)
brightness(101%) contrast(96%);
flex-shrink: 0;
}
.textarea-box {
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 16rpx 20rpx 16rpx 90rpx; /* 增加左侧内边距为发送按钮留出空间 */
font-size: 28rpx;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s;
min-height: 120rpx;
max-height: 300rpx; /* 增加最大高度,适合复盘分析的长文本 */
width: 100%;
box-sizing: border-box;
.send-button:active {
transform: scale(0.95) rotate(-15deg);
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
}
.textarea-box::placeholder {
color: #362d2de1;
.send-button image {
width: 36rpx;
height: 36rpx;
filter: brightness(0) invert(1);
}
.input-box:focus,
.textarea-box:focus {
border-color: #f59e0b;
outline: none;
@ -675,8 +1177,6 @@ export default {
/* 底部安全区域 */
.safe-area-bottom {
height: 20rpx;
height: calc(20rpx + constant(safe-area-inset-bottom)); /* iOS 11.0-11.2 */
height: calc(20rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
height: calc(100rpx + env(safe-area-inset-bottom));
}
</style>

@ -7,7 +7,7 @@
<view class="bottom-content">
<!-- 标题移到气泡下方 -->
<view class="login-title">
<text class="title-text">云枢</text>
<text class="title-text">云枢</text>
<text class="subtitle-text">Intelligent Debate Cloud</text>
</view>
@ -25,6 +25,7 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useTokenStore } from '../../stores/tokenStore';
const fullText = ref('微信一键登录');
const displayText = ref('');
@ -81,6 +82,11 @@ const requestLogin = (code) =>{
success(res) {
if(res.data.code === 200){
wx.setStorageSync('token', res.data.data.token);
const tokenStore = useTokenStore();
tokenStore.setToken(res.data.data.token);
console.log("token:",res.data.data.token);
console.log("登录成功");
uni.redirectTo({
url: '/pages/index/index'

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -1,13 +1,27 @@
// stores/argumentStore.ts
import { defineStore } from 'pinia'
import { defineStore } from "pinia";
export const useArgumentStore = defineStore('argument', {
export const useArgumentStore = defineStore("argument", {
state: () => ({
selectedArgument: null
conversationId: -1,
selectedArgument: "",
conversation: [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
},
],
}),
actions: {
setArgument(arg) {
this.selectedArgument = arg
}
}
})
this.selectedArgument = arg;
},
setConversationId(id) {
this.conversationId = id;
},
setConversation(conversation) {
this.conversation = conversation;
},
},
});

@ -3,13 +3,25 @@ import { defineStore } from 'pinia'
export const useDebateStore = defineStore('debate', {
state: () => ({
selectedDebate: {
content: '',
},
conversationId: -1,
selectedDebate: "",
conversation: [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
},
],
}),
actions: {
setDebate(content) {
this.selectedDebate.content = content;
}
this.selectedDebate = content;
},
setConversationId(id) {
this.conversationId = id;
},
setConversation(conversation) {
this.conversation = conversation;
},
}
});

@ -0,0 +1,23 @@
// stores/argumentStore.ts
import { defineStore } from "pinia";
export const useReviewStore = defineStore("review", {
state: () => ({
conversationId: -1,
conversation: [
{
role: "ai",
content:
"哈喽~ 我是辩论助手,很高兴为你服务!请告诉我你想立论的立场和题目。",
},
],
}),
actions: {
setConversationId(id) {
this.conversationId = id;
},
setConversation(conversation) {
this.conversation = conversation;
},
},
});

@ -0,0 +1,15 @@
// stores/DebateStore.ts
import { defineStore } from 'pinia'
export const useTokenStore = defineStore('token', {
state: () => ({
token: {
content: '',
},
}),
actions: {
setToken(content) {
this.token.content = content;
}
}
});

@ -1,19 +0,0 @@
create database if not exists wx_miniapp default charset utf8mb4;
use wx_miniapp;
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';
Loading…
Cancel
Save