12月31号许愿树优化

This commit is contained in:
cxytw 2025-12-31 14:24:51 +08:00
parent 20f2e342ce
commit 3c9b31185f
30 changed files with 1843 additions and 81 deletions

View File

@ -0,0 +1,304 @@
# 许愿树移动端设计规范
> 版本V2.1
> 更新日期2025-12-31
> 设计范围Android移动端许愿树功能完善
---
## 一、设计原则
1. **不修改后端**复用现有API不做任何后端改动
2. **不修改管理端**:管理端功能保持不变
3. **功能完善**:确保所有功能逻辑正确
---
## 二、现有后端API不可修改
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取节日列表 | GET | `/api/front/wishtree/festivals` | 获取所有启用的节日 |
| 获取当前节日 | GET | `/api/front/wishtree/festivals/current` | 获取当前进行中的节日 |
| 获取心愿列表 | GET | `/api/front/wishtree/wishes` | 分页获取心愿列表 |
| 获取我的心愿 | GET | `/api/front/wishtree/wishes/my` | 获取当前用户的心愿 |
| 发布心愿 | POST | `/api/front/wishtree/wishes` | 发布新心愿 |
| 获取心愿详情 | GET | `/api/front/wishtree/wishes/{id}` | 获取心愿详情 |
| 删除心愿 | DELETE | `/api/front/wishtree/wishes/{id}` | 删除我的心愿 |
| 点赞心愿 | POST | `/api/front/wishtree/wishes/{id}/like` | 点赞 |
| 取消点赞 | DELETE | `/api/front/wishtree/wishes/{id}/like` | 取消点赞 |
| 获取评论 | GET | `/api/front/wishtree/wishes/{id}/comments` | 获取评论列表 |
| 发表评论 | POST | `/api/front/wishtree/wishes/{id}/comments` | 发表评论 |
| 获取背景素材 | GET | `/api/front/wishtree/backgrounds` | 获取背景素材列表 |
---
## 三、用户故事与验收标准
### US-001: 查看许愿树主页
**作为** 用户
**我想要** 进入许愿树页面查看许愿树和我的心愿
**以便于** 了解当前活动和管理我的心愿
**验收标准:**
- [x] 页面显示许愿树背景图
- [x] 显示当前节日活动横幅(调用 `/festivals/current`
- [x] 显示我的心愿卡片(调用 `/wishes/my`最多7个
- [x] 显示"添加心愿"按钮
- [x] 底部导航栏正确高亮"许愿树"选项
- [x] 显示倒计时
- [x] 显示祈愿值统计
---
### US-002: 发布心愿
**作为** 已登录用户
**我想要** 发布一个新的心愿
**以便于** 表达我的愿望
**验收标准:**
- [x] 点击"添加心愿"或空白卡片弹出许愿对话框
- [x] 输入框限制50字显示字数统计
- [x] 未登录时提示登录并跳转登录页
- [x] 调用 `POST /wishes` 发布心愿
- [x] 提交成功后显示成功动画
- [x] 心愿列表自动刷新
---
### US-003: 查看我的心愿
**作为** 用户
**我想要** 查看我已发布的心愿详情
**以便于** 回顾我的愿望
**验收标准:**
- [x] 点击心愿卡片弹出详情对话框
- [x] 显示心愿内容、点赞数、评论数
- [x] 提供"删除心愿"和"愿望达成"操作
---
### US-004: 删除心愿
**作为** 用户
**我想要** 删除我不想要的心愿
**以便于** 管理我的心愿列表
**验收标准:**
- [x] 点击"删除心愿"弹出确认对话框
- [x] 确认后调用 `DELETE /wishes/{id}` 删除
- [x] 删除成功后刷新列表
- [x] 显示删除成功提示
---
### US-005: 愿望达成
**作为** 用户
**我想要** 标记我的愿望已达成
**以便于** 庆祝愿望实现
**验收标准:**
- [x] 点击"愿望达成"弹出确认对话框
- [x] 确认后调用 `DELETE /wishes/{id}` 删除心愿
- [x] 显示庆祝动画持续2秒
- [x] 刷新心愿列表
---
### US-006: 查看节日活动
**作为** 用户
**我想要** 看到当前进行中的节日活动
**以便于** 参与节日许愿
**验收标准:**
- [x] 页面顶部显示活动横幅
- [x] 横幅显示节日名称
- [x] 显示倒计时
- [x] 无活动时显示默认文案
---
## 四、移动端文件结构
```
android-app/app/src/main/java/com/example/livestreaming/
├── WishTreeActivity.java # 许愿树主页面
└── net/
├── ApiService.java # API接口定义
├── WishtreeRequest.java # 请求类
└── WishtreeResponse.java # 响应类
android-app/app/src/main/res/layout/
├── activity_wish_tree.xml # 主页面布局
├── dialog_make_wish.xml # 许愿对话框
├── dialog_view_wish.xml # 查看心愿对话框
├── dialog_wish_success.xml # 成功提示对话框
└── dialog_wish_complete.xml # 愿望达成对话框
```
---
## 五、数据模型
### 5.1 WishtreeResponse.java
```java
public class WishtreeResponse {
// 节日信息
public static class Festival {
public int id;
public String name;
public String icon;
public String startDate;
public String endDate;
public int isLunar;
public String themeColor;
public int status;
}
// 心愿信息
public static class Wish {
public long id;
public int uid;
public String nickname;
public String avatar;
public int festivalId;
public String festivalName;
public String festivalIcon;
public String content;
public int backgroundId;
public String backgroundImage;
public int status; // 0待审核 1通过 2拒绝
public int likeCount;
public int commentCount;
public Boolean isLiked;
public String createTime;
}
// 分页数据
public static class WishPage {
public List<Wish> list;
public int total;
public int pageNum;
public int pageSize;
}
}
```
### 5.2 WishtreeRequest.java
```java
public class WishtreeRequest {
// 发布心愿请求
public static class PublishWish {
public Integer festivalId;
public String content;
public Integer backgroundId;
}
}
```
---
## 六、核心交互流程
### 6.1 页面加载
```
onCreate()
├── 初始化ApiService
├── 启动横幅倒计时
├── 设置底部导航
├── 设置心愿卡片点击事件
├── loadCurrentFestival() → 更新横幅文字
└── loadMyWishes() → 更新心愿卡片、祈愿值
```
### 6.2 发布心愿
```
点击"添加心愿"
├── checkLogin() → 未登录跳转登录页
├── showMakeWishInputDialog()
├── 输入内容≤50字
├── publishWish() → POST /wishes
├── 成功 → showSuccessDialog() → loadMyWishes()
└── 失败 → Toast提示
```
### 6.3 删除/达成心愿
```
点击心愿卡片
├── showViewWishDialog()
├── 点击"删除心愿"
│ ├── 确认对话框
│ ├── DELETE /wishes/{id}
│ └── 成功 → Toast → loadMyWishes()
└── 点击"愿望达成"
├── 确认对话框
├── DELETE /wishes/{id}
└── 成功 → 庆祝动画 → loadMyWishes()
```
---
## 七、错误处理
| 场景 | 处理方式 |
|------|----------|
| 网络错误 | Toast "网络错误" |
| 业务错误 | Toast 显示后端返回的message |
| 未登录 | 跳转LoginActivity |
| 空数据 | 显示空列表,卡片显示空白 |
| 内容为空 | Toast "请输入心愿内容" |
| 内容超长 | Toast "心愿内容不能超过50字" |
---
## 八、实现任务清单
### Phase 1: 数据层 ✅
- [x] WishtreeResponse.java - 响应类
- [x] WishtreeRequest.java - 请求类
- [x] ApiService.java - 接口定义
### Phase 2: UI层 ✅
- [x] activity_wish_tree.xml - 主页面
- [x] dialog_make_wish.xml - 许愿对话框
- [x] dialog_view_wish.xml - 查看心愿对话框
- [x] dialog_wish_success.xml - 成功提示
- [x] dialog_wish_complete.xml - 愿望达成
### Phase 3: 逻辑层 ✅
- [x] WishTreeActivity.java - 核心逻辑
- [x] 页面加载与数据刷新
- [x] 发布心愿功能
- [x] 删除心愿功能
- [x] 愿望达成功能
- [x] 登录检查
- [x] 错误处理
---
## 九、测试要点
### 功能测试
- [x] 页面正常加载
- [x] 节日信息正确显示
- [x] 心愿卡片正确显示最多7个
- [x] 发布心愿成功
- [x] 删除心愿成功
- [x] 愿望达成功能正常
- [x] 底部导航切换正常
### 异常测试
- [x] 网络断开时的提示
- [x] 未登录时跳转登录页
- [x] 空数据时的显示
- [x] 心愿内容校验(空、超长)

View File

@ -68,14 +68,6 @@ export function wishTreeNodeListApi(params) {
});
}
// 节点详情
export function wishTreeNodeInfoApi(id) {
return request({
url: `/admin/wish/tree/node/info/${id}`,
method: 'get',
});
}
// 新增节点
export function wishTreeNodeSaveApi(data) {
return request({
@ -112,7 +104,7 @@ export function wishTreeNodeStatusApi(id) {
// ========== 用户留言管理 ==========
// 留言列表(按许愿树查询)
// 留言列表
export function wishTreeMessageListApi(params) {
return request({
url: '/admin/wish/tree/message/list',
@ -121,14 +113,6 @@ export function wishTreeMessageListApi(params) {
});
}
// 留言详情
export function wishTreeMessageInfoApi(id) {
return request({
url: `/admin/wish/tree/message/info/${id}`,
method: 'get',
});
}
// 删除留言
export function wishTreeMessageDeleteApi(id) {
return request({
@ -144,3 +128,107 @@ export function wishTreeMessageStatusApi(id) {
method: 'post',
});
}
// ========== 节日管理 ==========
// 节日列表
export function festivalList(params) {
return request({
url: '/admin/wishtree/festival/list',
method: 'get',
params,
});
}
// 保存节日
export function festivalSave(data) {
return request({
url: '/admin/wishtree/festival/save',
method: 'post',
data,
});
}
// 删除节日 - 后端使用DELETE方法
export function festivalDelete(id) {
return request({
url: `/admin/wishtree/festival/delete/${id}`,
method: 'delete',
});
}
// 修改节日状态 - 后端接收RequestBody
export function festivalStatus(id, status) {
return request({
url: '/admin/wishtree/festival/status',
method: 'post',
data: { id, status },
});
}
// ========== 心愿管理 ==========
// 心愿列表 - 后端使用POST方法
export function wishList(data) {
return request({
url: '/admin/wishtree/wish/list',
method: 'post',
data,
});
}
// 审核心愿
export function wishAudit(data) {
return request({
url: '/admin/wishtree/wish/audit',
method: 'post',
data,
});
}
// 删除心愿 - 后端使用DELETE方法
export function wishDelete(id) {
return request({
url: `/admin/wishtree/wish/delete/${id}`,
method: 'delete',
});
}
// ========== 背景管理 ==========
// 背景列表
export function backgroundList(params) {
return request({
url: '/admin/wishtree/background/list',
method: 'get',
params,
});
}
// 保存背景
export function backgroundSave(data) {
return request({
url: '/admin/wishtree/background/save',
method: 'post',
data,
});
}
// 删除背景 - 后端使用DELETE方法
export function backgroundDelete(id) {
return request({
url: `/admin/wishtree/background/delete/${id}`,
method: 'delete',
});
}
// ========== 统计 ==========
// 获取统计数据
export function getStatistics(params) {
return request({
url: '/admin/wishtree/statistics',
method: 'get',
params,
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@ -4,21 +4,20 @@
v-if="$store.state.themeConfig.themeConfig.layout !== 'columns' && !$store.state.themeConfig.themeConfig.isCollapse"
@click="onThemeConfigChange"
>
<img v-if="platMerLoginInfo" class="layout-logo-medium-img" :src="platMerLoginInfo.siteLogoSquare" />
<!-- 直接使用本地Logo图片替换Logo只需修改 logo-square.png 文件 -->
<img class="layout-logo-medium-img" src="@/assets/imgs/logo-square.png" />
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img v-if="platMerLoginInfo" class="layout-logo-size-img" :src="platMerLoginInfo.siteLogoLeftTop" />
<!-- 直接使用本地Logo图片替换Logo只需修改 logo-lefttop.png 文件 -->
<img class="layout-logo-size-img" src="@/assets/imgs/logo-lefttop.png" />
</div>
</template>
<script>
import Cookies from 'js-cookie';
export default {
name: 'layoutLogo',
data() {
return {
platMerLoginInfo: JSON.parse(Cookies.get('logoInfo')), //
};
return {};
},
computed: {
//

View File

@ -35,6 +35,31 @@ const wishtreeManageRouter = {
name: 'WishtreeStatistics',
meta: { title: '数据统计' },
},
{
path: 'tree',
component: () => import('@/views/wishtree/tree/index'),
name: 'WishTreeList',
meta: { title: '许愿树列表' },
},
{
path: 'tree/detail/:id',
component: () => import('@/views/wishtree/tree/detail'),
name: 'WishTreeDetail',
meta: { title: '许愿树详情' },
hidden: true,
},
{
path: 'node',
component: () => import('@/views/wishtree/node/index'),
name: 'WishTreeNode',
meta: { title: '节点管理' },
},
{
path: 'message',
component: () => import('@/views/wishtree/message/index'),
name: 'WishTreeMessage',
meta: { title: '留言管理' },
},
],
};

View File

@ -15,8 +15,8 @@ const VUE_APP_WS_URL =
const SettingMer = {
// 服务器地址
httpUrl: VUE_APP_API_URL,
// 接口请求地址
apiBaseURL: VUE_APP_API_URL + '/api/',
// 接口请求地址 - 不再重复添加/api
apiBaseURL: VUE_APP_API_URL,
// socket连接
wsSocketUrl: VUE_APP_WS_URL,
};

View File

@ -159,7 +159,7 @@ export default {
}
},
handleViewMessages(row) {
this.$router.push({ path: `/wishTree/tree/detail/${row.id}` });
this.$router.push({ path: `/wishtree/tree/detail/${row.id}` });
},
addNode() {
this.formData.nodes.push({ title: '', description: '', open_time: null, sort: 0, status: 1 });

View File

@ -61,7 +61,7 @@
</template>
<script>
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishtree';
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishTree';
export default {
name: 'WishtreeBackground',

View File

@ -78,7 +78,7 @@
</template>
<script>
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishtree';
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishTree';
export default {
name: 'WishtreeFestival',
@ -132,7 +132,7 @@ export default {
});
},
async handleStatusChange(row) {
await festivalStatus({ id: row.id, status: row.status });
await festivalStatus(row.id, row.status);
this.$message.success('状态更新成功');
},
handleDelete(row) {

View File

@ -86,7 +86,7 @@
</template>
<script>
import { getStatistics } from '@/api/wishtree';
import { getStatistics } from '@/api/wishTree';
import * as echarts from 'echarts';
export default {

View File

@ -74,7 +74,7 @@
</template>
<script>
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishtree';
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishTree';
export default {
name: 'WishtreeWish',

View File

@ -49,6 +49,12 @@ module.exports = {
warnings: false,
errors: true,
},
proxy: {
'/api': {
target: 'http://localhost:30001',
changeOrigin: true
}
}
},
configureWebpack: {
// provide the app's title in webpack's name field, so that

View File

@ -296,8 +296,9 @@ public class WishtreeServiceImpl implements WishtreeService {
WishtreeStatisticsVO vo = new WishtreeStatisticsVO();
// 总心愿数
vo.setTotalWishes(Long.valueOf(wishDao.selectCount(new LambdaQueryWrapper<WishtreeWish>()
.eq(WishtreeWish::getIsDelete, 0))));
Integer totalWishes = wishDao.selectCount(new LambdaQueryWrapper<WishtreeWish>()
.eq(WishtreeWish::getIsDelete, 0));
vo.setTotalWishes(totalWishes != null ? totalWishes.longValue() : 0L);
// 今日新增
vo.setTodayWishes(wishDao.countTodayWishes());
@ -306,11 +307,13 @@ public class WishtreeServiceImpl implements WishtreeService {
vo.setPendingWishes(wishDao.countPendingWishes());
// 总点赞数
vo.setTotalLikes(Long.valueOf(likeDao.selectCount(null)));
Integer totalLikes = likeDao.selectCount(null);
vo.setTotalLikes(totalLikes != null ? totalLikes.longValue() : 0L);
// 总评论数
vo.setTotalComments(Long.valueOf(commentDao.selectCount(new LambdaQueryWrapper<WishtreeComment>()
.eq(WishtreeComment::getIsDelete, 0))));
Integer totalComments = commentDao.selectCount(new LambdaQueryWrapper<WishtreeComment>()
.eq(WishtreeComment::getIsDelete, 0));
vo.setTotalComments(totalComments != null ? totalComments.longValue() : 0L);
// 心愿趋势
vo.setWishTrend(wishDao.selectWishTrend(days));

View File

@ -0,0 +1,98 @@
-- ============================================
-- 许愿树快速建表脚本
-- 执行此脚本创建许愿树所需的所有表
-- ============================================
-- 1. 节日表
CREATE TABLE IF NOT EXISTS `eb_wishtree_festival` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '节日ID',
`name` varchar(50) NOT NULL COMMENT '节日名称',
`icon` varchar(255) DEFAULT '' COMMENT '节日图标',
`start_date` varchar(20) DEFAULT '' COMMENT '开始日期',
`end_date` varchar(20) DEFAULT '' COMMENT '结束日期',
`is_lunar` tinyint DEFAULT 0 COMMENT '是否农历',
`theme_color` varchar(20) DEFAULT '#FF6B6B' COMMENT '主题色',
`sort` int DEFAULT 0 COMMENT '排序',
`status` tinyint DEFAULT 1 COMMENT '状态 0禁用 1启用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树节日表';
-- 2. 心愿表
CREATE TABLE IF NOT EXISTS `eb_wishtree_wish` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '心愿ID',
`uid` int NOT NULL COMMENT '用户ID',
`festival_id` int DEFAULT 0 COMMENT '节日ID',
`content` varchar(500) NOT NULL COMMENT '心愿内容',
`background_id` int DEFAULT 0 COMMENT '背景ID',
`status` tinyint DEFAULT 1 COMMENT '状态 0待审核 1通过 2拒绝',
`audit_type` tinyint DEFAULT 0 COMMENT '审核方式 0自动 1人工',
`audit_remark` varchar(255) DEFAULT '' COMMENT '审核备注',
`like_count` int DEFAULT 0 COMMENT '点赞数',
`comment_count` int DEFAULT 0 COMMENT '评论数',
`is_delete` tinyint DEFAULT 0 COMMENT '是否删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`),
KEY `idx_festival` (`festival_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树心愿表';
-- 3. 点赞表
CREATE TABLE IF NOT EXISTS `eb_wishtree_like` (
`id` bigint NOT NULL AUTO_INCREMENT,
`uid` int NOT NULL COMMENT '用户ID',
`wish_id` bigint NOT NULL COMMENT '心愿ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uid_wish` (`uid`, `wish_id`),
KEY `idx_wish` (`wish_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树点赞表';
-- 4. 评论表
CREATE TABLE IF NOT EXISTS `eb_wishtree_comment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`wish_id` bigint NOT NULL COMMENT '心愿ID',
`uid` int NOT NULL COMMENT '用户ID',
`content` varchar(255) NOT NULL COMMENT '评论内容',
`parent_id` bigint DEFAULT 0 COMMENT '父评论ID',
`status` tinyint DEFAULT 1 COMMENT '状态',
`is_delete` tinyint DEFAULT 0 COMMENT '是否删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_wish` (`wish_id`),
KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树评论表';
-- 5. 背景素材表
CREATE TABLE IF NOT EXISTS `eb_wishtree_background` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '背景名称',
`image` varchar(255) NOT NULL COMMENT '背景图片',
`festival_id` int DEFAULT 0 COMMENT '关联节日ID',
`sort` int DEFAULT 0 COMMENT '排序',
`status` tinyint DEFAULT 1 COMMENT '状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树背景素材表';
-- ============================================
-- 初始化节日数据
-- ============================================
INSERT IGNORE INTO `eb_wishtree_festival` (`id`, `name`, `icon`, `start_date`, `end_date`, `is_lunar`, `theme_color`, `sort`, `status`) VALUES
(1, '元旦', '🎉', '12-31', '01-03', 0, '#FF6B6B', 100, 1),
(2, '春节', '🧧', '除夕', '正月十五', 1, '#E74C3C', 99, 1),
(3, '情人节', '💕', '02-13', '02-15', 0, '#FF69B4', 98, 1),
(4, '生日祝福', '🎂', '全年', '全年', 0, '#FF69B4', 50, 1),
(5, '日常祝福', '', '全年', '全年', 0, '#9B59B6', 49, 1);
-- ============================================
-- 验证表是否创建成功
-- ============================================
SELECT 'eb_wishtree_festival' AS table_name, COUNT(*) AS count FROM eb_wishtree_festival;
SELECT 'eb_wishtree_wish' AS table_name, COUNT(*) AS count FROM eb_wishtree_wish;
SELECT 'eb_wishtree_like' AS table_name, COUNT(*) AS count FROM eb_wishtree_like;
SELECT 'eb_wishtree_comment' AS table_name, COUNT(*) AS count FROM eb_wishtree_comment;
SELECT 'eb_wishtree_background' AS table_name, COUNT(*) AS count FROM eb_wishtree_background;

View File

@ -1,9 +1,9 @@
package com.example.livestreaming;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
@ -16,24 +16,48 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.WishtreeRequest;
import com.example.livestreaming.net.WishtreeResponse;
import com.example.livestreaming.net.ApiService;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 许愿树页面
* 数据通过API实时同步到服务端
*/
public class WishTreeActivity extends AppCompatActivity {
private ActivityWishTreeBinding binding;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable timerRunnable;
private SharedPreferences prefs;
private String[] wishes = new String[7];
private ApiService apiService;
// 心愿数据从服务端获取
private List<WishtreeResponse.Wish> myWishes = new ArrayList<>();
// 当前节日
private WishtreeResponse.Festival currentFestival;
// 是否正在加载
private boolean isLoading = false;
public static void start(Context context) {
context.startActivity(new Intent(context, WishTreeActivity.class));
@ -45,36 +69,112 @@ public class WishTreeActivity extends AppCompatActivity {
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
prefs = getSharedPreferences("wishes", MODE_PRIVATE);
loadWishes();
apiService = ApiClient.getService(this);
startBannerCountdown();
setupBottomNav();
setupWishCards();
setupMakeWishButton();
setupTopBarButtons();
// 加载数据
loadCurrentFestival();
loadMyWishes();
}
private void loadWishes() {
for (int i = 0; i < 7; i++) {
wishes[i] = prefs.getString("wish_" + i, "");
/**
* 设置顶部栏按钮
*/
private void setupTopBarButtons() {
// 搜索按钮
binding.searchButton.setOnClickListener(v -> {
SearchActivity.start(this);
});
// 通知按钮
binding.notifyButton.setOnClickListener(v -> {
NotificationsActivity.start(this);
});
}
/**
* 检查是否已登录
*/
private boolean checkLogin() {
String token = AuthStore.getToken(this);
if (token == null || token.isEmpty()) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, LoginActivity.class));
return false;
}
updateWishCards();
return true;
}
private void saveWish(int index, String wish) {
wishes[index] = wish;
prefs.edit().putString("wish_" + index, wish).apply();
updateWishCards();
/**
* 加载当前节日
*/
private void loadCurrentFestival() {
apiService.getCurrentFestival().enqueue(new Callback<ApiResponse<WishtreeResponse.Festival>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call,
@NonNull Response<ApiResponse<WishtreeResponse.Festival>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
currentFestival = response.body().getData();
updateBannerText();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call, @NonNull Throwable t) {
// 使用默认文案
}
});
}
/**
* 更新横幅文字
*/
private void updateBannerText() {
if (currentFestival != null && binding != null) {
String text = currentFestival.name + "许愿树 | 许下心愿,愿望成真";
binding.bannerText.setText(text);
}
}
/**
* 从服务端加载我的心愿
*/
private void loadMyWishes() {
apiService.getMyWishes(1, 10).enqueue(new Callback<ApiResponse<WishtreeResponse.WishPage>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call,
@NonNull Response<ApiResponse<WishtreeResponse.WishPage>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
myWishes = response.body().getData().list;
if (myWishes == null) myWishes = new ArrayList<>();
updateWishCards();
updateWishCount();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call, @NonNull Throwable t) {
Toast.makeText(WishTreeActivity.this, "加载心愿失败", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 更新心愿卡片显示
*/
private void updateWishCards() {
TextView[] cards = {
binding.wishCard1, binding.wishCard2, binding.wishCard3,
binding.wishCard4, binding.wishCard5, binding.wishCard6, binding.wishCard7
};
for (int i = 0; i < cards.length; i++) {
if (wishes[i] != null && !wishes[i].isEmpty()) {
String text = wishes[i].length() > 8 ? wishes[i].substring(0, 8) + "..." : wishes[i];
if (i < myWishes.size() && myWishes.get(i) != null) {
String content = myWishes.get(i).content;
String text = content.length() > 8 ? content.substring(0, 8) + "..." : content;
cards[i].setText(text);
} else {
cards[i].setText("");
@ -82,6 +182,17 @@ public class WishTreeActivity extends AppCompatActivity {
}
}
/**
* 更新祈愿值显示
*/
private void updateWishCount() {
int count = myWishes != null ? myWishes.size() : 0;
binding.tvWishCount.setText("祈愿值:" + count + "/100");
}
/**
* 设置心愿卡片点击事件
*/
private void setupWishCards() {
TextView[] cards = {
binding.wishCard1, binding.wishCard2, binding.wishCard3,
@ -91,29 +202,36 @@ public class WishTreeActivity extends AppCompatActivity {
final int index = i;
cards[i].setOnClickListener(v -> onWishCardClick(index));
}
binding.addWishCard.setOnClickListener(v -> showMakeWishInputDialog(-1));
binding.addWishCard.setOnClickListener(v -> showMakeWishInputDialog());
}
/**
* 心愿卡片点击
*/
private void onWishCardClick(int index) {
if (wishes[index] != null && !wishes[index].isEmpty()) {
if (index < myWishes.size() && myWishes.get(index) != null) {
showViewWishDialog(index);
} else {
showMakeWishInputDialog(index);
showMakeWishInputDialog();
}
}
private void showMakeWishInputDialog(int index) {
/**
* 显示许愿输入对话框
*/
private void showMakeWishInputDialog() {
// 检查登录状态
if (!checkLogin()) return;
Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.dialog_make_wish);
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
// 设置对话框宽度为屏幕宽度的85%
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.85);
dialog.getWindow().setLayout(width, android.view.WindowManager.LayoutParams.WRAP_CONTENT);
}
EditText input = dialog.findViewById(R.id.editWish);
TextView tvCharCount = dialog.findViewById(R.id.tvCharCount);
View btnCancel = dialog.findViewById(R.id.btnCancel);
@ -136,14 +254,11 @@ public class WishTreeActivity extends AppCompatActivity {
btnMakeWish.setOnClickListener(v -> {
String wish = input.getText().toString().trim();
if (!wish.isEmpty()) {
int saveIndex = index >= 0 ? index : findEmptySlot();
if (saveIndex >= 0) {
saveWish(saveIndex, wish);
dialog.dismiss();
showSuccessDialog();
} else {
Toast.makeText(this, "心愿牌已满", Toast.LENGTH_SHORT).show();
if (wish.length() > 50) {
Toast.makeText(this, "心愿内容不能超过50字", Toast.LENGTH_SHORT).show();
return;
}
publishWish(wish, dialog);
} else {
Toast.makeText(this, "请输入心愿内容", Toast.LENGTH_SHORT).show();
}
@ -152,13 +267,42 @@ public class WishTreeActivity extends AppCompatActivity {
dialog.show();
}
private int findEmptySlot() {
for (int i = 0; i < wishes.length; i++) {
if (wishes[i] == null || wishes[i].isEmpty()) return i;
}
return -1;
/**
* 发布心愿到服务端
*/
private void publishWish(String content, Dialog dialog) {
Integer festivalId = currentFestival != null ? currentFestival.id : null;
WishtreeRequest.PublishWish request = new WishtreeRequest.PublishWish(festivalId, content, null);
apiService.publishWish(request).enqueue(new Callback<ApiResponse<WishtreeResponse.Wish>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call,
@NonNull Response<ApiResponse<WishtreeResponse.Wish>> response) {
if (response.isSuccessful() && response.body() != null) {
if (response.body().getCode() == 200) {
dialog.dismiss();
showSuccessDialog();
loadMyWishes(); // 重新加载心愿列表
} else {
Toast.makeText(WishTreeActivity.this,
response.body().getMessage() != null ? response.body().getMessage() : "发布失败",
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(WishTreeActivity.this, "发布失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call, @NonNull Throwable t) {
Toast.makeText(WishTreeActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 显示成功对话框
*/
private void showSuccessDialog() {
Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
@ -170,12 +314,20 @@ public class WishTreeActivity extends AppCompatActivity {
handler.postDelayed(dialog::dismiss, 1500);
}
/**
* 显示查看心愿对话框
*/
private void showViewWishDialog(int index) {
if (index >= myWishes.size()) return;
WishtreeResponse.Wish wish = myWishes.get(index);
Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.dialog_view_wish);
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.85);
dialog.getWindow().setLayout(width, android.view.WindowManager.LayoutParams.WRAP_CONTENT);
}
TextView tvContent = dialog.findViewById(R.id.tvWishContent);
@ -183,27 +335,94 @@ public class WishTreeActivity extends AppCompatActivity {
TextView btnDelete = dialog.findViewById(R.id.btnDeleteWish);
TextView btnComplete = dialog.findViewById(R.id.btnCompleteWish);
tvContent.setText(wishes[index]);
tvContent.setText(wish.content);
// 显示点赞数和评论数
TextView tvLikeCount = dialog.findViewById(R.id.tvLikeCount);
TextView tvCommentCount = dialog.findViewById(R.id.tvCommentCount);
if (tvLikeCount != null) {
tvLikeCount.setText("" + wish.likeCount);
}
if (tvCommentCount != null) {
tvCommentCount.setText("💬 " + wish.commentCount);
}
btnClose.setOnClickListener(v -> dialog.dismiss());
// 删除心愿 - 添加确认对话框
btnDelete.setOnClickListener(v -> {
saveWish(index, "");
Toast.makeText(this, "心愿已删除", Toast.LENGTH_SHORT).show();
dialog.dismiss();
new AlertDialog.Builder(this)
.setTitle("确认删除")
.setMessage("确定要删除这个心愿吗?")
.setPositiveButton("删除", (d, w) -> {
deleteWish(wish.id, dialog, false);
})
.setNegativeButton("取消", null)
.show();
});
// 愿望达成 - 添加确认对话框
btnComplete.setOnClickListener(v -> {
saveWish(index, "");
Toast.makeText(this, "恭喜愿望达成!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
new AlertDialog.Builder(this)
.setTitle("愿望达成")
.setMessage("恭喜!确认愿望已经达成了吗?")
.setPositiveButton("确认", (d, w) -> {
deleteWish(wish.id, dialog, true);
})
.setNegativeButton("取消", null)
.show();
});
dialog.show();
}
/**
* 删除心愿
* @param wishId 心愿ID
* @param dialog 对话框
* @param isComplete 是否是愿望达成
*/
private void deleteWish(long wishId, Dialog dialog, boolean isComplete) {
apiService.deleteMyWish(wishId).enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<String>> call,
@NonNull Response<ApiResponse<String>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getCode() == 200) {
dialog.dismiss();
if (isComplete) {
showCompleteSuccessDialog();
} else {
Toast.makeText(WishTreeActivity.this, "心愿已删除", Toast.LENGTH_SHORT).show();
}
loadMyWishes(); // 重新加载
} else {
Toast.makeText(WishTreeActivity.this, "操作失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<String>> call, @NonNull Throwable t) {
Toast.makeText(WishTreeActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 显示愿望达成成功对话框
*/
private void showCompleteSuccessDialog() {
Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.dialog_wish_complete);
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
dialog.show();
handler.postDelayed(dialog::dismiss, 2000);
}
private void setupMakeWishButton() {
binding.btnMakeWish.setOnClickListener(v -> showMakeWishInputDialog(-1));
binding.btnMakeWish.setOnClickListener(v -> showMakeWishInputDialog());
}
private void setupBottomNav() {
@ -295,5 +514,7 @@ public class WishTreeActivity extends AppCompatActivity {
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
}
// 每次返回页面时刷新数据
loadMyWishes();
}
}

View File

@ -387,4 +387,88 @@ public interface ApiService {
@POST("api/front/live/rooms/{roomId}/broadcast/online")
Call<ApiResponse<Map<String, Object>>> broadcastOnlineCount(@Path("roomId") String roomId);
// ==================== 许愿树接口 ====================
/**
* 获取节日列表
*/
@GET("api/front/wishtree/festivals")
Call<ApiResponse<List<WishtreeResponse.Festival>>> getWishtreeFestivals();
/**
* 获取当前进行中的节日
*/
@GET("api/front/wishtree/festivals/current")
Call<ApiResponse<WishtreeResponse.Festival>> getCurrentFestival();
/**
* 获取心愿列表
*/
@GET("api/front/wishtree/wishes")
Call<ApiResponse<WishtreeResponse.WishPage>> getWishtreeWishes(
@Query("festivalId") Integer festivalId,
@Query("page") int page,
@Query("limit") int limit);
/**
* 获取我的心愿列表
*/
@GET("api/front/wishtree/wishes/my")
Call<ApiResponse<WishtreeResponse.WishPage>> getMyWishes(
@Query("page") int page,
@Query("limit") int limit);
/**
* 发布心愿
*/
@POST("api/front/wishtree/wishes")
Call<ApiResponse<WishtreeResponse.Wish>> publishWish(@Body WishtreeRequest.PublishWish body);
/**
* 获取心愿详情
*/
@GET("api/front/wishtree/wishes/{id}")
Call<ApiResponse<WishtreeResponse.Wish>> getWishDetail(@Path("id") long id);
/**
* 删除我的心愿
*/
@DELETE("api/front/wishtree/wishes/{id}")
Call<ApiResponse<String>> deleteMyWish(@Path("id") long id);
/**
* 点赞心愿
*/
@POST("api/front/wishtree/wishes/{id}/like")
Call<ApiResponse<String>> likeWish(@Path("id") long id);
/**
* 取消点赞
*/
@DELETE("api/front/wishtree/wishes/{id}/like")
Call<ApiResponse<String>> unlikeWish(@Path("id") long id);
/**
* 获取评论列表
*/
@GET("api/front/wishtree/wishes/{id}/comments")
Call<ApiResponse<WishtreeResponse.CommentPage>> getWishComments(
@Path("id") long wishId,
@Query("page") int page,
@Query("limit") int limit);
/**
* 发表评论
*/
@POST("api/front/wishtree/wishes/{id}/comments")
Call<ApiResponse<WishtreeResponse.Comment>> addWishComment(
@Path("id") long wishId,
@Body WishtreeRequest.AddComment body);
/**
* 获取背景素材列表
*/
@GET("api/front/wishtree/backgrounds")
Call<ApiResponse<List<WishtreeResponse.Background>>> getWishtreeBackgrounds();
}

View File

@ -0,0 +1,50 @@
package com.example.livestreaming.net;
import com.google.gson.annotations.SerializedName;
/**
* 许愿树相关请求类
*/
public class WishtreeRequest {
/**
* 发布心愿请求
*/
public static class PublishWish {
@SerializedName("festivalId")
public Integer festivalId;
@SerializedName("content")
public String content;
@SerializedName("backgroundId")
public Integer backgroundId;
public PublishWish(String content) {
this.content = content;
}
public PublishWish(Integer festivalId, String content, Integer backgroundId) {
this.festivalId = festivalId;
this.content = content;
this.backgroundId = backgroundId;
}
}
/**
* 发表评论请求
*/
public static class AddComment {
@SerializedName("content")
public String content;
@SerializedName("parentId")
public Long parentId;
public AddComment(String content) {
this.content = content;
}
public AddComment(String content, Long parentId) {
this.content = content;
this.parentId = parentId;
}
}
}

View File

@ -0,0 +1,136 @@
package com.example.livestreaming.net;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* 许愿树相关响应类
*/
public class WishtreeResponse {
/**
* 节日信息
*/
public static class Festival {
@SerializedName("id")
public int id;
@SerializedName("name")
public String name;
@SerializedName("icon")
public String icon;
@SerializedName("startDate")
public String startDate;
@SerializedName("endDate")
public String endDate;
@SerializedName("isLunar")
public int isLunar;
@SerializedName("themeColor")
public String themeColor;
@SerializedName("status")
public int status;
}
/**
* 心愿信息
*/
public static class Wish {
@SerializedName("id")
public long id;
@SerializedName("uid")
public int uid;
@SerializedName("nickname")
public String nickname;
@SerializedName("avatar")
public String avatar;
@SerializedName("festivalId")
public int festivalId;
@SerializedName("festivalName")
public String festivalName;
@SerializedName("festivalIcon")
public String festivalIcon;
@SerializedName("content")
public String content;
@SerializedName("backgroundId")
public int backgroundId;
@SerializedName("backgroundImage")
public String backgroundImage;
@SerializedName("status")
public int status;
@SerializedName("likeCount")
public int likeCount;
@SerializedName("commentCount")
public int commentCount;
@SerializedName("isLiked")
public Boolean isLiked;
@SerializedName("createTime")
public String createTime;
}
/**
* 背景素材
*/
public static class Background {
@SerializedName("id")
public int id;
@SerializedName("name")
public String name;
@SerializedName("image")
public String image;
@SerializedName("festivalId")
public int festivalId;
@SerializedName("status")
public int status;
}
/**
* 评论信息
*/
public static class Comment {
@SerializedName("id")
public long id;
@SerializedName("wishId")
public long wishId;
@SerializedName("uid")
public int uid;
@SerializedName("nickname")
public String nickname;
@SerializedName("avatar")
public String avatar;
@SerializedName("content")
public String content;
@SerializedName("parentId")
public long parentId;
@SerializedName("status")
public int status;
@SerializedName("createTime")
public String createTime;
}
/**
* 分页数据
*/
public static class WishPage {
@SerializedName("list")
public List<Wish> list;
@SerializedName("total")
public int total;
@SerializedName("pageNum")
public int pageNum;
@SerializedName("pageSize")
public int pageSize;
}
/**
* 评论分页数据
*/
public static class CommentPage {
@SerializedName("list")
public List<Comment> list;
@SerializedName("total")
public int total;
@SerializedName("pageNum")
public int pageNum;
@SerializedName("pageSize")
public int pageSize;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="22dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF6B9D" />
<corners android:radius="22dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#E0E0E0" />
</shape>

View File

@ -3,13 +3,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:background="@drawable/bg_dialog_rounded"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="许下你的愿望"
android:textSize="18sp"
android:textColor="#333333"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
@ -19,6 +21,54 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="写下你的愿望..."
android:textColorHint="#999999"
android:textColor="#333333"
android:minLines="3"
android:gravity="top" />
android:maxLength="50"
android:gravity="top"
android:padding="12dp"
android:background="@drawable/bg_edit_text"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvCharCount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0/50"
android:textSize="12sp"
android:textColor="#999999"
android:gravity="end"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="取消"
android:textSize="16sp"
android:textColor="#666666"
android:gravity="center"
android:background="@drawable/bg_btn_cancel"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/btnMakeWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="许愿"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:gravity="center"
android:background="@drawable/bg_btn_primary"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_dialog_rounded"
android:padding="20dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="我的心愿"
android:textSize="18sp"
android:textColor="#333333"
android:textStyle="bold" />
<ImageView
android:id="@+id/btnClose"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="关闭" />
</RelativeLayout>
<TextView
android:id="@+id/tvWishContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="80dp"
android:text=""
android:textSize="16sp"
android:textColor="#333333"
android:padding="12dp"
android:background="@drawable/bg_edit_text"
android:layout_marginBottom="12dp" />
<!-- 点赞数和评论数 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvLikeCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="❤ 0"
android:textSize="14sp"
android:textColor="#FF6B6B"
android:layout_marginEnd="20dp" />
<TextView
android:id="@+id/tvCommentCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="💬 0"
android:textSize="14sp"
android:textColor="#666666" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/btnDeleteWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="删除心愿"
android:textSize="16sp"
android:textColor="#FF6B6B"
android:gravity="center"
android:background="@drawable/bg_btn_cancel"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/btnCompleteWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="愿望达成"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:gravity="center"
android:background="@drawable/bg_btn_primary"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_dialog_rounded"
android:padding="32dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🎉"
android:textSize="48sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="恭喜愿望达成!"
android:textSize="20sp"
android:textColor="#FF6D00"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="愿你心想事成,万事如意"
android:textSize="14sp"
android:textColor="#666666" />
</LinearLayout>

View File

@ -1,8 +1,8 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
# 使用 Android Studio 自带的 JDK 17
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
# JDK配置由Android Studio自动管理
# 请在Android Studio中编译Build -> Rebuild Project
systemProp.gradle.wrapperUser=myuser
systemProp.gradle.wrapperPassword=mypassword

View File

@ -0,0 +1,537 @@
# 许愿树移动端功能设计文档
> 版本V1.0
> 更新日期2025-12-31
> 设计范围Android移动端许愿树功能
---
## 一、功能概述
### 1.1 核心特性
- **服务端实时存储**心愿数据通过API实时同步到服务端
- **节日主题**:支持不同节日的许愿活动
- **社交互动**:支持点赞、评论功能
- **多端同步**:数据云端存储,多设备共享
### 1.2 数据流架构
```
┌─────────────────┐ API请求 ┌─────────────────┐
│ WishTreeActivity│ ◄──────────────► │ 后端服务 │
└─────────────────┘ └─────────────────┘
│ │
│ Retrofit │ MyBatis
▼ ▼
ApiService MySQL数据库
(许愿树接口) (eb_wishtree_*)
```
---
## 二、页面设计
### 2.1 主页面布局 (activity_wish_tree.xml)
```
┌─────────────────────────────────────────────────────────┐
│ [许愿树标题] [🔍] [🔔] │ ← 顶部栏
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🎉 元旦许愿树 | 许下心愿,愿望成真 [12:30 05:00] │ │ ← 活动横幅
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ │
│ [心愿1] [心愿2] [心愿3] │
│ │
│ [心愿4] [心愿+] [心愿5] │ ← 许愿树区域
│ │
│ [心愿6] [心愿7] │
│ │
│ 元 │
│ 旦 │
│ 许 │
│ 愿 │
│ 树 │
│ │
├─────────────────────────────────────────────────────────┤
│ 祈愿值3/100 │ ← 底部区域
│ [ 前往祈愿 ] │
├─────────────────────────────────────────────────────────┤
│ [首页] [缘池] [许愿树] [消息] [我的] │ ← 底部导航
└─────────────────────────────────────────────────────────┘
```
### 2.2 许愿对话框 (dialog_make_wish.xml)
```
┌─────────────────────────────────────┐
│ 许下心愿 │
├─────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ 请输入您的心愿... │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ 0/50 │
├─────────────────────────────────────┤
│ [取消] [许愿] │
└─────────────────────────────────────┘
```
### 2.3 查看心愿对话框 (dialog_view_wish.xml)
```
┌─────────────────────────────────────┐
│ 我的心愿 [✕] │
├─────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ 新年快乐,万事如意! │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ ❤ 128 💬 32 │
├─────────────────────────────────────┤
│ [删除心愿] [愿望达成] │
└─────────────────────────────────────┘
```
### 2.4 成功提示对话框 (dialog_wish_success.xml)
```
┌─────────────────────────────────────┐
│ │
│ ✨ │
│ 许愿成功! │
│ │
└─────────────────────────────────────┘
```
---
## 三、API接口设计
### 3.1 接口列表
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取节日列表 | GET | `/api/front/wishtree/festivals` | 获取所有启用的节日 |
| 获取当前节日 | GET | `/api/front/wishtree/festivals/current` | 获取当前进行中的节日 |
| 获取心愿列表 | GET | `/api/front/wishtree/wishes` | 分页获取心愿列表 |
| 获取我的心愿 | GET | `/api/front/wishtree/wishes/my` | 获取当前用户的心愿 |
| 发布心愿 | POST | `/api/front/wishtree/wishes` | 发布新心愿 |
| 获取心愿详情 | GET | `/api/front/wishtree/wishes/{id}` | 获取心愿详情 |
| 删除心愿 | DELETE | `/api/front/wishtree/wishes/{id}` | 删除我的心愿 |
| 点赞心愿 | POST | `/api/front/wishtree/wishes/{id}/like` | 点赞 |
| 取消点赞 | DELETE | `/api/front/wishtree/wishes/{id}/like` | 取消点赞 |
| 获取评论 | GET | `/api/front/wishtree/wishes/{id}/comments` | 获取评论列表 |
| 发表评论 | POST | `/api/front/wishtree/wishes/{id}/comments` | 发表评论 |
| 获取背景素材 | GET | `/api/front/wishtree/backgrounds` | 获取背景素材列表 |
### 3.2 请求/响应示例
#### 发布心愿
```json
// POST /api/front/wishtree/wishes
// Request:
{
"festivalId": 1,
"content": "新年快乐,万事如意!",
"backgroundId": 1
}
// Response:
{
"code": 200,
"message": "success",
"data": {
"id": 10001,
"uid": 1001,
"festivalId": 1,
"content": "新年快乐,万事如意!",
"status": 1,
"likeCount": 0,
"commentCount": 0,
"createTime": "2025-12-31 12:00:00"
}
}
```
#### 获取我的心愿列表
```json
// GET /api/front/wishtree/wishes/my?page=1&limit=10
// Response:
{
"code": 200,
"data": {
"list": [
{
"id": 10001,
"uid": 1001,
"nickname": "用户昵称",
"avatar": "https://xxx/avatar.jpg",
"festivalId": 1,
"festivalName": "元旦",
"festivalIcon": "🎉",
"content": "新年快乐,万事如意!",
"backgroundImage": "https://xxx/bg1.jpg",
"status": 1,
"likeCount": 128,
"commentCount": 32,
"isLiked": false,
"createTime": "2025-12-31 12:00:00"
}
],
"total": 5,
"pageNum": 1,
"pageSize": 10
}
}
```
---
## 四、代码结构
### 4.1 文件清单
```
android-app/app/src/main/java/com/example/livestreaming/
├── WishTreeActivity.java # 许愿树主页面
└── net/
├── ApiService.java # API接口定义含许愿树接口
├── WishtreeRequest.java # 请求类
│ ├── PublishWish # 发布心愿请求
│ └── AddComment # 发表评论请求
└── WishtreeResponse.java # 响应类
├── Festival # 节日信息
├── Wish # 心愿信息
├── Background # 背景素材
├── Comment # 评论信息
├── WishPage # 心愿分页数据
└── CommentPage # 评论分页数据
android-app/app/src/main/res/layout/
├── activity_wish_tree.xml # 主页面布局
├── dialog_make_wish.xml # 许愿对话框
├── dialog_view_wish.xml # 查看心愿对话框
└── dialog_wish_success.xml # 成功提示对话框
android-app/app/src/main/res/drawable/
├── bg_wish_tree_banner.xml # 横幅背景
├── bg_wish_tree_timer.xml # 倒计时背景
├── bg_wish_card.xml # 心愿卡片背景
├── bg_wish_card_add.xml # 添加心愿卡片背景
├── bg_trunk_text.xml # 树干文字背景
├── bg_dialog_rounded.xml # 对话框圆角背景
├── bg_edit_text.xml # 输入框背景
├── bg_btn_primary.xml # 主按钮背景
└── bg_btn_cancel.xml # 取消按钮背景
```
### 4.2 核心类设计
#### WishTreeActivity.java
```java
public class WishTreeActivity extends AppCompatActivity {
private ActivityWishTreeBinding binding;
private ApiService apiService;
private List<WishtreeResponse.Wish> myWishes;
private WishtreeResponse.Festival currentFestival;
// 生命周期方法
onCreate() // 初始化页面、加载数据
onResume() // 刷新数据
onStart() // 启动倒计时
onStop() // 停止倒计时
// 数据加载
loadCurrentFestival() // 加载当前节日
loadMyWishes() // 加载我的心愿
// UI更新
updateBannerText() // 更新横幅文字
updateWishCards() // 更新心愿卡片
updateWishCount() // 更新祈愿值
updateBannerTimer() // 更新倒计时
// 用户交互
showMakeWishInputDialog() // 显示许愿对话框
showViewWishDialog() // 显示查看心愿对话框
showSuccessDialog() // 显示成功提示
// API调用
publishWish() // 发布心愿
deleteWish() // 删除心愿
}
```
#### WishtreeRequest.java
```java
public class WishtreeRequest {
// 发布心愿请求
public static class PublishWish {
public Integer festivalId;
public String content;
public Integer backgroundId;
}
// 发表评论请求
public static class AddComment {
public String content;
public Long parentId;
}
}
```
#### WishtreeResponse.java
```java
public class WishtreeResponse {
// 节日信息
public static class Festival {
public int id;
public String name;
public String icon;
public String startDate;
public String endDate;
public String themeColor;
public int status;
}
// 心愿信息
public static class Wish {
public long id;
public int uid;
public String nickname;
public String avatar;
public int festivalId;
public String festivalName;
public String festivalIcon;
public String content;
public String backgroundImage;
public int status;
public int likeCount;
public int commentCount;
public Boolean isLiked;
public String createTime;
}
// 背景素材
public static class Background {
public int id;
public String name;
public String image;
public int festivalId;
public int status;
}
// 评论信息
public static class Comment {
public long id;
public long wishId;
public int uid;
public String nickname;
public String avatar;
public String content;
public long parentId;
public String createTime;
}
}
```
---
## 五、交互流程
### 5.1 页面加载流程
```
┌─────────────┐
│ onCreate │
└──────┬──────┘
┌─────────────────────────────────────────────────────┐
│ 1. 初始化ApiService │
│ 2. 设置底部导航 │
│ 3. 设置心愿卡片点击事件 │
│ 4. 启动横幅倒计时 │
│ 5. 加载当前节日 (loadCurrentFestival) │
│ 6. 加载我的心愿 (loadMyWishes) │
└─────────────────────────────────────────────────────┘
```
### 5.2 发布心愿流程
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 点击许愿按钮 │ ──► │ 显示许愿对话框│ ──► │ 输入心愿内容 │
└─────────────┘ └─────────────┘ └──────┬──────┘
┌───────────────────────────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 点击许愿按钮 │ ──► │ 调用API发布 │ ──► │ 显示成功提示 │
└─────────────┘ └─────────────┘ └──────┬──────┘
┌───────────────────────────────────────┘
┌─────────────────────┐
│ 重新加载心愿列表 │
│ 更新心愿卡片显示 │
└─────────────────────┘
```
### 5.3 查看/删除心愿流程
```
┌─────────────┐ ┌─────────────┐
│ 点击心愿卡片 │ ──► │ 显示心愿详情 │
└─────────────┘ └──────┬──────┘
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 点击关闭 │ │ 点击删除 │ │ 愿望达成 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 关闭对话框 │ │ 调用API删除│ │ 调用API删除│
└───────────┘ │ 刷新列表 │ │ 显示祝贺 │
└───────────┘ └───────────┘
```
---
## 六、状态管理
### 6.1 心愿状态
| 状态值 | 说明 | 显示 |
|--------|------|------|
| 0 | 待审核 | 审核中 |
| 1 | 已通过 | 正常显示 |
| 2 | 已拒绝 | 不显示 |
### 6.2 页面状态
```java
// 心愿数据
private List<WishtreeResponse.Wish> myWishes = new ArrayList<>();
// 当前节日
private WishtreeResponse.Festival currentFestival;
// 状态更新时机
- onCreate: 初始加载
- onResume: 每次返回页面刷新
- 发布/删除心愿后: 重新加载
```
---
## 七、错误处理
### 7.1 网络错误
```java
@Override
public void onFailure(Call<...> call, Throwable t) {
Toast.makeText(context, "网络错误", Toast.LENGTH_SHORT).show();
}
```
### 7.2 业务错误
```java
if (response.body().getCode() != 200) {
String msg = response.body().getMessage();
Toast.makeText(context, msg != null ? msg : "操作失败", Toast.LENGTH_SHORT).show();
}
```
### 7.3 空数据处理
```java
if (myWishes == null) {
myWishes = new ArrayList<>();
}
```
---
## 八、后续扩展
### 8.1 待实现功能
- [ ] 心愿列表页面(查看所有用户心愿)
- [ ] 点赞功能
- [ ] 评论功能
- [ ] 背景素材选择
- [ ] 节日主题切换
- [ ] 心愿分享功能
### 8.2 优化方向
- 添加加载状态指示器
- 添加下拉刷新
- 添加心愿卡片动画效果
- 支持心愿图片上传
- 添加本地缓存减少网络请求
---
## 九、数据库表结构
### 9.1 相关表
```sql
-- 节日表
eb_wishtree_festival (id, name, icon, start_date, end_date, theme_color, status, ...)
-- 心愿表
eb_wishtree_wish (id, uid, festival_id, content, background_id, status, like_count, comment_count, ...)
-- 点赞表
eb_wishtree_like (id, uid, wish_id, create_time)
-- 评论表
eb_wishtree_comment (id, wish_id, uid, content, parent_id, status, ...)
-- 背景素材表
eb_wishtree_background (id, name, image, festival_id, status, ...)
```
---
## 十、测试要点
### 10.1 功能测试
- [ ] 页面正常加载
- [ ] 节日信息正确显示
- [ ] 心愿卡片正确显示
- [ ] 发布心愿成功
- [ ] 删除心愿成功
- [ ] 愿望达成功能正常
- [ ] 底部导航切换正常
### 10.2 异常测试
- [ ] 网络断开时的提示
- [ ] 未登录时的处理
- [ ] 空数据时的显示
- [ ] 心愿内容为空时的校验