移动端,首页、缘池、许愿树、我的,页面优化,功能完善。

This commit is contained in:
cxytw 2026-01-02 20:39:14 +08:00
parent 6e8f5c4011
commit 7042137e5b
114 changed files with 9099 additions and 1122 deletions

View File

@ -55,3 +55,12 @@ export function memberPackageToggleHotApi(id, data) {
data
})
}
// 切换启用/禁用状态
export function memberPackageToggleStatusApi(id, data) {
return request({
url: `/admin/member/package/toggleStatus/${id}`,
method: 'post',
data
})
}

View File

@ -11,20 +11,50 @@
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading" border>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="months" label="月数" width="100" align="center" />
<el-table-column prop="price" label="价格" width="120" align="center" />
<el-table-column prop="ios_price" label="ios端价格" width="120" align="center" />
<el-table-column prop="label" label="标识" align="center" />
<el-table-column prop="ios_product_id" label="ios产品ID" align="center" />
<el-table-column label="操作" width="300" align="center" fixed="right">
<el-table-column prop="id" label="ID" width="60" align="center" />
<el-table-column prop="name" label="套餐名称" width="120" align="center" />
<el-table-column prop="duration" label="有效期(天)" width="100" align="center" />
<el-table-column prop="original_price" label="原价" width="100" align="center">
<template slot-scope="scope">
¥{{ scope.row.original_price }}
</template>
</el-table-column>
<el-table-column prop="price" label="现价" width="100" align="center">
<template slot-scope="scope">
¥{{ scope.row.price }}
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column label="热销" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.is_hot === 1 ? 'danger' : 'info'" size="small">
{{ scope.row.is_hot === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
<template slot-scope="scope">
<el-button
:type="scope.row.is_hot === 1 ? 'success' : 'info'"
:type="scope.row.is_hot === 1 ? 'danger' : 'default'"
size="mini"
@click="handleToggleHot(scope.row)"
>
{{ scope.row.is_hot === 1 ? '热销推荐' : '热销推荐' }}
{{ scope.row.is_hot === 1 ? '取消热销' : '设为热销' }}
</el-button>
<el-button
:type="scope.row.status === 1 ? 'info' : 'success'"
size="mini"
@click="handleToggleStatus(scope.row)"
>
{{ scope.row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button
type="warning"
@ -64,7 +94,7 @@
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="600px"
width="650px"
:close-on-click-modal="false"
>
<el-form
@ -73,48 +103,104 @@
ref="form"
label-width="100px"
>
<el-form-item label="月数" prop="months">
<el-form-item label="套餐名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入套餐名称,如:月度会员"
maxlength="64"
show-word-limit
/>
</el-form-item>
<el-form-item label="有效期(天)" prop="duration">
<el-input-number
v-model="form.months"
v-model="form.duration"
:min="1"
placeholder="请输入月数"
placeholder="请输入有效期天数"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number
v-model="form.price"
:min="0"
:precision="2"
placeholder="请输入价格"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="ios端价格" prop="iosPrice">
<el-input-number
v-model="form.iosPrice"
:min="0"
:precision="2"
placeholder="请输入ios端价格"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="标识" prop="label">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="原价" prop="originalPrice">
<el-input-number
v-model="form.originalPrice"
:min="0"
:precision="2"
placeholder="请输入原价"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="现价" prop="price">
<el-input-number
v-model="form.price"
:min="0"
:precision="2"
placeholder="请输入现价"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="套餐描述" prop="description">
<el-input
v-model="form.label"
placeholder="请输入标识"
maxlength="50"
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入套餐描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="ios产品ID" prop="iosProductId">
<el-form-item label="套餐图标" prop="icon">
<el-input
v-model="form.iosProductId"
placeholder="请输入ios产品ID"
maxlength="100"
show-word-limit
v-model="form.icon"
placeholder="请输入图标URL或图标名称"
maxlength="255"
/>
</el-form-item>
<el-form-item label="特权列表" prop="privileges">
<el-input
v-model="form.privileges"
type="textarea"
:rows="3"
placeholder='请输入特权列表JSON["无限聊天","专属标识","优先推荐"]'
maxlength="1000"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="form.sort"
:min="0"
placeholder="排序"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="热销推荐" prop="isHot">
<el-switch
v-model="form.isHot"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">返回</el-button>
@ -132,7 +218,8 @@ import {
memberPackageAddApi,
memberPackageUpdateApi,
memberPackageDeleteApi,
memberPackageToggleHotApi
memberPackageToggleHotApi,
memberPackageToggleStatusApi
} from '@/api/memberPackage';
export default {
@ -151,27 +238,26 @@ export default {
submitting: false,
form: {
id: null,
months: null,
price: null,
iosPrice: null,
label: '',
iosProductId: ''
name: '',
description: '',
icon: '',
duration: 30,
originalPrice: 0,
price: 0,
privileges: '[]',
isHot: 0,
sort: 0,
status: 1
},
formRules: {
months: [
{ required: true, message: '请输入月数', trigger: 'blur' }
name: [
{ required: true, message: '请输入套餐名称', trigger: 'blur' }
],
duration: [
{ required: true, message: '请输入有效期', trigger: 'blur' }
],
price: [
{ required: true, message: '请输入价格', trigger: 'blur' }
],
iosPrice: [
{ required: true, message: '请输入ios端价格', trigger: 'blur' }
],
label: [
{ required: true, message: '请输入标识', trigger: 'blur' }
],
iosProductId: [
{ required: true, message: '请输入ios产品ID', trigger: 'blur' }
{ required: true, message: '请输入现价', trigger: 'blur' }
]
}
};
@ -208,14 +294,19 @@ export default {
//
handleAdd() {
this.dialogTitle = '添加';
this.dialogTitle = '添加会员套餐';
this.form = {
id: null,
months: null,
price: null,
iosPrice: null,
label: '',
iosProductId: ''
name: '',
description: '',
icon: '',
duration: 30,
originalPrice: 0,
price: 0,
privileges: '[]',
isHot: 0,
sort: 0,
status: 1
};
this.dialogVisible = true;
this.$nextTick(() => {
@ -225,14 +316,19 @@ export default {
//
handleEdit(row) {
this.dialogTitle = '编辑';
this.dialogTitle = '编辑会员套餐';
this.form = {
id: row.id,
months: row.months,
price: row.price,
iosPrice: row.ios_price,
label: row.label,
iosProductId: row.ios_product_id
name: row.name || '',
description: row.description || '',
icon: row.icon || '',
duration: row.duration || 30,
originalPrice: row.original_price || 0,
price: row.price || 0,
privileges: row.privileges || '[]',
isHot: row.is_hot || 0,
sort: row.sort || 0,
status: row.status || 1
};
this.dialogVisible = true;
this.$nextTick(() => {
@ -266,7 +362,7 @@ export default {
//
handleDelete(row) {
this.$confirm('确定要删除该会员吗?', '提示', {
this.$confirm('确定要删除该会员套餐吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@ -297,6 +393,25 @@ export default {
});
})
.catch(() => {});
},
// /
handleToggleStatus(row) {
const newStatus = row.status === 1 ? 0 : 1;
const msg = newStatus === 1 ? '启用' : '禁用';
this.$confirm(`确定要${msg}该套餐吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
memberPackageToggleStatusApi(row.id, { status: newStatus }).then(() => {
this.$message.success('状态更新成功');
this.getList();
});
})
.catch(() => {});
}
}
};

View File

@ -6,7 +6,7 @@
<el-form-item label="用户地址:" prop="addres">
<el-input v-model="ruleForm.addres"></el-input>
</el-form-item>
<el-form-item label="用户备注" prop="mark">
<el-form-item label="个性签名" prop="mark">
<el-input v-model="ruleForm.mark" type="textarea"></el-input>
</el-form-item>
<el-form-item label="用户分组:" prop="groupId">
@ -73,7 +73,7 @@ export default {
rules: {
id: [{ required: true, message: '请输入用户编号', trigger: 'change' }],
addres: [{ required: true, message: '请输入用户地址', trigger: 'change' }],
mark: [{ required: true, message: '请输入用户备注', trigger: 'blur' }],
mark: [{ required: true, message: '请输入个性签名', trigger: 'blur' }],
groupId: [{ required: true, message: '请选择用户分组', trigger: 'blur' }],
isPromoter: [{ required: true, message: '请选择状态', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }],

View File

@ -164,10 +164,10 @@
</div>
<div class="user-info">
<div class="section">
<div class="section-hd">用户备注</div>
<div class="section-hd">个性签名</div>
<div class="section-bd">
<div class="item">
<div>备注</div>
<div>签名</div>
<div class="value">{{ userDetailData.mark || '-' }}</div>
</div>
</div>

View File

@ -42,7 +42,7 @@ public class MemberPackageController {
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class);
// 分页查询
String sql = "SELECT * FROM eb_member_package ORDER BY id ASC LIMIT ?, ?";
String sql = "SELECT * FROM eb_member_package ORDER BY sort ASC, id ASC LIMIT ?, ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, (page - 1) * limit, limit);
CommonPage<Map<String, Object>> result = new CommonPage<>();
@ -82,13 +82,18 @@ public class MemberPackageController {
@RequestMapping(value = "/add", method = RequestMethod.POST)
public CommonResult<String> add(@RequestBody Map<String, Object> params) {
try {
String sql = "INSERT INTO eb_member_package (months, price, ios_price, label, ios_product_id) VALUES (?, ?, ?, ?, ?)";
String sql = "INSERT INTO eb_member_package (name, description, icon, duration, original_price, price, privileges, is_hot, sort, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
params.get("months"),
params.get("price"),
params.get("iosPrice"),
params.get("label"),
params.get("iosProductId")
params.getOrDefault("name", ""),
params.getOrDefault("description", ""),
params.getOrDefault("icon", ""),
params.getOrDefault("duration", 30),
params.getOrDefault("originalPrice", 0),
params.getOrDefault("price", 0),
params.getOrDefault("privileges", "[]"),
params.getOrDefault("isHot", 0),
params.getOrDefault("sort", 0),
params.getOrDefault("status", 1)
);
return CommonResult.success("添加成功");
} catch (Exception e) {
@ -104,13 +109,18 @@ public class MemberPackageController {
@RequestMapping(value = "/update", method = RequestMethod.POST)
public CommonResult<String> update(@RequestBody Map<String, Object> params) {
try {
String sql = "UPDATE eb_member_package SET months = ?, price = ?, ios_price = ?, label = ?, ios_product_id = ? WHERE id = ?";
String sql = "UPDATE eb_member_package SET name = ?, description = ?, icon = ?, duration = ?, original_price = ?, price = ?, privileges = ?, is_hot = ?, sort = ?, status = ? WHERE id = ?";
jdbcTemplate.update(sql,
params.get("months"),
params.get("price"),
params.get("iosPrice"),
params.get("label"),
params.get("iosProductId"),
params.getOrDefault("name", ""),
params.getOrDefault("description", ""),
params.getOrDefault("icon", ""),
params.getOrDefault("duration", 30),
params.getOrDefault("originalPrice", 0),
params.getOrDefault("price", 0),
params.getOrDefault("privileges", "[]"),
params.getOrDefault("isHot", 0),
params.getOrDefault("sort", 0),
params.getOrDefault("status", 1),
params.get("id")
);
return CommonResult.success("更新成功");
@ -151,4 +161,20 @@ public class MemberPackageController {
return CommonResult.failed("状态更新失败:" + e.getMessage());
}
}
/**
* 切换状态
*/
@ApiOperation(value = "切换状态")
@RequestMapping(value = "/toggleStatus/{id}", method = RequestMethod.POST)
public CommonResult<String> toggleStatus(@PathVariable Integer id, @RequestBody Map<String, Object> params) {
try {
String sql = "UPDATE eb_member_package SET status = ? WHERE id = ?";
jdbcTemplate.update(sql, params.get("status"), id);
return CommonResult.success("状态更新成功");
} catch (Exception e) {
log.error("切换状态失败", e);
return CommonResult.failed("状态更新失败:" + e.getMessage());
}
}
}

View File

@ -36,7 +36,21 @@ public class UserEditRequest implements Serializable {
private String nickname;
@ApiModelProperty(value = "用户头像")
@NotBlank(message = "请上传用户头像")
@Length(max = 255, message = "用户头像不能超过255个字符")
private String avatar;
@ApiModelProperty(value = "生日格式yyyy-MM-dd")
@Length(max = 32, message = "生日格式不正确")
private String birthday;
@ApiModelProperty(value = "性别0=未知1=男2=女3=保密")
private Integer sex;
@ApiModelProperty(value = "地址")
@Length(max = 255, message = "地址不能超过255个字符")
private String addres;
@ApiModelProperty(value = "个人签名/备注")
@Length(max = 255, message = "个人签名不能超过255个字符")
private String mark;
}

View File

@ -44,7 +44,7 @@ public class UserResponse {
@ApiModelProperty(value = "身份证号码")
private String cardId;
@ApiModelProperty(value = "用户备注")
@ApiModelProperty(value = "个性签名")
private String mark;
@ApiModelProperty(value = "合伙人id")

View File

@ -16,7 +16,7 @@ public class NearbyUserVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户ID")
private Integer uid;
private Integer id;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ -24,18 +24,33 @@ public class NearbyUserVO implements Serializable {
@ApiModelProperty(value = "用户头像")
private String avatar;
@ApiModelProperty(value = "性别 0未知 1男 2女")
private Integer sex;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "位置/地址")
private String location;
@ApiModelProperty(value = "个人简介")
private String bio;
@ApiModelProperty(value = "距离(米)")
private Double distance;
@ApiModelProperty(value = "是否在线")
private Boolean isOnline;
@ApiModelProperty(value = "个性签名")
private String signature;
@ApiModelProperty(value = "最后活跃时间")
private String lastActiveTime;
// 兼容旧字段
public void setOnline(Boolean online) {
this.isOnline = online;
}
public Boolean getOnline() {
return this.isOnline;
}
}

View File

@ -58,11 +58,21 @@ public class CommunityFrontController {
@ApiOperation("获取附近用户")
@GetMapping("/nearby-users")
public CommonResult<List<NearbyUserVO>> getNearbyUsers(
public CommonResult<java.util.Map<String, Object>> getNearbyUsers(
@RequestParam Double latitude,
@RequestParam Double longitude,
@RequestParam(defaultValue = "5000") Integer radius,
@RequestParam(defaultValue = "6") Integer limit) {
return CommonResult.success(communityService.getNearbyUsers(latitude, longitude, radius, limit));
List<NearbyUserVO> users = communityService.getNearbyUsers(latitude, longitude, radius, limit);
java.util.Map<String, Object> result = new java.util.HashMap<>();
result.put("list", users);
result.put("total", users.size());
return CommonResult.success(result);
}
@ApiOperation("获取可匹配用户总数")
@GetMapping("/user-count")
public CommonResult<Integer> getMatchableUserCount() {
return CommonResult.success(communityService.getMatchableUserCount());
}
}

View File

@ -179,4 +179,19 @@ public class FriendController {
return CommonResult.failed("获取失败: " + e.getMessage());
}
}
/**
* 检查好友状态是否是好友是否有待处理的好友请求
*/
@ApiOperation(value = "检查好友状态")
@GetMapping("/friends/check/{userId}")
public CommonResult<Map<String, Object>> checkFriendStatus(@PathVariable Integer userId) {
try {
Map<String, Object> result = friendService.checkFriendStatus(userId);
return CommonResult.success(result);
} catch (Exception e) {
log.error("检查好友状态失败: {}", e.getMessage());
return CommonResult.failed("检查失败: " + e.getMessage());
}
}
}

View File

@ -22,6 +22,7 @@ crmeb:
# 配置端口
server:
port: 8081
address: 0.0.0.0 # 绑定到所有网络接口,允许外部访问
servlet:
context-path: / # 访问path
tomcat:

View File

@ -88,4 +88,9 @@ public interface CommunityService {
* 获取附近用户
*/
List<NearbyUserVO> getNearbyUsers(Double latitude, Double longitude, Integer radius, Integer limit);
/**
* 获取可匹配用户总数排除当前用户
*/
Integer getMatchableUserCount();
}

View File

@ -55,4 +55,9 @@ public interface FriendService {
* 获取黑名单列表
*/
CommonPage<Map<String, Object>> getBlockedList(Integer page, Integer pageSize);
/**
* 检查好友状态是否是好友是否有待处理的好友请求
*/
Map<String, Object> checkFriendStatus(Integer targetUserId);
}

View File

@ -8,6 +8,7 @@ import com.zbkj.common.exception.CrmebException;
import com.zbkj.common.model.community.CommunityCategory;
import com.zbkj.common.model.community.CommunityMatchConfig;
import com.zbkj.common.model.community.CommunityMessage;
import com.zbkj.common.model.user.User;
import com.zbkj.common.request.CommunityMessageRequest;
import com.zbkj.common.vo.CommunityMessageVO;
import com.zbkj.common.vo.NearbyUserVO;
@ -181,9 +182,91 @@ public class CommunityServiceImpl implements CommunityService {
// ==================== 用户匹配 ====================
@Override
public List<NearbyUserVO> getNearbyUsers(Double latitude, Double longitude, Integer radius, Integer limit) {
// TODO: 实现基于位置的用户匹配逻辑
// 这里需要根据用户表中的经纬度字段进行距离计算
return new ArrayList<>();
// 获取当前登录用户ID
Integer currentUserId = null;
try {
currentUserId = userService.getUserIdException();
} catch (Exception e) {
// 未登录用户
}
// 从数据库随机获取用户排除当前用户
List<NearbyUserVO> result = new ArrayList<>();
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, true); // 只查询正常状态的用户
if (currentUserId != null) {
wrapper.ne(User::getUid, currentUserId); // 排除当前用户
}
wrapper.last("ORDER BY RAND() LIMIT " + limit); // 随机获取
List<User> users = userService.list(wrapper);
for (User user : users) {
NearbyUserVO vo = new NearbyUserVO();
vo.setId(user.getUid());
vo.setNickname(user.getNickname() != null ? user.getNickname() : "用户" + user.getUid());
vo.setAvatar(user.getAvatar());
vo.setGender(user.getSex() == 1 ? "" : (user.getSex() == 2 ? "" : "保密"));
vo.setAge(0); // 用户表没有年龄字段
// 处理地址优先使用addres字段否则使用country
String location = user.getAddres();
if (location == null || location.isEmpty()) {
location = user.getCountry();
}
if (location == null || location.isEmpty()) {
location = "附近";
}
// 简化地址显示只取城市部分
if (location.contains("-")) {
String[] parts = location.split("-");
location = parts.length > 1 ? parts[1] : parts[0];
} else if (location.contains("·")) {
String[] parts = location.split("·");
location = parts.length > 1 ? parts[1] : parts[0];
}
vo.setLocation(location);
vo.setBio(user.getMark() != null ? user.getMark() : "");
vo.setDistance(Math.random() * radius); // 模拟距离
// 判断在线状态最后登录时间在5分钟内视为在线
boolean isOnline = false;
if (user.getLastLoginTime() != null) {
long diff = System.currentTimeMillis() - user.getLastLoginTime().getTime();
isOnline = diff < 5 * 60 * 1000; // 5分钟
}
vo.setOnline(isOnline);
vo.setLastActiveTime(user.getLastLoginTime() != null ?
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(user.getLastLoginTime()) : null);
result.add(vo);
}
return result;
}
// ==================== 获取可匹配用户总数 ====================
@Override
public Integer getMatchableUserCount() {
// 获取当前登录用户ID
Integer currentUserId = null;
try {
currentUserId = userService.getUserIdException();
} catch (Exception e) {
// 未登录用户
}
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, true); // 只查询正常状态的用户
if (currentUserId != null) {
wrapper.ne(User::getUid, currentUserId); // 排除当前用户
}
return (int) userService.count(wrapper);
}
// ==================== 自动审核 ====================

View File

@ -277,4 +277,28 @@ public class FriendServiceImpl implements FriendService {
return result;
}
@Override
public Map<String, Object> checkFriendStatus(Integer targetUserId) {
Integer currentUserId = userService.getUserId();
Map<String, Object> result = new java.util.HashMap<>();
// 检查是否是好友
Long friendCount = friendDao.checkFriendship(currentUserId, targetUserId);
boolean isFriend = friendCount != null && friendCount > 0;
result.put("isFriend", isFriend);
// 检查是否有待处理的好友请求我发给对方的
Long requestCount = friendRequestDao.checkExistingRequest(currentUserId, targetUserId);
boolean hasPendingRequest = requestCount != null && requestCount > 0;
result.put("hasPendingRequest", hasPendingRequest);
// 检查对方是否发给我好友请求
Long receivedRequestCount = friendRequestDao.checkExistingRequest(targetUserId, currentUserId);
boolean hasReceivedRequest = receivedRequestCount != null && receivedRequestCount > 0;
result.put("hasReceivedRequest", hasReceivedRequest);
return result;
}
}

View File

@ -1711,8 +1711,35 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
@Override
public Boolean editUser(UserEditRequest request) {
User user = getInfo();
user.setAvatar(systemAttachmentService.clearPrefix(request.getAvatar()));
// 更新头像如果提供
if (request.getAvatar() != null && !request.getAvatar().isEmpty()) {
user.setAvatar(systemAttachmentService.clearPrefix(request.getAvatar()));
}
// 更新昵称
user.setNickname(request.getNickname());
// 更新生日如果提供
if (request.getBirthday() != null && !request.getBirthday().isEmpty()) {
user.setBirthday(request.getBirthday());
}
// 更新性别如果提供
if (request.getSex() != null) {
user.setSex(request.getSex());
}
// 更新地址如果提供
if (request.getAddres() != null) {
user.setAddres(request.getAddres());
}
// 更新个人签名/备注如果提供
if (request.getMark() != null) {
user.setMark(request.getMark());
}
user.setUpdateTime(DateUtil.date());
return updateById(user);
}

View File

@ -8,6 +8,7 @@
u.uid as userId,
u.nickname,
u.avatar,
u.mark as signature,
fr.create_time as followTime,
CASE
WHEN TIMESTAMPDIFF(MINUTE, u.last_login_time, NOW()) &lt; 5 THEN 1
@ -37,6 +38,7 @@
u.uid as userId,
u.nickname,
u.avatar,
u.mark as signature,
fr.create_time as followTime,
CASE
WHEN TIMESTAMPDIFF(MINUTE, u.last_login_time, NOW()) &lt; 5 THEN 1

View File

@ -9,6 +9,7 @@
u.nickname as name,
u.avatar as avatarUrl,
u.phone,
u.mark as signature,
u.last_login_time as lastOnlineTime,
CASE WHEN TIMESTAMPDIFF(MINUTE, u.last_login_time, NOW()) &lt; 5 THEN 1 ELSE 0 END as isOnline
FROM eb_friend f

View File

@ -103,6 +103,11 @@
android:name="com.example.livestreaming.HeartbeatSignalActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.DynamicCommunityActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name="com.example.livestreaming.OnlineDatingActivity"
android:exported="false" />

View File

@ -21,6 +21,7 @@ public class AvatarViewerDialog extends Dialog {
private Uri avatarUri;
private Integer avatarResId;
private Drawable avatarDrawable;
private String avatarUrl;
private Context activityContext;
public AvatarViewerDialog(@NonNull Context context) {
@ -36,6 +37,7 @@ public class AvatarViewerDialog extends Dialog {
this.avatarUri = uri;
this.avatarResId = null;
this.avatarDrawable = null;
this.avatarUrl = null;
return this;
}
@ -43,6 +45,7 @@ public class AvatarViewerDialog extends Dialog {
this.avatarResId = resId;
this.avatarUri = null;
this.avatarDrawable = null;
this.avatarUrl = null;
return this;
}
@ -50,6 +53,15 @@ public class AvatarViewerDialog extends Dialog {
this.avatarDrawable = drawable;
this.avatarUri = null;
this.avatarResId = null;
this.avatarUrl = null;
return this;
}
public AvatarViewerDialog setAvatarUrl(String url) {
this.avatarUrl = url;
this.avatarUri = null;
this.avatarResId = null;
this.avatarDrawable = null;
return this;
}
@ -123,7 +135,14 @@ public class AvatarViewerDialog extends Dialog {
}
try {
if (avatarUri != null) {
if (avatarUrl != null && !avatarUrl.isEmpty()) {
Glide.with(context)
.load(avatarUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.into(avatarImageView);
} else if (avatarUri != null) {
Glide.with(context)
.load(avatarUri)
.diskCacheStrategy(DiskCacheStrategy.ALL)
@ -159,4 +178,3 @@ public class AvatarViewerDialog extends Dialog {
}
}
}

View File

@ -0,0 +1,296 @@
package com.example.livestreaming;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 板块消息页面基类
* 从后端API加载对应板块的消息列表
*/
public abstract class BaseCategoryActivity extends AppCompatActivity {
protected ApiService apiService;
protected PostAdapter postAdapter;
protected RecyclerView recyclerView;
protected View emptyView;
protected SwipeRefreshLayout swipeRefresh;
protected View fabPublish;
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
private int resolvedCategoryId = -1; // 从API获取的实际板块ID
/**
* 获取板块ID子类实现如果返回-1则通过名称查找
*/
protected abstract int getCategoryId();
/**
* 获取板块名称子类实现
*/
protected abstract String getCategoryName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
apiService = ApiClient.getService(this);
}
/**
* 初始化消息列表子类在setupUI后调用
*/
protected void initMessageList(RecyclerView recyclerView, View emptyView,
SwipeRefreshLayout swipeRefresh, View fabPublish) {
this.recyclerView = recyclerView;
this.emptyView = emptyView;
this.swipeRefresh = swipeRefresh;
this.fabPublish = fabPublish;
// 设置RecyclerView
postAdapter = new PostAdapter(this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(postAdapter);
// 下拉刷新
if (swipeRefresh != null) {
swipeRefresh.setOnRefreshListener(this::refreshMessages);
}
// 上拉加载更多
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null && !isLoading && hasMore) {
int lastVisible = layoutManager.findLastVisibleItemPosition();
int total = layoutManager.getItemCount();
if (lastVisible >= total - 3) {
loadMoreMessages();
}
}
}
});
// 发布按钮
if (fabPublish != null) {
fabPublish.setOnClickListener(v -> showPublishDialog());
}
// 先获取板块ID再加载数据
resolveCategoryIdAndLoad();
}
/**
* 解析板块ID并加载数据
*/
private void resolveCategoryIdAndLoad() {
int staticId = getCategoryId();
if (staticId > 0) {
resolvedCategoryId = staticId;
loadMessages();
return;
}
// 通过名称查找板块ID
apiService.getCommunityCategories().enqueue(new Callback<ApiResponse<List<CommunityResponse.Category>>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<List<CommunityResponse.Category>>> call,
@NonNull Response<ApiResponse<List<CommunityResponse.Category>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
List<CommunityResponse.Category> categories = response.body().getData();
if (categories != null) {
String targetName = getCategoryName();
for (CommunityResponse.Category cat : categories) {
if (targetName.equals(cat.getName())) {
resolvedCategoryId = cat.getId();
android.util.Log.d("BaseCategoryActivity", "找到板块: " + targetName + " -> ID=" + resolvedCategoryId);
break;
}
}
}
}
loadMessages();
}
@Override
public void onFailure(@NonNull Call<ApiResponse<List<CommunityResponse.Category>>> call, @NonNull Throwable t) {
android.util.Log.e("BaseCategoryActivity", "获取板块列表失败", t);
loadMessages();
}
});
}
/**
* 刷新消息
*/
protected void refreshMessages() {
currentPage = 1;
hasMore = true;
loadMessages();
}
/**
* 加载更多消息
*/
protected void loadMoreMessages() {
if (!isLoading && hasMore) {
currentPage++;
loadMessages();
}
}
/**
* 从API加载消息
*/
protected void loadMessages() {
if (isLoading) return;
isLoading = true;
int categoryId = resolvedCategoryId > 0 ? resolvedCategoryId : getCategoryId();
android.util.Log.d("BaseCategoryActivity", "加载消息: categoryId=" + categoryId + ", page=" + currentPage);
apiService.getCommunityMessages(categoryId, currentPage, 20)
.enqueue(new Callback<ApiResponse<CommunityResponse.MessagePage>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<CommunityResponse.MessagePage>> call,
@NonNull Response<ApiResponse<CommunityResponse.MessagePage>> response) {
isLoading = false;
if (swipeRefresh != null) {
swipeRefresh.setRefreshing(false);
}
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
CommunityResponse.MessagePage data = response.body().getData();
if (data != null && data.list != null) {
List<Post> posts = convertToPosts(data.list);
android.util.Log.d("BaseCategoryActivity", "加载到 " + posts.size() + " 条消息");
if (currentPage == 1) {
postAdapter.setPosts(posts);
} else {
postAdapter.addPosts(posts);
}
hasMore = data.list.size() >= 20;
} else {
if (currentPage == 1) {
postAdapter.setPosts(new ArrayList<>());
}
hasMore = false;
}
} else {
android.util.Log.e("BaseCategoryActivity", "加载消息失败: " + response.code());
if (currentPage == 1) {
postAdapter.setPosts(new ArrayList<>());
}
}
updateEmptyView();
}
@Override
public void onFailure(@NonNull Call<ApiResponse<CommunityResponse.MessagePage>> call,
@NonNull Throwable t) {
isLoading = false;
if (swipeRefresh != null) {
swipeRefresh.setRefreshing(false);
}
android.util.Log.e("BaseCategoryActivity", "网络错误", t);
Toast.makeText(BaseCategoryActivity.this, "加载失败,请检查网络", Toast.LENGTH_SHORT).show();
updateEmptyView();
}
});
}
/**
* 将API返回的Message转换为Post对象
*/
protected List<Post> convertToPosts(List<CommunityResponse.Message> messages) {
List<Post> posts = new ArrayList<>();
for (CommunityResponse.Message msg : messages) {
Post post = new Post();
post.setId(msg.id);
post.setAuthorName(msg.nickname);
post.setAuthorAvatar(msg.avatar);
post.setContent(msg.content);
post.setCategory(getCategoryName());
post.setLikeCount(msg.likeCount);
post.setCommentCount(msg.commentCount);
post.setCreateTime(msg.createTime);
post.setLiked(msg.isLiked);
// 处理图片后端返回逗号分隔的字符串
List<String> imageList = msg.getImageList();
if (imageList != null && !imageList.isEmpty()) {
post.setImagesList(imageList);
}
posts.add(post);
}
return posts;
}
/**
* 更新空视图
*/
protected void updateEmptyView() {
if (emptyView != null && recyclerView != null) {
if (postAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}
}
/**
* 显示发布对话框
*/
protected void showPublishDialog() {
int categoryId = resolvedCategoryId > 0 ? resolvedCategoryId : getCategoryId();
PublishPostHelper.showPublishDialog(this, getCategoryName(), categoryId,
new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
// 刷新列表
refreshMessages();
}
@Override
public void onError(String error) {
Toast.makeText(BaseCategoryActivity.this, error, Toast.LENGTH_SHORT).show();
}
});
}
@Override
protected void onResume() {
super.onResume();
// 每次返回页面时刷新数据
if (postAdapter != null && resolvedCategoryId > 0) {
refreshMessages();
}
}
}

View File

@ -0,0 +1,324 @@
package com.example.livestreaming;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 板块适配器 - 动态加载后端板块数据
* 支持 Element UI 图标映射到 Android drawable
*/
public class CategoryAdapter extends RecyclerView.Adapter<CategoryAdapter.ViewHolder> {
private final Context context;
private final List<CategoryItem> categories = new ArrayList<>();
private OnCategoryClickListener listener;
// Element UI 图标映射到 Android drawable
private static final Map<String, Integer> ICON_MAP = new HashMap<>();
static {
// 默认图标
ICON_MAP.put("default", R.drawable.ic_grid3x3_24);
// ========== Element UI 图标 (管理端使用) ==========
// 用户/社交类 - 粉色用户图标
ICON_MAP.put("el-icon-user", R.drawable.ic_user_24);
ICON_MAP.put("el-icon-user-solid", R.drawable.ic_user_24);
ICON_MAP.put("el-icon-s-custom", R.drawable.ic_user_24);
// 星星/收藏类 - 黄色星星
ICON_MAP.put("el-icon-star-on", R.drawable.ic_star_24);
ICON_MAP.put("el-icon-star-off", R.drawable.ic_star_24);
ICON_MAP.put("el-icon-collection", R.drawable.ic_star_24);
// 手机/移动设备类 - 橙色手机
ICON_MAP.put("el-icon-mobile", R.drawable.ic_smartphone_24);
ICON_MAP.put("el-icon-mobile-phone", R.drawable.ic_smartphone_24);
ICON_MAP.put("el-icon-phone", R.drawable.ic_smartphone_24);
// 耳机/KTV类 - 青色耳机
ICON_MAP.put("el-icon-headset", R.drawable.ic_headset_24);
ICON_MAP.put("el-icon-service", R.drawable.ic_headset_24);
// 麦克风/语音类 - 蓝色麦克风
ICON_MAP.put("el-icon-microphone", R.drawable.ic_voice_24);
ICON_MAP.put("el-icon-mic", R.drawable.ic_voice_24);
// 播放/视频类 - 橙红色播放
ICON_MAP.put("el-icon-video-play", R.drawable.ic_play_circle_24);
ICON_MAP.put("el-icon-caret-right", R.drawable.ic_play_circle_24);
ICON_MAP.put("el-icon-video-camera", R.drawable.ic_play_circle_24);
// 瞄准/射击类 - 橙色瞄准镜
ICON_MAP.put("el-icon-aim", R.drawable.ic_target_24);
ICON_MAP.put("el-icon-position", R.drawable.ic_target_24);
ICON_MAP.put("el-icon-place", R.drawable.ic_target_24);
ICON_MAP.put("el-icon-location", R.drawable.ic_target_24);
// 九宫格/桌游类 - 青绿色网格
ICON_MAP.put("el-icon-grid", R.drawable.ic_grid3x3_24);
ICON_MAP.put("el-icon-menu", R.drawable.ic_grid3x3_24);
ICON_MAP.put("el-icon-s-grid", R.drawable.ic_grid3x3_24);
ICON_MAP.put("el-icon-s-operation", R.drawable.ic_grid3x3_24);
// 游戏手柄类
ICON_MAP.put("el-icon-coordinate", R.drawable.ic_game_24);
ICON_MAP.put("el-icon-trophy", R.drawable.ic_game_24);
// 编辑/绘画类
ICON_MAP.put("el-icon-edit", R.drawable.ic_palette_24);
ICON_MAP.put("el-icon-edit-outline", R.drawable.ic_palette_24);
ICON_MAP.put("el-icon-brush", R.drawable.ic_palette_24);
// 爱心类
ICON_MAP.put("el-icon-chat-dot-round", R.drawable.ic_heart_24);
ICON_MAP.put("el-icon-chat-line-round", R.drawable.ic_heart_24);
// ========== Lucide 图标名称 ==========
ICON_MAP.put("Microphone", R.drawable.ic_voice_24);
ICON_MAP.put("Star", R.drawable.ic_star_24);
ICON_MAP.put("User", R.drawable.ic_user_24);
ICON_MAP.put("Users", R.drawable.ic_user_24);
ICON_MAP.put("Heart", R.drawable.ic_heart_24);
ICON_MAP.put("Gamepad2", R.drawable.ic_game_24);
ICON_MAP.put("Gamepad", R.drawable.ic_game_24);
ICON_MAP.put("Smartphone", R.drawable.ic_smartphone_24);
ICON_MAP.put("Music", R.drawable.ic_headset_24);
ICON_MAP.put("Music2", R.drawable.ic_headset_24);
ICON_MAP.put("Headphones", R.drawable.ic_headset_24);
ICON_MAP.put("Pencil", R.drawable.ic_palette_24);
ICON_MAP.put("Palette", R.drawable.ic_palette_24);
ICON_MAP.put("Target", R.drawable.ic_target_24);
ICON_MAP.put("Crosshair", R.drawable.ic_target_24);
ICON_MAP.put("Dices", R.drawable.ic_grid3x3_24);
ICON_MAP.put("Grid3x3", R.drawable.ic_grid3x3_24);
ICON_MAP.put("Grid", R.drawable.ic_grid3x3_24);
ICON_MAP.put("CirclePlay", R.drawable.ic_play_circle_24);
ICON_MAP.put("Play", R.drawable.ic_play_circle_24);
// ========== 简单名称映射 ==========
ICON_MAP.put("user", R.drawable.ic_user_24);
ICON_MAP.put("star", R.drawable.ic_star_24);
ICON_MAP.put("heart", R.drawable.ic_heart_24);
ICON_MAP.put("smartphone", R.drawable.ic_smartphone_24);
ICON_MAP.put("headset", R.drawable.ic_headset_24);
ICON_MAP.put("play", R.drawable.ic_play_circle_24);
ICON_MAP.put("target", R.drawable.ic_target_24);
ICON_MAP.put("grid", R.drawable.ic_grid3x3_24);
ICON_MAP.put("game", R.drawable.ic_game_24);
ICON_MAP.put("palette", R.drawable.ic_palette_24);
ICON_MAP.put("message", R.drawable.ic_message_24);
ICON_MAP.put("ChatDotRound", R.drawable.ic_message_24);
}
public interface OnCategoryClickListener {
void onCategoryClick(CategoryItem category);
}
public CategoryAdapter(Context context) {
this.context = context;
}
public void setOnCategoryClickListener(OnCategoryClickListener listener) {
this.listener = listener;
}
public void setCategories(List<CategoryItem> newCategories) {
categories.clear();
if (newCategories != null) {
categories.addAll(newCategories);
}
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_category_card, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CategoryItem item = categories.get(position);
holder.bind(item);
}
@Override
public int getItemCount() {
return categories.size();
}
/**
* 根据图标名称获取对应的 drawable 资源
*/
private int getIconResource(String iconName) {
if (iconName == null || iconName.isEmpty()) {
android.util.Log.d("CategoryAdapter", "图标名称为空,使用默认图标");
return ICON_MAP.get("default");
}
// 1. 精确匹配
Integer res = ICON_MAP.get(iconName);
if (res != null) {
return res;
}
// 2. 去除空格后匹配
String trimmed = iconName.trim();
res = ICON_MAP.get(trimmed);
if (res != null) {
return res;
}
// 3. 小写匹配遍历所有key
String lowerName = trimmed.toLowerCase();
for (Map.Entry<String, Integer> entry : ICON_MAP.entrySet()) {
if (entry.getKey().toLowerCase().equals(lowerName)) {
return entry.getValue();
}
}
// 4. 部分匹配图标名称包含关键词
if (lowerName.contains("star") || lowerName.contains("collection")) {
return R.drawable.ic_star_24;
}
if (lowerName.contains("user") || lowerName.contains("custom")) {
return R.drawable.ic_user_24;
}
if (lowerName.contains("heart") || lowerName.contains("chat")) {
return R.drawable.ic_heart_24;
}
if (lowerName.contains("mobile") || lowerName.contains("phone") || lowerName.contains("smartphone")) {
return R.drawable.ic_smartphone_24;
}
if (lowerName.contains("headset") || lowerName.contains("headphone") || lowerName.contains("service") || lowerName.contains("music")) {
return R.drawable.ic_headset_24;
}
if (lowerName.contains("video") || lowerName.contains("play") || lowerName.contains("caret")) {
return R.drawable.ic_play_circle_24;
}
if (lowerName.contains("game") || lowerName.contains("trophy") || lowerName.contains("coordinate")) {
return R.drawable.ic_game_24;
}
if (lowerName.contains("mic") || lowerName.contains("voice")) {
return R.drawable.ic_voice_24;
}
if (lowerName.contains("edit") || lowerName.contains("pencil") || lowerName.contains("brush") || lowerName.contains("palette")) {
return R.drawable.ic_palette_24;
}
if (lowerName.contains("aim") || lowerName.contains("target") || lowerName.contains("crosshair") || lowerName.contains("position") || lowerName.contains("location")) {
return R.drawable.ic_target_24;
}
if (lowerName.contains("grid") || lowerName.contains("dice") || lowerName.contains("menu")) {
return R.drawable.ic_grid3x3_24;
}
if (lowerName.contains("message")) {
return R.drawable.ic_message_24;
}
android.util.Log.w("CategoryAdapter", "未找到图标映射: " + iconName + ",使用默认图标");
return ICON_MAP.get("default");
}
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView iconView;
private final TextView nameView;
private final View cardView;
ViewHolder(@NonNull View itemView) {
super(itemView);
cardView = itemView;
iconView = itemView.findViewById(R.id.categoryIcon);
nameView = itemView.findViewById(R.id.categoryName);
}
void bind(CategoryItem item) {
nameView.setText(item.getName());
iconView.setImageResource(getIconResource(item.getIcon()));
cardView.setOnClickListener(v -> {
if (listener != null) {
listener.onCategoryClick(item);
}
});
}
}
/**
* 板块数据类
*/
public static class CategoryItem {
private int id;
private String name;
private String icon;
private String description;
private String jumpPage; // 跳转页面Activity名称
private String type; // 类型: quick=快捷入口 card=功能卡片
private int sort;
private int status;
public CategoryItem() {}
public CategoryItem(int id, String name, String icon) {
this.id = id;
this.name = name;
this.icon = icon;
}
public CategoryItem(int id, String name, String icon, String jumpPage) {
this.id = id;
this.name = name;
this.icon = icon;
this.jumpPage = jumpPage;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getIcon() { return icon; }
public void setIcon(String icon) { this.icon = icon; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getJumpPage() { return jumpPage; }
public void setJumpPage(String jumpPage) { this.jumpPage = jumpPage; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public int getSort() { return sort; }
public void setSort(int sort) { this.sort = sort; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
/**
* CommunityResponse.Category 转换
*/
public static CategoryItem fromCategory(com.example.livestreaming.net.CommunityResponse.Category cat) {
CategoryItem item = new CategoryItem();
item.setId(cat.getId());
item.setName(cat.getName());
item.setIcon(cat.getIcon());
item.setJumpPage(cat.getJumpPage());
item.setType(cat.getType());
return item;
}
}
}

View File

@ -0,0 +1,163 @@
package com.example.livestreaming;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
/**
* 频道管理适配器
* 用于显示我的频道和推荐频道
*/
public class ChannelManagerAdapter extends RecyclerView.Adapter<ChannelManagerAdapter.ViewHolder> {
public interface OnChannelClickListener {
void onChannelClick(ChannelItem item, int position);
void onChannelDelete(ChannelItem item, int position);
void onChannelAdd(ChannelItem item, int position);
}
private final List<ChannelItem> items = new ArrayList<>();
private OnChannelClickListener listener;
private boolean isEditMode = false;
private boolean isRecommendMode = false; // 是否是推荐频道模式
private int fixedCount = 4; // 前4个固定不能删除
public void setOnChannelClickListener(OnChannelClickListener listener) {
this.listener = listener;
}
public void setEditMode(boolean editMode) {
this.isEditMode = editMode;
notifyDataSetChanged();
}
public void setRecommendMode(boolean recommendMode) {
this.isRecommendMode = recommendMode;
}
public void setFixedCount(int count) {
this.fixedCount = count;
}
public void submitList(List<ChannelItem> newItems) {
items.clear();
if (newItems != null) {
items.addAll(newItems);
}
notifyDataSetChanged();
}
public List<ChannelItem> getItems() {
return new ArrayList<>(items);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_channel_tag, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ChannelItem item = items.get(position);
holder.bind(item, position);
}
@Override
public int getItemCount() {
return items.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private final TextView channelName;
private final ImageView fixedIcon;
private final ImageView deleteIcon;
private final ImageView addIcon;
ViewHolder(@NonNull View itemView) {
super(itemView);
channelName = itemView.findViewById(R.id.channelName);
fixedIcon = itemView.findViewById(R.id.fixedIcon);
deleteIcon = itemView.findViewById(R.id.deleteIcon);
addIcon = itemView.findViewById(R.id.addIcon);
}
void bind(ChannelItem item, int position) {
channelName.setText(item.getName());
// 重置所有图标状态
fixedIcon.setVisibility(View.GONE);
deleteIcon.setVisibility(View.GONE);
addIcon.setVisibility(View.GONE);
if (isRecommendMode) {
// 推荐频道模式显示添加图标
addIcon.setVisibility(View.VISIBLE);
itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onChannelAdd(item, position);
}
});
} else {
// 我的频道模式
boolean isFixed = position < fixedCount;
if (isEditMode) {
// 编辑模式
if (isFixed) {
fixedIcon.setVisibility(View.VISIBLE);
} else {
deleteIcon.setVisibility(View.VISIBLE);
deleteIcon.setOnClickListener(v -> {
if (listener != null) {
listener.onChannelDelete(item, position);
}
});
}
}
itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onChannelClick(item, position);
}
});
}
}
}
/**
* 频道项数据类
*/
public static class ChannelItem {
private final String id;
private final String name;
private boolean isFixed;
public ChannelItem(String id, String name) {
this.id = id;
this.name = name;
this.isFixed = false;
}
public ChannelItem(String id, String name, boolean isFixed) {
this.id = id;
this.name = name;
this.isFixed = isFixed;
}
public String getId() { return id; }
public String getName() { return name; }
public boolean isFixed() { return isFixed; }
public void setFixed(boolean fixed) { this.isFixed = fixed; }
}
}

View File

@ -0,0 +1,153 @@
package com.example.livestreaming;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
/**
* 频道标签适配器发现页面使用
*/
public class ChannelTagAdapter extends ListAdapter<ChannelTagAdapter.ChannelTag, ChannelTagAdapter.ViewHolder> {
public interface OnTagClickListener {
void onTagClick(ChannelTag tag, int position);
void onTagAddClick(ChannelTag tag, int position); // 推荐频道的添加点击
}
private OnTagClickListener listener;
private boolean isRecommendMode = false; // 是否是推荐频道模式显示+
private int selectedPosition = -1;
public ChannelTagAdapter() {
super(DIFF_CALLBACK);
}
public void setOnTagClickListener(OnTagClickListener listener) {
this.listener = listener;
}
public void setRecommendMode(boolean recommendMode) {
this.isRecommendMode = recommendMode;
}
public void setSelectedPosition(int position) {
int oldPosition = this.selectedPosition;
this.selectedPosition = position;
if (oldPosition >= 0) notifyItemChanged(oldPosition);
if (position >= 0) notifyItemChanged(position);
}
private static final DiffUtil.ItemCallback<ChannelTag> DIFF_CALLBACK = new DiffUtil.ItemCallback<ChannelTag>() {
@Override
public boolean areItemsTheSame(@NonNull ChannelTag oldItem, @NonNull ChannelTag newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull ChannelTag oldItem, @NonNull ChannelTag newItem) {
return oldItem.equals(newItem);
}
};
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_channel_tag, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ChannelTag tag = getItem(position);
holder.bind(tag, position);
}
class ViewHolder extends RecyclerView.ViewHolder {
private final TextView tagText;
ViewHolder(@NonNull View itemView) {
super(itemView);
tagText = itemView.findViewById(R.id.channelName);
}
void bind(ChannelTag tag, int position) {
// 显示文本
if (isRecommendMode) {
tagText.setText("+ " + tag.getName());
} else {
tagText.setText(tag.getName());
}
// 选中状态
boolean isSelected = position == selectedPosition;
if (isSelected) {
tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected);
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.black, null));
} else {
tagText.setBackgroundResource(R.drawable.bg_channel_tag);
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null));
}
// 点击事件
itemView.setOnClickListener(v -> {
if (listener != null) {
if (isRecommendMode) {
listener.onTagAddClick(tag, position);
} else {
listener.onTagClick(tag, position);
}
}
});
}
}
/**
* 频道标签数据类
*/
public static class ChannelTag {
private int id;
private String name;
private String icon;
private boolean isMyChannel; // 是否是我的频道
public ChannelTag(int id, String name) {
this.id = id;
this.name = name;
this.isMyChannel = false;
}
public ChannelTag(int id, String name, boolean isMyChannel) {
this.id = id;
this.name = name;
this.isMyChannel = isMyChannel;
}
public int getId() { return id; }
public String getName() { return name; }
public String getIcon() { return icon; }
public void setIcon(String icon) { this.icon = icon; }
public boolean isMyChannel() { return isMyChannel; }
public void setMyChannel(boolean myChannel) { isMyChannel = myChannel; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChannelTag that = (ChannelTag) o;
return id == that.id && isMyChannel == that.isMyChannel
&& java.util.Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return java.util.Objects.hash(id, name, isMyChannel);
}
}
}

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityDrawGuessBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 电影讨论你画我猜 - 从后端API加载消息
*/
public class DrawGuessActivity extends BaseCategoryActivity {
public class DrawGuessActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "电影讨论";
private static final String CATEGORY = "你画我猜";
private ActivityDrawGuessBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, DrawGuessActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, DrawGuessActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,27 +38,12 @@ public class DrawGuessActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNavigation);
@ -82,28 +74,6 @@ public class DrawGuessActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
@ -111,7 +81,6 @@ public class DrawGuessActivity extends AppCompatActivity {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
loadPosts();
}
}
}

View File

@ -0,0 +1,233 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.livestreaming.databinding.ActivityDynamicCommunityBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 动态社区页面 - 根据板块ID动态加载消息列表
*/
public class DynamicCommunityActivity extends AppCompatActivity {
private static final String EXTRA_CATEGORY_ID = "category_id";
private static final String EXTRA_CATEGORY_NAME = "category_name";
private ActivityDynamicCommunityBinding binding;
private ApiService apiService;
private PostAdapter postAdapter;
private int categoryId;
private String categoryName;
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
public static void start(Context context, int categoryId, String categoryName) {
Intent intent = new Intent(context, DynamicCommunityActivity.class);
intent.putExtra(EXTRA_CATEGORY_ID, categoryId);
intent.putExtra(EXTRA_CATEGORY_NAME, categoryName);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDynamicCommunityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
categoryId = getIntent().getIntExtra(EXTRA_CATEGORY_ID, 0);
categoryName = getIntent().getStringExtra(EXTRA_CATEGORY_NAME);
apiService = ApiClient.getService(this);
setupUI();
setupRecyclerView();
setupSwipeRefresh();
setupBottomNav();
loadMessages(true);
}
private void setupUI() {
binding.titleText.setText(categoryName != null ? categoryName : "动态社区");
binding.backButton.setOnClickListener(v -> finish());
// 发布按钮
binding.fabPublish.setOnClickListener(v -> {
// TODO: 打开发布消息页面
Toast.makeText(this, "发布功能开发中", Toast.LENGTH_SHORT).show();
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter(this);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(postAdapter);
postAdapter.setOnItemClickListener(post -> {
// 点击帖子查看详情
Toast.makeText(this, "查看: " + post.getContent(), Toast.LENGTH_SHORT).show();
});
// 加载更多
binding.recyclerView.addOnScrollListener(new androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull androidx.recyclerview.widget.RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
if (!isLoading && hasMore && lastVisibleItem >= totalItemCount - 3) {
loadMessages(false);
}
}
}
});
}
private void setupSwipeRefresh() {
binding.swipeRefresh.setColorSchemeResources(R.color.purple_500, R.color.pink_500);
binding.swipeRefresh.setOnRefreshListener(() -> loadMessages(true));
}
private void setupBottomNav() {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
bottomNav.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
private void loadMessages(boolean refresh) {
if (isLoading) return;
isLoading = true;
if (refresh) {
currentPage = 1;
hasMore = true;
}
apiService.getCommunityMessages(categoryId, currentPage, 20).enqueue(new Callback<ApiResponse<CommunityResponse.MessagePage>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<CommunityResponse.MessagePage>> call,
@NonNull Response<ApiResponse<CommunityResponse.MessagePage>> response) {
isLoading = false;
binding.swipeRefresh.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
CommunityResponse.MessagePage page = response.body().getData();
List<Post> posts = convertToPosts(page.list);
if (refresh) {
postAdapter.setPosts(posts);
} else {
postAdapter.addPosts(posts);
}
hasMore = page.list != null && page.list.size() >= 20;
currentPage++;
updateEmptyState(postAdapter.getItemCount() == 0);
} else {
if (refresh) {
updateEmptyState(true);
}
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<CommunityResponse.MessagePage>> call, @NonNull Throwable t) {
isLoading = false;
binding.swipeRefresh.setRefreshing(false);
Toast.makeText(DynamicCommunityActivity.this, "加载失败", Toast.LENGTH_SHORT).show();
}
});
}
private List<Post> convertToPosts(List<CommunityResponse.Message> messages) {
List<Post> posts = new ArrayList<>();
if (messages == null) return posts;
for (CommunityResponse.Message msg : messages) {
Post post = new Post();
post.setId(msg.id);
post.setContent(msg.content);
post.setAuthorName(msg.nickname);
post.setAuthorAvatar(msg.avatar);
post.setImagesList(msg.getImageList()); // 使用getImageList方法获取图片列表
post.setLikeCount(msg.likeCount);
post.setCommentCount(msg.commentCount);
post.setCreateTime(msg.createTime);
posts.add(post);
}
return posts;
}
private void updateEmptyState(boolean isEmpty) {
if (isEmpty) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerView.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
}
}
}

View File

@ -4,10 +4,13 @@ import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -28,6 +31,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ActivityEditProfileBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.FileUploadResponse;
import com.example.livestreaming.net.UserEditRequest;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import android.widget.NumberPicker;
@ -40,14 +48,24 @@ import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class EditProfileActivity extends AppCompatActivity {
private static final String TAG = "EditProfileActivity";
private ActivityEditProfileBinding binding;
private ApiService apiService;
private static final String PREFS_NAME = "profile_prefs";
private static final String KEY_NAME = "profile_name";
private static final String KEY_BIO = "profile_bio";
private static final String KEY_AVATAR_URI = "profile_avatar_uri";
private static final String KEY_AVATAR_URL = "profile_avatar_url"; // 服务器返回的URL
private static final String KEY_AVATAR_RES = "profile_avatar_res";
private static final String KEY_BIRTHDAY = "profile_birthday";
private static final String KEY_GENDER = "profile_gender";
@ -57,6 +75,8 @@ public class EditProfileActivity extends AppCompatActivity {
private Uri selectedAvatarUri = null;
private Uri pendingCameraUri = null;
private String uploadedAvatarUrl = null; // 上传后的服务器URL
private boolean isUploading = false;
private ActivityResultLauncher<String> pickImageLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
@ -80,9 +100,12 @@ public class EditProfileActivity extends AppCompatActivity {
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
apiService = ApiClient.getService(this);
pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
if (uri == null) return;
selectedAvatarUri = uri;
uploadedAvatarUrl = null; // 重置上传URL
Glide.with(this).load(uri).circleCrop().into(binding.avatarPreview);
});
@ -90,6 +113,7 @@ public class EditProfileActivity extends AppCompatActivity {
if (!success) return;
if (pendingCameraUri == null) return;
selectedAvatarUri = pendingCameraUri;
uploadedAvatarUrl = null; // 重置上传URL
Glide.with(this).load(pendingCameraUri).circleCrop().into(binding.avatarPreview);
});
@ -142,95 +166,266 @@ public class EditProfileActivity extends AppCompatActivity {
dialog.show();
});
binding.saveButton.setOnClickListener(v -> {
// TODO: 接入后端接口 - 上传头像
// 接口路径: POST /api/users/{userId}/avatar
// 请求参数:
// - userId: 用户ID从token中获取
// - avatar: 头像文件multipart/form-data
// 返回数据格式: ApiResponse<{avatarUrl: string}>
// 上传成功后保存avatarUrl到本地并更新界面显示
// TODO: 接入后端接口 - 更新用户资料
// 接口路径: PUT /api/users/{userId}/profile
// 请求参数:
// - userId: 用户ID从token中获取
// - name: 昵称
// - bio: 个人签名
// - birthday: 生日格式yyyy-MM-dd
// - gender: 性别//保密
// - location: 所在地格式省份-城市
// 返回数据格式: ApiResponse<UserProfile>
// 更新成功后同步更新本地缓存和界面显示
String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : "";
String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : "";
String gender = binding.inputGender.getText() != null ? binding.inputGender.getText().toString().trim() : "";
String location = binding.inputLocation.getText() != null ? binding.inputLocation.getText().toString().trim() : "";
binding.saveButton.setOnClickListener(v -> saveProfile());
}
if (TextUtils.isEmpty(name)) {
Toast.makeText(this, "昵称不能为空", Toast.LENGTH_SHORT).show();
/**
* 保存用户资料
*/
private void saveProfile() {
if (isUploading) {
Toast.makeText(this, "正在保存中,请稍候...", Toast.LENGTH_SHORT).show();
return;
}
String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : "";
String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : "";
String gender = binding.inputGender.getText() != null ? binding.inputGender.getText().toString().trim() : "";
String location = binding.inputLocation.getText() != null ? binding.inputLocation.getText().toString().trim() : "";
if (TextUtils.isEmpty(name)) {
Toast.makeText(this, "昵称不能为空", Toast.LENGTH_SHORT).show();
return;
}
// 如果选择了新头像先上传头像
if (selectedAvatarUri != null && uploadedAvatarUrl == null) {
isUploading = true;
binding.saveButton.setEnabled(false);
Toast.makeText(this, "正在上传头像...", Toast.LENGTH_SHORT).show();
uploadAvatar(selectedAvatarUri, avatarUrl -> {
uploadedAvatarUrl = avatarUrl;
// 头像上传成功后更新用户资料
updateUserProfile(name, bio, birthday, gender, location, avatarUrl);
}, error -> {
isUploading = false;
binding.saveButton.setEnabled(true);
Toast.makeText(this, "头像上传失败: " + error, Toast.LENGTH_SHORT).show();
});
} else {
// 没有新头像直接更新资料
String existingAvatarUrl = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URL, null);
updateUserProfile(name, bio, birthday, gender, location, uploadedAvatarUrl != null ? uploadedAvatarUrl : existingAvatarUrl);
}
}
/**
* 上传头像到服务器
*/
private void uploadAvatar(Uri imageUri, OnAvatarUploadSuccess onSuccess, OnAvatarUploadError onError) {
try {
// 获取文件名
String fileName = getFileName(imageUri);
if (fileName == null) {
fileName = "avatar_" + System.currentTimeMillis() + ".jpg";
}
// 读取文件内容
InputStream inputStream = getContentResolver().openInputStream(imageUri);
if (inputStream == null) {
onError.onError("无法读取图片文件");
return;
}
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit()
.putString(KEY_NAME, name)
.apply();
if (TextUtils.isEmpty(bio) || BIO_HINT_TEXT.equals(bio)) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIO).apply();
} else {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIO, bio).apply();
// 读取所有字节
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
byte[] data = new byte[8192];
int nRead;
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
byte[] fileBytes = buffer.toByteArray();
inputStream.close();
if (selectedAvatarUri != null) {
Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri);
if (persisted != null) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit()
.putString(KEY_AVATAR_URI, persisted.toString())
.remove(KEY_AVATAR_RES)
.apply();
// 创建请求体
RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), fileBytes);
MultipartBody.Part body = MultipartBody.Part.createFormData("file", fileName, requestFile);
RequestBody model = RequestBody.create(MediaType.parse("text/plain"), "user");
RequestBody pid = RequestBody.create(MediaType.parse("text/plain"), "7"); // 7表示前台用户
// 调用上传接口
apiService.uploadImage(body, model, pid).enqueue(new Callback<ApiResponse<FileUploadResponse>>() {
@Override
public void onResponse(Call<ApiResponse<FileUploadResponse>> call, Response<ApiResponse<FileUploadResponse>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
FileUploadResponse data = response.body().getData();
if (data != null && data.getUrl() != null) {
Log.d(TAG, "头像上传成功: " + data.getUrl());
onSuccess.onSuccess(data.getUrl());
} else {
onError.onError("服务器返回数据异常");
}
} else {
String msg = response.body() != null ? response.body().getMessage() : "上传失败";
onError.onError(msg);
}
}
@Override
public void onFailure(Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
Log.e(TAG, "头像上传网络错误: " + t.getMessage());
onError.onError("网络错误: " + t.getMessage());
}
});
} catch (Exception e) {
Log.e(TAG, "头像上传异常: " + e.getMessage());
onError.onError("上传异常: " + e.getMessage());
}
}
/**
* 更新用户资料到服务器
*/
private void updateUserProfile(String name, String bio, String birthday, String gender, String location, String avatarUrl) {
UserEditRequest request = new UserEditRequest();
request.setNickname(name);
if (avatarUrl != null) {
request.setAvatar(avatarUrl);
}
if (!TextUtils.isEmpty(birthday)) {
request.setBirthday(birthday);
}
if (!TextUtils.isEmpty(gender)) {
// 转换性别=1, =2, 保密=3
int sex = 0;
if ("".equals(gender)) sex = 1;
else if ("".equals(gender)) sex = 2;
else if ("保密".equals(gender)) sex = 3;
request.setSex(sex);
}
if (!TextUtils.isEmpty(location)) {
request.setAddres(location);
}
if (!TextUtils.isEmpty(bio) && !BIO_HINT_TEXT.equals(bio)) {
request.setMark(bio);
}
apiService.updateUserInfo(request).enqueue(new Callback<ApiResponse<Object>>() {
@Override
public void onResponse(Call<ApiResponse<Object>> call, Response<ApiResponse<Object>> response) {
isUploading = false;
binding.saveButton.setEnabled(true);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Log.d(TAG, "用户资料更新成功");
// 保存到本地
saveToLocalPrefs(name, bio, birthday, gender, location, avatarUrl);
Toast.makeText(EditProfileActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
} else {
String msg = response.body() != null ? response.body().getMessage() : "更新失败";
Log.e(TAG, "用户资料更新失败: " + msg);
// 即使服务器更新失败也保存到本地
saveToLocalPrefs(name, bio, birthday, gender, location, avatarUrl);
Toast.makeText(EditProfileActivity.this, "已保存到本地,服务器同步失败: " + msg, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
}
// 生日已经在日期选择器中实时保存这里只需要确保同步
// 如果输入框为空则清除
if (TextUtils.isEmpty(birthday)) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIRTHDAY).apply();
} else {
// 确保使用输入框中的最新值
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIRTHDAY, birthday).apply();
@Override
public void onFailure(Call<ApiResponse<Object>> call, Throwable t) {
isUploading = false;
binding.saveButton.setEnabled(true);
Log.e(TAG, "用户资料更新网络错误: " + t.getMessage());
// 网络失败也保存到本地
saveToLocalPrefs(name, bio, birthday, gender, location, avatarUrl);
Toast.makeText(EditProfileActivity.this, "已保存到本地,网络同步失败", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
if (TextUtils.isEmpty(gender)) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_GENDER).apply();
} else {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_GENDER, gender).apply();
}
if (TextUtils.isEmpty(location)) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_LOCATION).apply();
} else {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_LOCATION, location).apply();
}
// 设置结果通知ProfileActivity需要刷新
setResult(RESULT_OK);
finish();
});
}
/**
* 保存到本地SharedPreferences
*/
private void saveToLocalPrefs(String name, String bio, String birthday, String gender, String location, String avatarUrl) {
android.content.SharedPreferences.Editor editor = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit();
editor.putString(KEY_NAME, name);
if (TextUtils.isEmpty(bio) || BIO_HINT_TEXT.equals(bio)) {
editor.remove(KEY_BIO);
} else {
editor.putString(KEY_BIO, bio);
}
if (TextUtils.isEmpty(birthday)) {
editor.remove(KEY_BIRTHDAY);
} else {
editor.putString(KEY_BIRTHDAY, birthday);
}
if (TextUtils.isEmpty(gender)) {
editor.remove(KEY_GENDER);
} else {
editor.putString(KEY_GENDER, gender);
}
if (TextUtils.isEmpty(location)) {
editor.remove(KEY_LOCATION);
} else {
editor.putString(KEY_LOCATION, location);
}
if (avatarUrl != null) {
editor.putString(KEY_AVATAR_URL, avatarUrl);
}
// 如果选择了新头像也保存本地URI
if (selectedAvatarUri != null) {
Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri);
if (persisted != null) {
editor.putString(KEY_AVATAR_URI, persisted.toString());
editor.remove(KEY_AVATAR_RES);
}
}
editor.apply();
}
/**
* 获取文件名
*/
private String getFileName(Uri uri) {
String result = null;
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index >= 0) {
result = cursor.getString(index);
}
}
}
}
if (result == null) {
result = uri.getPath();
if (result != null) {
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
}
return result;
}
// 回调接口
private interface OnAvatarUploadSuccess {
void onSuccess(String avatarUrl);
}
private interface OnAvatarUploadError {
void onError(String error);
}
private Uri persistAvatarToInternalStorage(Uri sourceUri) {
// TODO: 接入后端接口 - 上传头像到服务器
// 接口路径: POST /api/upload/image
// 请求参数:
// - file: 图片文件multipart/form-data
// - model: 模块类型"user"
// - pid: 分类ID如7表示前台用户
// 返回数据格式: ApiResponse<{url: string}>
// 上传成功后保存返回的URL到本地并更新界面显示
// 注意这里目前只是保存到本地实际应该先上传到服务器然后保存服务器返回的URL
if (sourceUri == null) return null;
InputStream in = null;
OutputStream out = null;

View File

@ -45,7 +45,9 @@ public class FansListActivity extends AppCompatActivity {
adapter = new FriendsAdapter(item -> {
if (item == null) return;
Toast.makeText(this, "打开粉丝:" + item.getName(), Toast.LENGTH_SHORT).show();
// 跳转到用户主页传递签名信息
UserProfileReadOnlyActivity.start(this, item.getId(), item.getName(),
"", item.getSubtitle(), item.getAvatarUrl());
});
binding.fansRecyclerView.setLayoutManager(new LinearLayoutManager(this));
@ -80,13 +82,22 @@ public class FansListActivity extends AppCompatActivity {
String id = String.valueOf(user.get("userId"));
String name = (String) user.get("nickname");
String phone = (String) user.get("phone");
String signature = (String) user.get("signature");
String avatar = (String) user.get("avatar");
Boolean isOnline = (Boolean) user.get("isOnline");
Boolean isMutualFollow = (Boolean) user.get("isMutualFollow");
String status = isMutualFollow != null && isMutualFollow ?
"互相关注" : (isOnline != null && isOnline ? "在线" : "离线");
items.add(new FriendItem(id, name != null ? name : phone, status,
isOnline != null && isOnline));
// 优先显示签名没有签名则显示互关/在线状态
String subtitle;
if (signature != null && !signature.isEmpty()) {
subtitle = signature;
} else if (isMutualFollow != null && isMutualFollow) {
subtitle = "互相关注";
} else {
subtitle = isOnline != null && isOnline ? "在线" : "离线";
}
items.add(new FriendItem(id, name != null ? name : phone, subtitle,
isOnline != null && isOnline, avatar));
}
adapter.submitList(items);
} else {

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityFindGameBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 找人玩游戏 - 从后端API加载消息
*/
public class FindGameActivity extends BaseCategoryActivity {
public class FindGameActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "找人玩游戏";
private static final String CATEGORY = "找人玩游戏";
private ActivityFindGameBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, FindGameActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, FindGameActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,27 +38,13 @@ public class FindGameActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
// 初始化消息列表从API加载
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNavigation);
@ -82,28 +75,6 @@ public class FindGameActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
@ -111,7 +82,6 @@ public class FindGameActivity extends AppCompatActivity {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
loadPosts();
}
}
}

View File

@ -7,21 +7,50 @@ import android.animation.ValueAnimator;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.recyclerview.widget.GridLayoutManager;
import com.example.livestreaming.databinding.ActivityFishPondBinding;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import com.example.livestreaming.net.ApiClient;
import com.bumptech.glide.Glide;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class FishPondActivity extends AppCompatActivity {
private static final String TAG = "FishPondActivity";
private static final int MAX_VISIBLE_CATEGORIES = 3; // 默认显示的板块数量
private ActivityFishPondBinding binding;
private ApiService apiService;
private CategoryAdapter categoryAdapter;
// 保存所有板块数据用于弹窗展示
private List<CategoryAdapter.CategoryItem> allCategories = new ArrayList<>();
// 板块收起/展开状态
private boolean isCategoryExpanded = true;
private View categoryContainer;
private View toggleCategoryBtn;
private android.widget.TextView toggleCategoryText;
private android.widget.ImageView toggleCategoryIcon;
private static class OrbitUserData {
final String id;
@ -29,6 +58,7 @@ public class FishPondActivity extends AppCompatActivity {
final String location;
final String bio;
final int avatarRes;
final String avatarUrl;
OrbitUserData(String id, String name, String location, String bio, int avatarRes) {
this.id = id;
@ -36,6 +66,16 @@ public class FishPondActivity extends AppCompatActivity {
this.location = location;
this.bio = bio;
this.avatarRes = avatarRes;
this.avatarUrl = null;
}
OrbitUserData(String id, String name, String location, String bio, String avatarUrl) {
this.id = id;
this.name = name;
this.location = location;
this.bio = bio;
this.avatarRes = R.drawable.wish_tree_checker_backup;
this.avatarUrl = avatarUrl;
}
}
@ -70,10 +110,14 @@ public class FishPondActivity extends AppCompatActivity {
binding = ActivityFishPondBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
apiService = ApiClient.getService(this);
setupOrbitUserInteractions();
setupCenterRefresh();
setupQuickActions();
setupCategoryToggle();
loadCurrentUserInfo();
loadMatchableUserCount(); // 加载可匹配用户总数
binding.orbitContainer.post(this::updateOrbitLayout);
refreshUsers();
@ -130,48 +174,306 @@ public class FishPondActivity extends AppCompatActivity {
private void setupQuickActions() {
if (binding == null) return;
// 语音匹配按钮
binding.actionLeft.setOnClickListener(v -> {
VoiceMatchActivity.start(this);
// 设置RecyclerView和适配器
setupCategoryRecyclerView();
// 从API加载板块
loadCategoriesFromApi();
}
/**
* 设置板块RecyclerView
*/
private void setupCategoryRecyclerView() {
categoryAdapter = new CategoryAdapter(this);
categoryAdapter.setOnCategoryClickListener(category -> {
onCategoryClick(category);
});
// 心动信号按钮
binding.actionRight.setOnClickListener(v -> {
HeartbeatSignalActivity.start(this);
});
// 使用GridLayoutManager4列3个板块 + 1个展开按钮
GridLayoutManager layoutManager = new GridLayoutManager(this, 4);
binding.categoryRecyclerView.setLayoutManager(layoutManager);
binding.categoryRecyclerView.setAdapter(categoryAdapter);
binding.categoryRecyclerView.setNestedScrollingEnabled(false);
}
// 获取GridLayout中的卡片并设置点击事件
android.widget.GridLayout grid = binding.grid;
if (grid != null) {
int childCount = grid.getChildCount();
for (int i = 0; i < childCount && i < 6; i++) {
View card = grid.getChildAt(i);
if (card != null) {
final int index = i;
card.setOnClickListener(v -> {
switch (index) {
case 0: // 在线处对象
OnlineDatingActivity.start(this);
break;
case 1: // 找人玩游戏
FindGameActivity.start(this);
break;
case 2: // 一起KTV
KTVTogetherActivity.start(this);
break;
case 3: // 你画我猜
DrawGuessActivity.start(this);
break;
case 4: // 和平精英
PeaceEliteActivity.start(this);
break;
case 5: // 桌子游
TableGamesActivity.start(this);
break;
/**
* 从API加载板块列表
*/
private void loadCategoriesFromApi() {
if (binding == null) return;
// 显示加载中
binding.categoryLoading.setVisibility(View.VISIBLE);
binding.categoryRecyclerView.setVisibility(View.GONE);
apiService.getCommunityCategories().enqueue(new Callback<ApiResponse<List<CommunityResponse.Category>>>() {
@Override
public void onResponse(Call<ApiResponse<List<CommunityResponse.Category>>> call,
Response<ApiResponse<List<CommunityResponse.Category>>> response) {
if (binding == null) return;
binding.categoryLoading.setVisibility(View.GONE);
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
List<CommunityResponse.Category> categories = response.body().getData();
Log.d(TAG, "加载到 " + categories.size() + " 个板块");
// 转换为CategoryItem列表
allCategories.clear();
for (CommunityResponse.Category cat : categories) {
// 只显示状态正常的板块
if (cat.status == 1 || cat.status == 0) {
allCategories.add(CategoryAdapter.CategoryItem.fromCategory(cat));
Log.d(TAG, "板块: " + cat.getName() + ", 图标: " + cat.getIcon() + ", 跳转: " + cat.getJumpPage());
}
});
}
if (!allCategories.isEmpty()) {
binding.categoryRecyclerView.setVisibility(View.VISIBLE);
// 只显示前3个板块
showLimitedCategories();
} else {
// 无数据使用默认板块
Log.w(TAG, "API返回空板块列表使用默认板块");
loadDefaultCategories();
}
} else {
Log.w(TAG, "API响应异常使用默认板块");
loadDefaultCategories();
}
}
@Override
public void onFailure(Call<ApiResponse<List<CommunityResponse.Category>>> call, Throwable t) {
if (binding == null) return;
binding.categoryLoading.setVisibility(View.GONE);
Log.e(TAG, "加载板块失败: " + t.getMessage());
loadDefaultCategories();
}
});
}
/**
* 显示有限数量的板块前3个 + 展开更多按钮
*/
private void showLimitedCategories() {
List<CategoryAdapter.CategoryItem> visibleItems = new ArrayList<>();
// 添加前3个板块
for (int i = 0; i < Math.min(MAX_VISIBLE_CATEGORIES, allCategories.size()); i++) {
visibleItems.add(allCategories.get(i));
}
// 如果有更多板块添加"展开更多"按钮
if (allCategories.size() > MAX_VISIBLE_CATEGORIES) {
CategoryAdapter.CategoryItem moreItem = new CategoryAdapter.CategoryItem(
-999, "展开更多", "el-icon-menu", "ShowAllCategories"
);
visibleItems.add(moreItem);
}
categoryAdapter.setCategories(visibleItems);
}
/**
* 加载默认板块API失败时的备用
*/
private void loadDefaultCategories() {
if (binding == null) return;
allCategories.clear();
allCategories.add(new CategoryAdapter.CategoryItem(1, "在线处对象", "el-icon-user", "OnlineDatingActivity"));
allCategories.add(new CategoryAdapter.CategoryItem(2, "找人玩游戏", "el-icon-coordinate", "FindGameActivity"));
allCategories.add(new CategoryAdapter.CategoryItem(3, "一起KTV", "el-icon-headset", "KTVTogetherActivity"));
allCategories.add(new CategoryAdapter.CategoryItem(4, "你画我猜", "el-icon-edit", "DrawGuessActivity"));
allCategories.add(new CategoryAdapter.CategoryItem(5, "和平精英", "el-icon-aim", "PeaceEliteActivity"));
allCategories.add(new CategoryAdapter.CategoryItem(6, "桌子游戏", "el-icon-grid", "TableGamesActivity"));
binding.categoryRecyclerView.setVisibility(View.VISIBLE);
showLimitedCategories();
}
/**
* 板块点击处理 - 根据jumpPage跳转到对应Activity
*/
private void onCategoryClick(CategoryAdapter.CategoryItem category) {
String jumpPage = category.getJumpPage();
String name = category.getName();
int categoryId = category.getId();
Log.d(TAG, "点击板块: " + name + ", jumpPage: " + jumpPage + ", id: " + categoryId);
// 检查是否是"展开更多"按钮
if ("ShowAllCategories".equals(jumpPage) || categoryId == -999) {
showAllCategoriesDialog();
return;
}
// 跳转到板块详情页传递板块名称
navigateToCategoryPage(categoryId, name, jumpPage);
}
/**
* 显示所有板块的弹窗
*/
private void showAllCategoriesDialog() {
if (allCategories.isEmpty()) return;
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(this);
// 创建弹窗布局
View dialogView = getLayoutInflater().inflate(R.layout.dialog_all_categories, null);
androidx.recyclerview.widget.RecyclerView dialogRecyclerView = dialogView.findViewById(R.id.dialogCategoryRecyclerView);
View closeBtn = dialogView.findViewById(R.id.dialogCloseBtn);
// 设置RecyclerView
CategoryAdapter dialogAdapter = new CategoryAdapter(this);
dialogRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
dialogRecyclerView.setAdapter(dialogAdapter);
// 设置所有板块数据不包含"展开更多"
dialogAdapter.setCategories(allCategories);
builder.setView(dialogView);
android.app.AlertDialog dialog = builder.create();
// 设置透明背景
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
}
// 板块点击事件
dialogAdapter.setOnCategoryClickListener(category -> {
dialog.dismiss();
navigateToCategoryPage(category.getId(), category.getName(), category.getJumpPage());
});
// 关闭按钮
if (closeBtn != null) {
closeBtn.setOnClickListener(v -> dialog.dismiss());
}
dialog.show();
}
/**
* 跳转到板块详情页
*/
private void navigateToCategoryPage(int categoryId, String categoryName, String jumpPage) {
// 根据jumpPage或板块名称跳转
if (jumpPage != null && !jumpPage.isEmpty()) {
navigateByJumpPage(jumpPage, categoryId, categoryName);
} else {
// 没有jumpPage根据名称匹配
navigateByName(categoryName, categoryId);
}
}
/**
* 根据jumpPage跳转 - 统一使用DynamicCommunityActivity传递板块名称
*/
private void navigateByJumpPage(String jumpPage, int categoryId, String categoryName) {
// 所有板块统一使用DynamicCommunityActivity确保标题显示正确的板块名称
navigateToGenericCategory(categoryId, categoryName);
}
/**
* 根据板块名称跳转 - 统一使用DynamicCommunityActivity
*/
private void navigateByName(String name, int categoryId) {
if (name == null) {
navigateToGenericCategory(categoryId, "未知板块");
return;
}
// 统一使用通用跳转确保标题显示正确的板块名称
navigateToGenericCategory(categoryId, name);
}
/**
* 通用板块跳转使用DynamicCommunityActivity
*/
private void navigateToGenericCategory(int categoryId, String categoryName) {
try {
Intent intent = new Intent(this, DynamicCommunityActivity.class);
intent.putExtra("category_id", categoryId);
intent.putExtra("category_name", categoryName);
startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "跳转失败: " + e.getMessage());
Toast.makeText(this, "板块 \"" + categoryName + "\" 暂未开放", Toast.LENGTH_SHORT).show();
}
}
/**
* 设置板块隐藏/展开功能
*/
private void setupCategoryToggle() {
// 使用getResources().getIdentifier动态获取ID
int containerId = getResources().getIdentifier("categoryContainer", "id", getPackageName());
int btnId = getResources().getIdentifier("toggleCategoryBtn", "id", getPackageName());
int textId = getResources().getIdentifier("toggleCategoryText", "id", getPackageName());
int iconId = getResources().getIdentifier("toggleCategoryIcon", "id", getPackageName());
categoryContainer = findViewById(containerId);
toggleCategoryBtn = findViewById(btnId);
toggleCategoryText = findViewById(textId);
toggleCategoryIcon = findViewById(iconId);
// 如果布局中没有这些视图使用RecyclerView作为categoryContainer
if (categoryContainer == null && binding != null && binding.categoryRecyclerView != null) {
categoryContainer = binding.categoryRecyclerView;
}
if (toggleCategoryBtn != null) {
toggleCategoryBtn.setOnClickListener(v -> {
v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
toggleCategoryVisibility();
});
}
}
/**
* 切换板块显示/隐藏
*/
private void toggleCategoryVisibility() {
if (categoryContainer == null) return;
isCategoryExpanded = !isCategoryExpanded;
if (isCategoryExpanded) {
// 展开动画
categoryContainer.setVisibility(View.VISIBLE);
categoryContainer.setAlpha(0f);
categoryContainer.animate()
.alpha(1f)
.setDuration(200)
.start();
if (toggleCategoryText != null) {
toggleCategoryText.setText("收起");
}
if (toggleCategoryIcon != null) {
toggleCategoryIcon.animate()
.rotation(0f)
.setDuration(200)
.start();
}
} else {
// 收起动画
categoryContainer.animate()
.alpha(0f)
.setDuration(200)
.withEndAction(() -> categoryContainer.setVisibility(View.GONE))
.start();
if (toggleCategoryText != null) {
toggleCategoryText.setText("展开");
}
if (toggleCategoryIcon != null) {
toggleCategoryIcon.animate()
.rotation(180f)
.setDuration(200)
.start();
}
}
}
@ -181,14 +483,17 @@ public class FishPondActivity extends AppCompatActivity {
int size = binding.orbitContainer.getWidth();
if (size <= 0) return;
int avatarSize = binding.orbitUserTopAvatar.getWidth();
if (avatarSize <= 0 && binding.orbitUserTopAvatar.getLayoutParams() != null) {
avatarSize = binding.orbitUserTopAvatar.getLayoutParams().width;
}
if (avatarSize <= 0) return;
float density = getResources().getDisplayMetrics().density;
int outerRadius = size / 2;
orbitRadiusPx = outerRadius - (avatarSize / 2);
// 圆环有 36dp margin所以圆环半径 = (容器宽度 - 72dp) / 2
int ringMargin = (int) (36 * density);
int ringRadius = (size / 2) - ringMargin;
// 轨道半径 = 圆环半径
// 这样 LinearLayout 的中心会落在圆环上
// 由于头像在 LinearLayout 顶部头像中心会稍微在圆环内侧
// 但视觉效果是可接受的
orbitRadiusPx = ringRadius;
applyOrbitAngles();
}
@ -322,66 +627,265 @@ public class FishPondActivity extends AppCompatActivity {
binding.rightInfo.setText(genderText + " | " + locationText + " | 在线");
}
private void refreshUsers() {
// TODO: 接入后端接口 - 获取附近用户缘池功能
// 接口路径: GET /api/users/nearby
// 请求参数:
// - latitude: 当前用户纬度必填
// - longitude: 当前用户经度必填
// - radius (可选): 搜索半径单位默认5000
// - limit (可选): 返回数量默认6
// 返回数据格式: ApiResponse<List<User>>
// User对象应包含: id, name, avatarUrl, location, bio, distance距离单位, isLive等字段
// 返回距离最近的N个用户用于显示在轨道上
/**
* 加载可匹配用户总数
*/
private void loadMatchableUserCount() {
if (binding == null) return;
int[] avatars = new int[] {
R.drawable.wish_tree_checker_backup,
R.drawable.wish_tree_prev_no_bg,
R.drawable.wish_tree_trim_backup,
R.drawable.wish_tree_black,
R.drawable.wish_tree
};
apiService.getMatchableUserCount().enqueue(new Callback<ApiResponse<Integer>>() {
@Override
public void onResponse(Call<ApiResponse<Integer>> call,
Response<ApiResponse<Integer>> response) {
if (binding == null) return;
String[] names = new String[] { "小树", "阿宁", "Lina", "暖暖", "小七", "小北", "Mika", "安安", "小鹿" };
String[] cities = new String[] { "杭州", "南宁", "深圳", "成都", "武汉", "西安", "上海", "北京", "重庆" };
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
int count = response.body().getData();
binding.leftCount.setText(count + "");
Log.d(TAG, "可匹配用户总数: " + count);
} else {
Log.w(TAG, "获取用户总数失败,使用默认值");
binding.leftCount.setText("-- 人");
}
}
userTop = bindUser("u_top", binding.orbitUserTopAvatar, binding.orbitUserTopName, binding.orbitUserTopLocation, avatars, names, cities);
userRight = bindUser("u_right", binding.orbitUserRightAvatar, binding.orbitUserRightName, binding.orbitUserRightLocation, avatars, names, cities);
userBottomRight = bindUser("u_bottom_right", binding.orbitUserBottomRightAvatar, binding.orbitUserBottomRightName, binding.orbitUserBottomRightLocation, avatars, names, cities);
userBottomCenter = bindUser("u_bottom_center", binding.orbitUserBottomCenterAvatar, binding.orbitUserBottomCenterName, binding.orbitUserBottomCenterLocation, avatars, names, cities);
userBottomLeft = bindUser("u_bottom_left", binding.orbitUserBottomLeftAvatar, binding.orbitUserBottomLeftName, binding.orbitUserBottomLeftLocation, avatars, names, cities);
userLeft = bindUser("u_left", binding.orbitUserLeftAvatar, binding.orbitUserLeftName, binding.orbitUserLeftLocation, avatars, names, cities);
@Override
public void onFailure(Call<ApiResponse<Integer>> call, Throwable t) {
if (binding == null) return;
Log.e(TAG, "获取用户总数网络错误: " + t.getMessage());
binding.leftCount.setText("-- 人");
}
});
}
private OrbitUserData bindUser(String id,
android.widget.ImageView avatarView,
android.widget.TextView nameView,
android.widget.TextView locationView,
int[] avatars,
String[] names,
String[] cities) {
if (avatarView == null || nameView == null || locationView == null) {
return new OrbitUserData(id, "", "", "", R.drawable.wish_tree_checker_backup);
private void refreshUsers() {
if (binding == null) return;
// 刷新用户总数
loadMatchableUserCount();
// 尝试从API获取附近用户
loadNearbyUsersFromApi();
}
private void loadNearbyUsersFromApi() {
// 获取当前位置这里使用默认位置实际应该获取GPS位置
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
double latitude = prefs.getFloat("latitude", 22.82f); // 默认南宁纬度
double longitude = prefs.getFloat("longitude", 108.32f); // 默认南宁经度
int radius = 5000; // 5公里范围
int limit = 6; // 获取6个用户
apiService.getNearbyUsers(latitude, longitude, radius, limit)
.enqueue(new Callback<ApiResponse<CommunityResponse.NearbyUserList>>() {
@Override
public void onResponse(Call<ApiResponse<CommunityResponse.NearbyUserList>> call,
Response<ApiResponse<CommunityResponse.NearbyUserList>> response) {
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
List<CommunityResponse.NearbyUser> users = response.body().getData().getUsers();
if (users != null && !users.isEmpty()) {
bindUsersFromApi(users);
return;
}
}
// API失败或无数据使用模拟数据
Log.w(TAG, "API返回空数据使用模拟数据");
loadMockUsers();
}
@Override
public void onFailure(Call<ApiResponse<CommunityResponse.NearbyUserList>> call, Throwable t) {
Log.e(TAG, "获取附近用户失败: " + t.getMessage());
// 网络失败使用模拟数据
loadMockUsers();
}
});
}
private void bindUsersFromApi(List<CommunityResponse.NearbyUser> users) {
if (binding == null || users == null) return;
// 绑定最多6个用户到轨道位置
if (users.size() > 0) {
CommunityResponse.NearbyUser u = users.get(0);
userTop = bindUserFromApi("u_top", binding.orbitUserTopAvatar, binding.orbitUserTopName, binding.orbitUserTopLocation, u);
}
if (users.size() > 1) {
CommunityResponse.NearbyUser u = users.get(1);
userRight = bindUserFromApi("u_right", binding.orbitUserRightAvatar, binding.orbitUserRightName, binding.orbitUserRightLocation, u);
}
if (users.size() > 2) {
CommunityResponse.NearbyUser u = users.get(2);
userBottomRight = bindUserFromApi("u_bottom_right", binding.orbitUserBottomRightAvatar, binding.orbitUserBottomRightName, binding.orbitUserBottomRightLocation, u);
}
if (users.size() > 3) {
CommunityResponse.NearbyUser u = users.get(3);
userBottomCenter = bindUserFromApi("u_bottom_center", binding.orbitUserBottomCenterAvatar, binding.orbitUserBottomCenterName, binding.orbitUserBottomCenterLocation, u);
}
if (users.size() > 4) {
CommunityResponse.NearbyUser u = users.get(4);
userBottomLeft = bindUserFromApi("u_bottom_left", binding.orbitUserBottomLeftAvatar, binding.orbitUserBottomLeftName, binding.orbitUserBottomLeftLocation, u);
}
if (users.size() > 5) {
CommunityResponse.NearbyUser u = users.get(5);
userLeft = bindUserFromApi("u_left", binding.orbitUserLeftAvatar, binding.orbitUserLeftName, binding.orbitUserLeftLocation, u);
}
int a = avatars[(int) (Math.random() * avatars.length)];
String n = names[(int) (Math.random() * names.length)];
String c = cities[(int) (Math.random() * cities.length)];
// 如果用户数量不足6个剩余位置显示默认头像
if (users.size() < 6) {
fillRemainingWithDefaultAvatar(users.size());
}
}
String bio = "真诚交友 · " + c + " · 在线";
// 默认头像资源三种颜色的圆形头像
private static final int[] DEFAULT_AVATARS = new int[] {
R.drawable.default_avatar_purple,
R.drawable.default_avatar_pink,
R.drawable.default_avatar_blue
};
Glide.with(avatarView)
.load(a)
.circleCrop()
.into(avatarView);
nameView.setText(n);
locationView.setText(c);
private OrbitUserData bindUserFromApi(String id,
android.widget.ImageView avatarView,
android.widget.TextView nameView,
android.widget.TextView locationView,
CommunityResponse.NearbyUser user) {
if (avatarView == null || nameView == null || locationView == null || user == null) {
return new OrbitUserData(id, "", "", "", DEFAULT_AVATARS[0]);
}
String name = user.getNickname() != null ? user.getNickname() : "用户";
String location = user.getLocation() != null ? user.getLocation() : "附近";
String avatarUrl = user.getAvatar();
String bio = "真诚交友 · " + location + " · " + (user.isOnline() ? "在线" : "离线");
// 加载头像有URL则加载网络图片否则随机使用默认头像
if (avatarUrl != null && !avatarUrl.isEmpty()) {
// 拼接完整的头像URL
String fullAvatarUrl = avatarUrl;
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
// 获取API基础地址并拼接
String baseUrl = ApiClient.getCurrentBaseUrl(this);
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
fullAvatarUrl = baseUrl + "/" + avatarUrl;
}
Log.d(TAG, "加载头像: " + fullAvatarUrl);
// 随机选择一个默认头像作为占位图和错误图
int defaultAvatar = DEFAULT_AVATARS[(int) (Math.random() * DEFAULT_AVATARS.length)];
Glide.with(avatarView)
.load(fullAvatarUrl)
.placeholder(defaultAvatar)
.error(defaultAvatar)
.circleCrop()
.into(avatarView);
} else {
// 没有头像URL随机使用默认头像
int defaultAvatar = DEFAULT_AVATARS[(int) (Math.random() * DEFAULT_AVATARS.length)];
Glide.with(avatarView)
.load(defaultAvatar)
.circleCrop()
.into(avatarView);
}
nameView.setText(name);
locationView.setText(location);
avatarView.setAlpha(0.6f);
avatarView.animate().alpha(1f).setDuration(250L).start();
return new OrbitUserData(id, n, c, bio, a);
return new OrbitUserData(String.valueOf(user.getUserId()), name, location, bio, avatarUrl);
}
/**
* 用户数量不足时剩余位置显示默认头像
*/
private void fillRemainingWithDefaultAvatar(int startIndex) {
if (binding == null) return;
if (startIndex <= 0) {
userTop = setDefaultUserDisplay("u_top", binding.orbitUserTopAvatar, binding.orbitUserTopName, binding.orbitUserTopLocation);
}
if (startIndex <= 1) {
userRight = setDefaultUserDisplay("u_right", binding.orbitUserRightAvatar, binding.orbitUserRightName, binding.orbitUserRightLocation);
}
if (startIndex <= 2) {
userBottomRight = setDefaultUserDisplay("u_bottom_right", binding.orbitUserBottomRightAvatar, binding.orbitUserBottomRightName, binding.orbitUserBottomRightLocation);
}
if (startIndex <= 3) {
userBottomCenter = setDefaultUserDisplay("u_bottom_center", binding.orbitUserBottomCenterAvatar, binding.orbitUserBottomCenterName, binding.orbitUserBottomCenterLocation);
}
if (startIndex <= 4) {
userBottomLeft = setDefaultUserDisplay("u_bottom_left", binding.orbitUserBottomLeftAvatar, binding.orbitUserBottomLeftName, binding.orbitUserBottomLeftLocation);
}
if (startIndex <= 5) {
userLeft = setDefaultUserDisplay("u_left", binding.orbitUserLeftAvatar, binding.orbitUserLeftName, binding.orbitUserLeftLocation);
}
}
/**
* 设置默认用户显示用户数量不足时
*/
private OrbitUserData setDefaultUserDisplay(String id,
android.widget.ImageView avatarView,
android.widget.TextView nameView,
android.widget.TextView locationView) {
if (avatarView == null || nameView == null || locationView == null) {
return new OrbitUserData(id, "", "", "", DEFAULT_AVATARS[0]);
}
int defaultAvatar = DEFAULT_AVATARS[(int) (Math.random() * DEFAULT_AVATARS.length)];
Glide.with(avatarView)
.load(defaultAvatar)
.circleCrop()
.into(avatarView);
nameView.setText("暂无");
locationView.setText("");
avatarView.setAlpha(0.6f);
avatarView.animate().alpha(1f).setDuration(250L).start();
return new OrbitUserData(id, "暂无", "", "", defaultAvatar);
}
private void loadMockUsers() {
if (binding == null) return;
// API失败时显示提示而不是虚假数据
Log.w(TAG, "无法从服务器获取用户数据");
Toast.makeText(this, "无法获取用户数据,请检查网络连接", Toast.LENGTH_SHORT).show();
// 清空显示
clearUserDisplay(binding.orbitUserTopAvatar, binding.orbitUserTopName, binding.orbitUserTopLocation);
clearUserDisplay(binding.orbitUserRightAvatar, binding.orbitUserRightName, binding.orbitUserRightLocation);
clearUserDisplay(binding.orbitUserBottomRightAvatar, binding.orbitUserBottomRightName, binding.orbitUserBottomRightLocation);
clearUserDisplay(binding.orbitUserBottomCenterAvatar, binding.orbitUserBottomCenterName, binding.orbitUserBottomCenterLocation);
clearUserDisplay(binding.orbitUserBottomLeftAvatar, binding.orbitUserBottomLeftName, binding.orbitUserBottomLeftLocation);
clearUserDisplay(binding.orbitUserLeftAvatar, binding.orbitUserLeftName, binding.orbitUserLeftLocation);
}
/**
* 清空用户显示网络失败时使用
*/
private void clearUserDisplay(android.widget.ImageView avatarView,
android.widget.TextView nameView,
android.widget.TextView locationView) {
if (avatarView != null) {
int defaultAvatar = DEFAULT_AVATARS[(int) (Math.random() * DEFAULT_AVATARS.length)];
Glide.with(avatarView)
.load(defaultAvatar)
.circleCrop()
.into(avatarView);
}
if (nameView != null) {
nameView.setText("加载中...");
}
if (locationView != null) {
locationView.setText("");
}
}
private void setupOrbitUserInteractions() {
@ -398,14 +902,28 @@ public class FishPondActivity extends AppCompatActivity {
private void onOrbitUserClick(View view, OrbitUserData user) {
if (view == null || user == null) return;
playAvatarClickEffect(view);
view.postDelayed(() -> UserProfileReadOnlyActivity.start(
this,
user.id,
user.name,
user.location,
user.bio,
user.avatarRes
), 120L);
view.postDelayed(() -> {
// 如果有avatarUrl使用URL否则使用资源ID
if (user.avatarUrl != null && !user.avatarUrl.isEmpty()) {
UserProfileReadOnlyActivity.start(
this,
user.id,
user.name,
user.location,
user.bio,
user.avatarUrl
);
} else {
UserProfileReadOnlyActivity.start(
this,
user.id,
user.name,
user.location,
user.bio,
user.avatarRes
);
}
}, 120L);
}
private void playAvatarClickEffect(View view) {

View File

@ -45,7 +45,9 @@ public class FollowingListActivity extends AppCompatActivity {
adapter = new FriendsAdapter(item -> {
if (item == null) return;
Toast.makeText(this, "打开关注:" + item.getName(), Toast.LENGTH_SHORT).show();
// 跳转到用户主页传递签名信息
UserProfileReadOnlyActivity.start(this, item.getId(), item.getName(),
"", item.getSubtitle(), item.getAvatarUrl());
});
binding.followingRecyclerView.setLayoutManager(new LinearLayoutManager(this));
@ -80,11 +82,16 @@ public class FollowingListActivity extends AppCompatActivity {
String id = String.valueOf(user.get("userId"));
String name = (String) user.get("nickname");
String phone = (String) user.get("phone");
String signature = (String) user.get("signature");
String avatar = (String) user.get("avatar");
Boolean isOnline = (Boolean) user.get("isOnline");
String status = isOnline != null && isOnline ? "在线" : "离线";
items.add(new FriendItem(id, name != null ? name : phone, status,
isOnline != null && isOnline));
// 优先显示签名没有签名则显示在线状态
String subtitle = signature != null && !signature.isEmpty()
? signature
: (isOnline != null && isOnline ? "在线" : "离线");
items.add(new FriendItem(id, name != null ? name : phone, subtitle,
isOnline != null && isOnline, avatar));
}
adapter.submitList(items);
} else {

View File

@ -1,18 +1,46 @@
package com.example.livestreaming;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityHeartbeatSignalBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 心动信号页面 - 探探风格卡片滑动匹配
*/
public class HeartbeatSignalActivity extends AppCompatActivity {
private ActivityHeartbeatSignalBinding binding;
private ApiService apiService;
private List<CommunityResponse.MatchUser> users = new ArrayList<>();
private int currentIndex = 0;
private View currentCard;
private boolean isAnimating = false;
public static void start(Context context) {
Intent intent = new Intent(context, HeartbeatSignalActivity.class);
@ -25,16 +53,45 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
binding = ActivityHeartbeatSignalBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
apiService = ApiClient.getService(this);
setupUI();
loadRecommendUsers();
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
// 跳过按钮
binding.btnSkip.setOnClickListener(v -> {
if (!isAnimating && currentCard != null) {
swipeCard(false);
}
});
// 喜欢按钮
binding.btnLike.setOnClickListener(v -> {
if (!isAnimating && currentCard != null) {
swipeCard(true);
}
});
// 超级喜欢按钮
binding.btnSuperLike.setOnClickListener(v -> {
if (!isAnimating && currentCard != null) {
superLike();
}
});
// 刷新按钮
binding.btnRefresh.setOnClickListener(v -> loadRecommendUsers());
setupBottomNav();
}
private void setupBottomNav() {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation);
bottomNavigation.setOnItemSelectedListener(item -> {
@ -44,6 +101,11 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
finish();
@ -63,13 +125,348 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
});
}
private void loadRecommendUsers() {
showLoading(true);
apiService.getRecommendUsers(10).enqueue(new Callback<ApiResponse<List<CommunityResponse.MatchUser>>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<List<CommunityResponse.MatchUser>>> call,
@NonNull Response<ApiResponse<List<CommunityResponse.MatchUser>>> response) {
showLoading(false);
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
users = response.body().getData();
currentIndex = 0;
showCards();
} else {
// 使用模拟数据
loadMockUsers();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<List<CommunityResponse.MatchUser>>> call, @NonNull Throwable t) {
showLoading(false);
// 使用模拟数据
loadMockUsers();
}
});
}
/**
* 加载模拟数据
*/
private void loadMockUsers() {
users = new ArrayList<>();
String[] names = {"小美", "阿宁", "Lina", "暖暖", "小七", "小北", "Mika", "安安", "小鹿", "糖糖"};
String[] locations = {"南宁 · 3km", "杭州 · 5km", "深圳 · 2km", "成都 · 8km", "武汉 · 4km"};
String[] bios = {
"喜欢旅行和美食,希望遇到有趣的灵魂~",
"爱笑的女孩运气不会太差",
"生活不止眼前的苟且,还有诗和远方",
"简单生活,快乐每一天",
"寻找那个对的人"
};
for (int i = 0; i < 10; i++) {
CommunityResponse.MatchUser user = new CommunityResponse.MatchUser();
user.id = i + 1;
user.nickname = names[i % names.length];
user.age = 20 + (int) (Math.random() * 8);
user.location = locations[i % locations.length];
user.bio = bios[i % bios.length];
user.gender = "";
users.add(user);
}
currentIndex = 0;
showCards();
}
private void showCards() {
binding.cardContainer.removeAllViews();
if (users.isEmpty()) {
showEmpty(true);
return;
}
showEmpty(false);
// 显示最多3张卡片层叠效果
int cardsToShow = Math.min(3, users.size() - currentIndex);
for (int i = cardsToShow - 1; i >= 0; i--) {
int index = currentIndex + i;
if (index < users.size()) {
View card = createCard(users.get(index), i);
binding.cardContainer.addView(card);
if (i == 0) {
currentCard = card;
setupCardTouchListener(card);
}
}
}
}
private View createCard(CommunityResponse.MatchUser user, int stackIndex) {
View card = getLayoutInflater().inflate(R.layout.item_swipe_card, binding.cardContainer, false);
// 设置用户信息
android.widget.TextView userName = card.findViewById(R.id.userName);
android.widget.TextView userAge = card.findViewById(R.id.userAge);
android.widget.TextView userLocation = card.findViewById(R.id.userLocation);
android.widget.TextView userBio = card.findViewById(R.id.userBio);
android.widget.ImageView userPhoto = card.findViewById(R.id.userPhoto);
userName.setText(user.nickname);
userAge.setText(user.age + "");
userLocation.setText(user.location);
userBio.setText(user.bio);
// 加载头像
String photoUrl = null;
if (user.photos != null && !user.photos.isEmpty()) {
photoUrl = user.photos.get(0);
} else {
photoUrl = user.avatar;
}
if (photoUrl != null && !photoUrl.isEmpty()) {
com.bumptech.glide.Glide.with(this)
.load(photoUrl)
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.centerCrop()
.into(userPhoto);
}
// 层叠效果
float scale = 1f - (stackIndex * 0.05f);
float translationY = stackIndex * 20f;
card.setScaleX(scale);
card.setScaleY(scale);
card.setTranslationY(translationY);
return card;
}
private void setupCardTouchListener(View card) {
card.setOnTouchListener(new View.OnTouchListener() {
private float startX, startY;
private float dX, dY;
@Override
public boolean onTouch(View v, android.view.MotionEvent event) {
if (isAnimating) return false;
switch (event.getAction()) {
case android.view.MotionEvent.ACTION_DOWN:
startX = event.getRawX();
startY = event.getRawY();
dX = v.getX() - startX;
dY = v.getY() - startY;
return true;
case android.view.MotionEvent.ACTION_MOVE:
float newX = event.getRawX() + dX;
float newY = event.getRawY() + dY;
v.setX(newX);
v.setY(newY);
// 旋转效果
float rotation = (newX - startX) / 20f;
v.setRotation(rotation);
// 显示喜欢/跳过标签
float deltaX = event.getRawX() - startX;
View likeStamp = v.findViewById(R.id.likeStamp);
View nopeStamp = v.findViewById(R.id.nopeStamp);
if (deltaX > 50) {
float alpha = Math.min(1f, (deltaX - 50) / 150f);
likeStamp.setAlpha(alpha);
nopeStamp.setAlpha(0f);
} else if (deltaX < -50) {
float alpha = Math.min(1f, (-deltaX - 50) / 150f);
nopeStamp.setAlpha(alpha);
likeStamp.setAlpha(0f);
} else {
likeStamp.setAlpha(0f);
nopeStamp.setAlpha(0f);
}
return true;
case android.view.MotionEvent.ACTION_UP:
float deltaXFinal = event.getRawX() - startX;
if (deltaXFinal > 150) {
// 右滑 - 喜欢
animateCardOut(v, true);
} else if (deltaXFinal < -150) {
// 左滑 - 跳过
animateCardOut(v, false);
} else {
// 回弹
v.animate()
.x(0)
.y(0)
.rotation(0)
.setDuration(200)
.start();
v.findViewById(R.id.likeStamp).setAlpha(0f);
v.findViewById(R.id.nopeStamp).setAlpha(0f);
}
return true;
}
return false;
}
});
}
private void swipeCard(boolean isLike) {
if (currentCard == null || isAnimating) return;
animateCardOut(currentCard, isLike);
}
private void animateCardOut(View card, boolean isLike) {
isAnimating = true;
float targetX = isLike ? 1000f : -1000f;
float targetRotation = isLike ? 30f : -30f;
card.animate()
.x(targetX)
.rotation(targetRotation)
.alpha(0f)
.setDuration(300)
.setInterpolator(new AccelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
// 发送喜欢/跳过请求
if (currentIndex < users.size()) {
CommunityResponse.MatchUser user = users.get(currentIndex);
if (isLike) {
sendLike(user.id);
} else {
sendSkip(user.id);
}
}
currentIndex++;
showCards();
}
})
.start();
}
private void superLike() {
if (currentCard == null || isAnimating) return;
isAnimating = true;
// 超级喜欢动画 - 向上飞出
currentCard.animate()
.y(-1000f)
.scaleX(1.2f)
.scaleY(1.2f)
.alpha(0f)
.setDuration(400)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
if (currentIndex < users.size()) {
CommunityResponse.MatchUser user = users.get(currentIndex);
sendSuperLike(user.id);
}
currentIndex++;
showCards();
}
})
.start();
Toast.makeText(this, "超级喜欢!", Toast.LENGTH_SHORT).show();
}
private void sendLike(int userId) {
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
apiService.likeUser(body).enqueue(new Callback<ApiResponse<CommunityResponse.MatchResult>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<CommunityResponse.MatchResult>> call,
@NonNull Response<ApiResponse<CommunityResponse.MatchResult>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
CommunityResponse.MatchResult result = response.body().getData();
if (result.isMatch) {
showMatchDialog(result.matchedUser);
}
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<CommunityResponse.MatchResult>> call, @NonNull Throwable t) {
// 忽略错误
}
});
}
private void sendSkip(int userId) {
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
apiService.skipUser(body).enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<String>> call, @NonNull Response<ApiResponse<String>> response) {
// 忽略结果
}
@Override
public void onFailure(@NonNull Call<ApiResponse<String>> call, @NonNull Throwable t) {
// 忽略错误
}
});
}
private void sendSuperLike(int userId) {
// 超级喜欢也是喜欢只是权重更高
sendLike(userId);
}
private void showMatchDialog(CommunityResponse.MatchUser user) {
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("💕 配对成功!")
.setMessage("你和 " + (user != null ? user.nickname : "TA") + " 互相喜欢!\n快去打个招呼吧~")
.setPositiveButton("发消息", (dialog, which) -> {
// TODO: 跳转到聊天页面
Toast.makeText(this, "即将跳转到聊天", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("继续浏览", null)
.show();
}
private void showLoading(boolean show) {
binding.loadingView.setVisibility(show ? View.VISIBLE : View.GONE);
binding.cardContainer.setVisibility(show ? View.GONE : View.VISIBLE);
binding.emptyView.setVisibility(View.GONE);
}
private void showEmpty(boolean show) {
binding.emptyView.setVisibility(show ? View.VISIBLE : View.GONE);
binding.cardContainer.setVisibility(show ? View.GONE : View.VISIBLE);
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNav);
}
}

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityKtvTogetherBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 一起KTV - 从后端API加载消息
*/
public class KTVTogetherActivity extends BaseCategoryActivity {
public class KTVTogetherActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "一起KTV";
private static final String CATEGORY = "一起KTV";
private ActivityKtvTogetherBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, KTVTogetherActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, KTVTogetherActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,27 +38,12 @@ public class KTVTogetherActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNavigation);
@ -82,28 +74,6 @@ public class KTVTogetherActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
@ -111,7 +81,6 @@ public class KTVTogetherActivity extends AppCompatActivity {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
loadPosts();
}
}
}

View File

@ -216,8 +216,10 @@ public class MyFriendsActivity extends AppCompatActivity {
String id = String.valueOf(item.opt("id"));
String name = item.optString("name", "未知用户");
String avatarUrl = item.optString("avatarUrl", "");
String signature = item.optString("signature", "");
boolean isOnline = item.optInt("isOnline", 0) == 1;
String subtitle = isOnline ? "在线" : "离线";
// 优先显示签名没有签名则显示在线状态
String subtitle = !signature.isEmpty() ? signature : (isOnline ? "在线" : "离线");
allFriends.add(new FriendItem(id, name, subtitle, isOnline, avatarUrl));
} catch (Exception e) {
Log.e(TAG, "解析好友项失败", e);

View File

@ -18,37 +18,48 @@ public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapt
void onUserClick(NearbyUser user);
}
public interface OnAddFriendClickListener {
void onAddFriendClick(NearbyUser user);
}
private final OnUserClickListener onUserClickListener;
private OnAddFriendClickListener onAddFriendClickListener;
public NearbyUsersAdapter(OnUserClickListener onUserClickListener) {
super(DIFF);
this.onUserClickListener = onUserClickListener;
}
public void setOnAddFriendClickListener(OnAddFriendClickListener listener) {
this.onAddFriendClickListener = listener;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemNearbyUserBinding binding = ItemNearbyUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new VH(binding, onUserClickListener);
return new VH(binding, onUserClickListener, onAddFriendClickListener);
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
holder.bind(getItem(position));
holder.bind(getItem(position), onAddFriendClickListener);
}
static class VH extends RecyclerView.ViewHolder {
private final ItemNearbyUserBinding binding;
private final OnUserClickListener onUserClickListener;
private OnAddFriendClickListener onAddFriendClickListener;
VH(ItemNearbyUserBinding binding, OnUserClickListener onUserClickListener) {
VH(ItemNearbyUserBinding binding, OnUserClickListener onUserClickListener, OnAddFriendClickListener onAddFriendClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onUserClickListener = onUserClickListener;
this.onAddFriendClickListener = onAddFriendClickListener;
}
void bind(NearbyUser user) {
void bind(NearbyUser user, OnAddFriendClickListener addFriendListener) {
if (user == null) return;
// 设置用户名
@ -63,11 +74,6 @@ public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapt
binding.distanceText.setText(distanceText);
// 使用Glide加载头像
// TODO: 接入后端接口 - 从后端获取附近用户头像URL
// 接口路径: GET /api/user/profile/{userId}
// 请求参数: userId (路径参数从NearbyUser对象中获取)
// 返回数据格式: ApiResponse<{avatarUrl: string}>
// 目前使用默认占位图
Glide.with(binding.avatarImage.getContext())
.load((String) null) // 暂时为null等待后端接口
.circleCrop()
@ -82,14 +88,16 @@ public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapt
binding.liveBadge.setVisibility(View.GONE);
}
// 添加按钮点击事件
// 添加按钮点击事件 - 发送好友请求
binding.addButton.setOnClickListener(v -> {
if (onUserClickListener != null) {
if (addFriendListener != null) {
addFriendListener.onAddFriendClick(user);
} else if (onUserClickListener != null) {
onUserClickListener.onUserClick(user);
}
});
// 整个item点击也可以触发添加
// 整个item点击跳转到用户主页
binding.getRoot().setOnClickListener(v -> {
if (onUserClickListener != null) {
onUserClickListener.onUserClick(user);

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityOnlineDatingBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 在线处对象 - 从后端API加载消息
*/
public class OnlineDatingActivity extends BaseCategoryActivity {
public class OnlineDatingActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "在线处对象";
private static final String CATEGORY = "在线处对象";
private ActivityOnlineDatingBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, OnlineDatingActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, OnlineDatingActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,35 +38,14 @@ public class OnlineDatingActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
// 发布动态按钮
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
// 发布成功添加到列表顶部
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {
// 发布失败
}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation);
bottomNavigation.setOnItemSelectedListener(item -> {
@ -88,39 +74,13 @@ public class OnlineDatingActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
// 加载该分类的动态
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNav);
// 刷新动态列表
loadPosts();
}
}
}

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityPeaceEliteBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 和平精英 - 从后端API加载消息
*/
public class PeaceEliteActivity extends BaseCategoryActivity {
public class PeaceEliteActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "和平精英";
private static final String CATEGORY = "和平精英";
private ActivityPeaceEliteBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, PeaceEliteActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, PeaceEliteActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,27 +38,12 @@ public class PeaceEliteActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNavigation);
@ -82,28 +74,6 @@ public class PeaceEliteActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
@ -111,7 +81,6 @@ public class PeaceEliteActivity extends AppCompatActivity {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
loadPosts();
}
}
}

View File

@ -1,42 +1,51 @@
package com.example.livestreaming;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Post implements Serializable {
private String id;
private String userId;
private String userName;
private String userAvatar;
private long id;
private String odlId; // 兼容旧的字符串ID
private String odlUserId;
private String authorName;
private String authorAvatar;
private String content;
private String imageUrl;
private List<String> images; // 多图片支持
private String category;
private long timestamp;
private String createTime;
private int likeCount;
private int commentCount;
private boolean isLiked;
public Post() {}
public Post(String id, String userId, String userName, String content, String category) {
this.id = id;
this.userId = userId;
this.userName = userName;
public Post(String odlId, String odlUserId, String authorName, String content, String category) {
this.odlId = odlId;
this.odlUserId = odlUserId;
this.authorName = authorName;
this.content = content;
this.category = category;
this.timestamp = System.currentTimeMillis();
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getOdlId() { return odlId; }
public void setOdlId(String odlId) { this.odlId = odlId; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getOdlUserId() { return odlUserId; }
public void setOdlUserId(String odlUserId) { this.odlUserId = odlUserId; }
public String getUserAvatar() { return userAvatar; }
public void setUserAvatar(String userAvatar) { this.userAvatar = userAvatar; }
public String getAuthorName() { return authorName; }
public void setAuthorName(String authorName) { this.authorName = authorName; }
public String getAuthorAvatar() { return authorAvatar; }
public void setAuthorAvatar(String authorAvatar) { this.authorAvatar = authorAvatar; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
@ -44,12 +53,50 @@ public class Post implements Serializable {
public String getImageUrl() { return imageUrl; }
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
/**
* 获取图片列表
*/
public List<String> getImages() {
if (images != null && !images.isEmpty()) {
return images;
}
// 兼容旧的单图片字段
if (imageUrl != null && !imageUrl.isEmpty()) {
return Arrays.asList(imageUrl);
}
return new ArrayList<>();
}
/**
* 设置图片列表支持逗号分隔的字符串或List
*/
public void setImages(String imagesStr) {
if (imagesStr != null && !imagesStr.isEmpty()) {
// 解析逗号分隔的图片URL
String[] arr = imagesStr.split(",");
this.images = new ArrayList<>();
for (String url : arr) {
String trimmed = url.trim();
if (!trimmed.isEmpty()) {
this.images.add(trimmed);
}
}
}
}
public void setImagesList(List<String> images) {
this.images = images;
}
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public String getCreateTime() { return createTime; }
public void setCreateTime(String createTime) { this.createTime = createTime; }
public int getLikeCount() { return likeCount; }
public void setLikeCount(int likeCount) { this.likeCount = likeCount; }

View File

@ -1,14 +1,18 @@
package com.example.livestreaming;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@ -17,13 +21,35 @@ import java.util.Locale;
public class PostAdapter extends RecyclerView.Adapter<PostAdapter.PostViewHolder> {
private final Context context;
private List<Post> posts = new ArrayList<>();
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(Post post);
}
public PostAdapter(Context context) {
this.context = context;
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
public void setPosts(List<Post> posts) {
this.posts = posts != null ? posts : new ArrayList<>();
notifyDataSetChanged();
}
public void addPosts(List<Post> newPosts) {
if (newPosts != null && !newPosts.isEmpty()) {
int start = posts.size();
posts.addAll(newPosts);
notifyItemRangeInserted(start, newPosts.size());
}
}
public void addPost(Post post) {
if (post != null) {
posts.add(0, post);
@ -50,31 +76,107 @@ public class PostAdapter extends RecyclerView.Adapter<PostAdapter.PostViewHolder
return posts.size();
}
static class PostViewHolder extends RecyclerView.ViewHolder {
class PostViewHolder extends RecyclerView.ViewHolder {
private final ImageView ivAvatar;
private final TextView tvUserName;
private final TextView tvContent;
private final TextView tvTime;
private final TextView tvLikeCount;
private final TextView tvCommentCount;
private final LinearLayout imagesContainer;
public PostViewHolder(@NonNull View itemView) {
super(itemView);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
tvUserName = itemView.findViewById(R.id.tv_user_name);
tvContent = itemView.findViewById(R.id.tv_content);
tvTime = itemView.findViewById(R.id.tv_time);
tvLikeCount = itemView.findViewById(R.id.tv_like_count);
tvCommentCount = itemView.findViewById(R.id.tv_comment_count);
imagesContainer = itemView.findViewById(R.id.images_container);
itemView.setOnClickListener(v -> {
int pos = getAdapterPosition();
if (pos != RecyclerView.NO_POSITION && listener != null) {
listener.onItemClick(posts.get(pos));
}
});
}
public void bind(Post post) {
if (tvUserName != null) tvUserName.setText(post.getUserName());
if (tvContent != null) tvContent.setText(post.getContent());
if (tvTime != null) {
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
tvTime.setText(sdf.format(new Date(post.getTimestamp())));
// 用户名
if (tvUserName != null) {
tvUserName.setText(post.getAuthorName() != null ? post.getAuthorName() : "匿名用户");
}
// 内容
if (tvContent != null) {
tvContent.setText(post.getContent());
}
// 时间
if (tvTime != null) {
if (post.getCreateTime() != null && !post.getCreateTime().isEmpty()) {
tvTime.setText(post.getCreateTime());
} else if (post.getTimestamp() > 0) {
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
tvTime.setText(sdf.format(new Date(post.getTimestamp())));
}
}
// 点赞数
if (tvLikeCount != null) {
tvLikeCount.setText(String.valueOf(post.getLikeCount()));
}
// 评论数
if (tvCommentCount != null) {
tvCommentCount.setText(String.valueOf(post.getCommentCount()));
}
// 头像
if (ivAvatar != null) {
String avatarUrl = post.getAuthorAvatar();
if (avatarUrl != null && !avatarUrl.isEmpty()) {
Glide.with(context)
.load(avatarUrl)
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.circleCrop()
.into(ivAvatar);
} else {
ivAvatar.setImageResource(R.drawable.wish_tree_checker_backup);
}
}
// 图片列表
if (imagesContainer != null) {
imagesContainer.removeAllViews();
List<String> images = post.getImages();
if (images != null && !images.isEmpty()) {
imagesContainer.setVisibility(View.VISIBLE);
for (int i = 0; i < Math.min(images.size(), 9); i++) {
ImageView imageView = new ImageView(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
200, 200
);
params.setMargins(0, 0, 8, 0);
imageView.setLayoutParams(params);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
Glide.with(context)
.load(images.get(i))
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.centerCrop()
.into(imageView);
imagesContainer.addView(imageView);
}
} else {
imagesContainer.setVisibility(View.GONE);
}
}
if (tvLikeCount != null) tvLikeCount.setText(String.valueOf(post.getLikeCount()));
if (tvCommentCount != null) tvCommentCount.setText(String.valueOf(post.getCommentCount()));
}
}
}

View File

@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
@ -24,6 +25,9 @@ import com.bumptech.glide.Glide;
import com.example.livestreaming.BuildConfig;
import com.example.livestreaming.databinding.ActivityProfileBinding;
import com.example.livestreaming.ShareUtils;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.UserInfoResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
@ -36,14 +40,13 @@ import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ProfileActivity extends AppCompatActivity {
private static final String TAG = "ProfileActivity";
private ActivityProfileBinding binding;
private static final String PREFS_NAME = "profile_prefs";
@ -86,6 +89,7 @@ public class ProfileActivity extends AppCompatActivity {
);
loadProfileFromPrefs();
loadUserInfoFromServer(); // 从服务器加载用户信息
loadAndDisplayTags();
loadProfileInfo();
loadFollowStats(); // 加载关注统计
@ -191,6 +195,102 @@ public class ProfileActivity extends AppCompatActivity {
// 等级/称号/徽章保持固定显示例如月亮/星耀/至尊不从本地缓存覆盖
}
/**
* 从服务器加载用户信息并同步到本地
*/
private void loadUserInfoFromServer() {
if (!AuthHelper.isLoggedIn(this)) {
return;
}
ApiClient.getService(this).getUserInfo().enqueue(new Callback<ApiResponse<UserInfoResponse>>() {
@Override
public void onResponse(Call<ApiResponse<UserInfoResponse>> call, Response<ApiResponse<UserInfoResponse>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
UserInfoResponse userInfo = response.body().getData();
if (userInfo != null) {
syncUserInfoToLocal(userInfo);
}
}
}
@Override
public void onFailure(Call<ApiResponse<UserInfoResponse>> call, Throwable t) {
Log.e(TAG, "加载用户信息失败: " + t.getMessage());
}
});
}
/**
* 同步服务器用户信息到本地并更新UI
*/
private void syncUserInfoToLocal(UserInfoResponse userInfo) {
android.content.SharedPreferences.Editor editor = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit();
// 同步昵称
if (!TextUtils.isEmpty(userInfo.getNickname())) {
editor.putString(KEY_NAME, userInfo.getNickname());
binding.name.setText(userInfo.getNickname());
}
// 同步个性签名
if (!TextUtils.isEmpty(userInfo.getMark())) {
editor.putString(KEY_BIO, userInfo.getMark());
binding.bioText.setText(userInfo.getMark());
binding.bioText.setTextColor(0xFF111111);
}
// 同步头像
if (!TextUtils.isEmpty(userInfo.getAvatar())) {
String avatarUrl = userInfo.getAvatar();
String baseUrl = ApiClient.getCurrentBaseUrl(ProfileActivity.this);
// 处理相对路径
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
if (avatarUrl.startsWith("crmebimage/")) {
avatarUrl = baseUrl.replace("/api/", "/") + avatarUrl;
}
}
editor.putString(KEY_AVATAR_URI, avatarUrl);
Glide.with(ProfileActivity.this)
.load(avatarUrl)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(binding.avatar);
}
// 同步生日
if (!TextUtils.isEmpty(userInfo.getBirthday())) {
editor.putString(KEY_BIRTHDAY, userInfo.getBirthday());
}
// 同步性别
if (userInfo.getSex() != null) {
String gender = "";
switch (userInfo.getSex()) {
case 1: gender = ""; break;
case 2: gender = ""; break;
case 3: gender = "保密"; break;
}
if (!TextUtils.isEmpty(gender)) {
editor.putString(KEY_GENDER, gender);
}
}
// 同步地址
if (!TextUtils.isEmpty(userInfo.getAddres())) {
editor.putString(KEY_LOCATION, userInfo.getAddres());
}
editor.apply();
// 刷新标签显示
loadAndDisplayTags();
loadProfileInfo();
}
private void setupEditableAreas() {
// TODO: 接入后端接口 - 更新用户资料
// 接口路径: PUT /api/users/{userId}/profile
@ -698,13 +798,21 @@ public class ProfileActivity extends AppCompatActivity {
// 更新关注数
Object followingCount = stats.get("followingCount");
if (followingCount != null) {
binding.following.setText(String.valueOf(followingCount) + "\n关注");
int count = 0;
if (followingCount instanceof Number) {
count = ((Number) followingCount).intValue();
}
binding.following.setText(count + "\n关注");
}
// 更新粉丝数
Object followersCount = stats.get("followersCount");
if (followersCount != null) {
binding.followers.setText(String.valueOf(followersCount) + "\n粉丝");
int count = 0;
if (followersCount instanceof Number) {
count = ((Number) followersCount).intValue();
}
binding.followers.setText(count + "\n粉丝");
}
}
}

View File

@ -5,6 +5,20 @@ import android.app.AlertDialog;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import java.util.HashMap;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class PublishPostHelper {
public interface PublishCallback {
@ -12,9 +26,22 @@ public class PublishPostHelper {
void onError(String error);
}
/**
* 显示发布对话框旧版本本地保存
*/
public static void showPublishDialog(Activity activity, String category, PublishCallback callback) {
showPublishDialog(activity, category, -1, callback);
}
/**
* 显示发布对话框新版本支持API发布
* @param categoryId 板块ID如果>0则通过API发布否则本地保存
*/
public static void showPublishDialog(Activity activity, String category, int categoryId, PublishCallback callback) {
EditText input = new EditText(activity);
input.setHint("输入内容...");
input.setMinLines(3);
input.setPadding(32, 32, 32, 32);
new AlertDialog.Builder(activity)
.setTitle("发布动态")
@ -25,19 +52,86 @@ public class PublishPostHelper {
Toast.makeText(activity, "内容不能为空", Toast.LENGTH_SHORT).show();
return;
}
Post post = new Post();
post.setId(String.valueOf(System.currentTimeMillis()));
post.setContent(content);
post.setCategory(category);
post.setUserName("用户");
post.setTimestamp(System.currentTimeMillis());
PostManager.savePost(activity, post);
if (callback != null) {
callback.onSuccess(post);
if (categoryId > 0) {
// 通过API发布
publishToApi(activity, categoryId, category, content, callback);
} else {
// 本地保存兼容旧逻辑
Post post = new Post();
post.setId(System.currentTimeMillis());
post.setContent(content);
post.setCategory(category);
post.setAuthorName("用户");
post.setTimestamp(System.currentTimeMillis());
PostManager.savePost(activity, post);
if (callback != null) {
callback.onSuccess(post);
}
}
})
.setNegativeButton("取消", null)
.show();
}
/**
* 通过API发布消息
*/
private static void publishToApi(Activity activity, int categoryId, String category,
String content, PublishCallback callback) {
ApiService apiService = ApiClient.getService(activity);
Map<String, Object> body = new HashMap<>();
body.put("categoryId", categoryId);
body.put("content", content);
apiService.publishCommunityMessage(body).enqueue(new Callback<ApiResponse<CommunityResponse.Message>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<CommunityResponse.Message>> call,
@NonNull Response<ApiResponse<CommunityResponse.Message>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Toast.makeText(activity, "发布成功", Toast.LENGTH_SHORT).show();
// 转换为Post对象
CommunityResponse.Message msg = response.body().getData();
Post post = new Post();
if (msg != null) {
post.setId(msg.id);
post.setAuthorName(msg.nickname);
post.setAuthorAvatar(msg.avatar);
post.setContent(msg.content);
post.setCategory(category);
post.setCreateTime(msg.createTime);
} else {
post.setId(System.currentTimeMillis());
post.setContent(content);
post.setCategory(category);
}
if (callback != null) {
callback.onSuccess(post);
}
} else {
String errorMsg = "发布失败";
if (response.body() != null && response.body().getMessage() != null) {
errorMsg = response.body().getMessage();
}
Toast.makeText(activity, errorMsg, Toast.LENGTH_SHORT).show();
if (callback != null) {
callback.onError(errorMsg);
}
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<CommunityResponse.Message>> call,
@NonNull Throwable t) {
Toast.makeText(activity, "网络错误,请重试", Toast.LENGTH_SHORT).show();
if (callback != null) {
callback.onError("网络错误");
}
}
});
}
}

View File

@ -0,0 +1,170 @@
package com.example.livestreaming;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
/**
* 推荐用户列表适配器关注页面使用
*/
public class RecommendUserAdapter extends ListAdapter<RecommendUserAdapter.RecommendUser, RecommendUserAdapter.ViewHolder> {
public interface OnUserActionListener {
void onFollowClick(RecommendUser user, int position);
void onRemoveClick(RecommendUser user, int position);
void onUserClick(RecommendUser user);
}
private OnUserActionListener listener;
public RecommendUserAdapter() {
super(DIFF_CALLBACK);
}
public void setOnUserActionListener(OnUserActionListener listener) {
this.listener = listener;
}
private static final DiffUtil.ItemCallback<RecommendUser> DIFF_CALLBACK = new DiffUtil.ItemCallback<RecommendUser>() {
@Override
public boolean areItemsTheSame(@NonNull RecommendUser oldItem, @NonNull RecommendUser newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull RecommendUser oldItem, @NonNull RecommendUser newItem) {
return oldItem.equals(newItem);
}
};
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_recommend_user, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
RecommendUser user = getItem(position);
holder.bind(user, position);
}
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView avatar;
private final TextView name;
private final TextView desc;
private final ImageView verifiedIcon;
private final TextView btnFollow;
private final ImageView btnRemove;
ViewHolder(@NonNull View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.userAvatar);
name = itemView.findViewById(R.id.userName);
desc = itemView.findViewById(R.id.userDesc);
verifiedIcon = itemView.findViewById(R.id.verifiedIcon);
btnFollow = itemView.findViewById(R.id.btnFollow);
btnRemove = itemView.findViewById(R.id.btnRemove);
}
void bind(RecommendUser user, int position) {
name.setText(user.getName());
desc.setText(user.getDescription());
// 加载头像
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
Glide.with(itemView.getContext())
.load(user.getAvatarUrl())
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatar);
} else {
avatar.setImageResource(R.drawable.ic_account_circle_24);
}
// 认证标识
verifiedIcon.setVisibility(user.isVerified() ? View.VISIBLE : View.GONE);
// 关注状态
if (user.isFollowed()) {
btnFollow.setText("已关注");
btnFollow.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null));
btnFollow.setBackgroundResource(R.drawable.bg_channel_tag);
} else {
btnFollow.setText("关注");
btnFollow.setTextColor(itemView.getContext().getResources().getColor(R.color.red_primary, null));
btnFollow.setBackgroundResource(R.drawable.bg_follow_button);
}
// 点击事件
itemView.setOnClickListener(v -> {
if (listener != null) listener.onUserClick(user);
});
btnFollow.setOnClickListener(v -> {
if (listener != null) listener.onFollowClick(user, position);
});
btnRemove.setOnClickListener(v -> {
if (listener != null) listener.onRemoveClick(user, position);
});
}
}
/**
* 推荐用户数据类
*/
public static class RecommendUser {
private int id;
private String name;
private String avatarUrl;
private String description;
private boolean verified;
private boolean followed;
public RecommendUser(int id, String name, String avatarUrl, String description, boolean verified) {
this.id = id;
this.name = name;
this.avatarUrl = avatarUrl;
this.description = description;
this.verified = verified;
this.followed = false;
}
public int getId() { return id; }
public String getName() { return name; }
public String getAvatarUrl() { return avatarUrl; }
public String getDescription() { return description; }
public boolean isVerified() { return verified; }
public boolean isFollowed() { return followed; }
public void setFollowed(boolean followed) { this.followed = followed; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecommendUser that = (RecommendUser) o;
return id == that.id && verified == that.verified && followed == that.followed
&& java.util.Objects.equals(name, that.name)
&& java.util.Objects.equals(avatarUrl, that.avatarUrl)
&& java.util.Objects.equals(description, that.description);
}
@Override
public int hashCode() {
return java.util.Objects.hash(id, name, avatarUrl, description, verified, followed);
}
}
}

View File

@ -0,0 +1,153 @@
package com.example.livestreaming;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.livestreaming.net.CommunityResponse;
import java.util.ArrayList;
import java.util.List;
/**
* 心动信号卡片适配器 - 探探风格滑动卡片
*/
public class SwipeCardAdapter extends RecyclerView.Adapter<SwipeCardAdapter.ViewHolder> {
private final Context context;
private final List<CommunityResponse.MatchUser> users = new ArrayList<>();
public SwipeCardAdapter(Context context) {
this.context = context;
}
public void setUsers(List<CommunityResponse.MatchUser> newUsers) {
users.clear();
if (newUsers != null) {
users.addAll(newUsers);
}
notifyDataSetChanged();
}
public void addUsers(List<CommunityResponse.MatchUser> newUsers) {
if (newUsers != null && !newUsers.isEmpty()) {
int start = users.size();
users.addAll(newUsers);
notifyItemRangeInserted(start, newUsers.size());
}
}
public CommunityResponse.MatchUser getUser(int position) {
if (position >= 0 && position < users.size()) {
return users.get(position);
}
return null;
}
public void removeUser(int position) {
if (position >= 0 && position < users.size()) {
users.remove(position);
notifyItemRemoved(position);
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_swipe_card, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CommunityResponse.MatchUser user = users.get(position);
holder.bind(user);
}
@Override
public int getItemCount() {
return users.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView userPhoto;
private final TextView userName;
private final TextView userAge;
private final TextView userLocation;
private final TextView userBio;
private final ImageView likeStamp;
private final ImageView nopeStamp;
ViewHolder(@NonNull View itemView) {
super(itemView);
userPhoto = itemView.findViewById(R.id.userPhoto);
userName = itemView.findViewById(R.id.userName);
userAge = itemView.findViewById(R.id.userAge);
userLocation = itemView.findViewById(R.id.userLocation);
userBio = itemView.findViewById(R.id.userBio);
likeStamp = itemView.findViewById(R.id.likeStamp);
nopeStamp = itemView.findViewById(R.id.nopeStamp);
}
void bind(CommunityResponse.MatchUser user) {
userName.setText(user.nickname);
userAge.setText(user.age + "");
userLocation.setText(user.location);
userBio.setText(user.bio);
// 加载头像
String photoUrl = null;
if (user.photos != null && !user.photos.isEmpty()) {
photoUrl = user.photos.get(0);
} else {
photoUrl = user.avatar;
}
if (photoUrl != null && !photoUrl.isEmpty()) {
Glide.with(context)
.load(photoUrl)
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.centerCrop()
.into(userPhoto);
} else {
userPhoto.setImageResource(R.drawable.wish_tree_checker_backup);
}
// 重置标签透明度
likeStamp.setAlpha(0f);
nopeStamp.setAlpha(0f);
}
/**
* 显示喜欢标签
*/
void showLikeStamp(float alpha) {
likeStamp.setAlpha(Math.min(1f, alpha));
nopeStamp.setAlpha(0f);
}
/**
* 显示跳过标签
*/
void showNopeStamp(float alpha) {
nopeStamp.setAlpha(Math.min(1f, alpha));
likeStamp.setAlpha(0f);
}
/**
* 隐藏所有标签
*/
void hideStamps() {
likeStamp.setAlpha(0f);
nopeStamp.setAlpha(0f);
}
}
}

View File

@ -3,25 +3,32 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityTableGamesBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List;
/**
* 桌游 - 从后端API加载消息
*/
public class TableGamesActivity extends BaseCategoryActivity {
public class TableGamesActivity extends AppCompatActivity {
private static final int CATEGORY_ID = -1; // 通过名称自动查找
private static final String CATEGORY_NAME = "桌游";
private static final String CATEGORY = "桌子游戏";
private ActivityTableGamesBinding binding;
private PostAdapter postAdapter;
public static void start(Context context) {
Intent intent = new Intent(context, TableGamesActivity.class);
context.startActivity(intent);
context.startActivity(new Intent(context, TableGamesActivity.class));
}
@Override
protected int getCategoryId() {
return CATEGORY_ID;
}
@Override
protected String getCategoryName() {
return CATEGORY_NAME;
}
@Override
@ -31,27 +38,12 @@ public class TableGamesActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
setupRecyclerView();
loadPosts();
initMessageList(binding.recyclerPosts, binding.emptyView, null, binding.fabPublish);
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
binding.fabPublish.setOnClickListener(v -> {
PublishPostHelper.showPublishDialog(this, CATEGORY, new PublishPostHelper.PublishCallback() {
@Override
public void onSuccess(Post post) {
postAdapter.addPost(post);
binding.recyclerPosts.scrollToPosition(0);
updateEmptyView();
}
@Override
public void onError(String error) {}
});
});
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNavigation);
@ -82,28 +74,6 @@ public class TableGamesActivity extends AppCompatActivity {
});
}
private void setupRecyclerView() {
postAdapter = new PostAdapter();
binding.recyclerPosts.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerPosts.setAdapter(postAdapter);
}
private void loadPosts() {
List<Post> posts = PostManager.getPostsByCategory(this, CATEGORY);
postAdapter.setPosts(posts);
updateEmptyView();
}
private void updateEmptyView() {
if (postAdapter.getItemCount() == 0) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.recyclerPosts.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.recyclerPosts.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
@ -111,7 +81,6 @@ public class TableGamesActivity extends AppCompatActivity {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
loadPosts();
}
}
}

View File

@ -13,6 +13,7 @@ import com.example.livestreaming.databinding.ActivityUserProfileReadOnlyBinding;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.AuthStore;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
@ -35,9 +36,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
private static final String EXTRA_LOCATION = "extra_location";
private static final String EXTRA_BIO = "extra_bio";
private static final String EXTRA_AVATAR_RES = "extra_avatar_res";
private static final String EXTRA_AVATAR_URL = "extra_avatar_url";
private String currentUserId;
private String currentUserName;
private boolean isFollowing = false;
private boolean isFriend = false;
private boolean hasPendingRequest = false; // 是否已发送好友请求
public static void start(Context context, String userId, String name, String location, String bio, int avatarRes) {
Intent intent = new Intent(context, UserProfileReadOnlyActivity.class);
@ -49,6 +54,16 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
context.startActivity(intent);
}
public static void start(Context context, String userId, String name, String location, String bio, String avatarUrl) {
Intent intent = new Intent(context, UserProfileReadOnlyActivity.class);
intent.putExtra(EXTRA_USER_ID, userId);
intent.putExtra(EXTRA_NAME, name);
intent.putExtra(EXTRA_LOCATION, location);
intent.putExtra(EXTRA_BIO, bio);
intent.putExtra(EXTRA_AVATAR_URL, avatarUrl);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -58,20 +73,39 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
binding.backButton.setOnClickListener(v -> finish());
currentUserId = getIntent().getStringExtra(EXTRA_USER_ID);
String name = getIntent().getStringExtra(EXTRA_NAME);
currentUserName = getIntent().getStringExtra(EXTRA_NAME);
String name = currentUserName;
String location = getIntent().getStringExtra(EXTRA_LOCATION);
String bio = getIntent().getStringExtra(EXTRA_BIO);
int avatarRes = getIntent().getIntExtra(EXTRA_AVATAR_RES, R.drawable.wish_tree_checker_backup);
String avatarUrl = getIntent().getStringExtra(EXTRA_AVATAR_URL);
binding.name.setText(name != null ? name : "");
binding.location.setText(location != null ? location : "");
binding.bio.setText(bio != null ? bio : "");
binding.avatar.setImageResource(avatarRes);
// 优先使用avatarUrl如果没有则使用avatarRes
if (avatarUrl != null && !avatarUrl.isEmpty()) {
com.bumptech.glide.Glide.with(this)
.load(avatarUrl)
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.circleCrop()
.into(binding.avatar);
} else {
binding.avatar.setImageResource(avatarRes);
}
// 设置头像点击放大功能
final String finalAvatarUrl = avatarUrl;
final int finalAvatarRes = avatarRes;
binding.avatar.setOnClickListener(v -> {
AvatarViewerDialog dialog = AvatarViewerDialog.create(this);
dialog.setAvatarResId(avatarRes);
if (finalAvatarUrl != null && !finalAvatarUrl.isEmpty()) {
dialog.setAvatarUrl(finalAvatarUrl);
} else {
dialog.setAvatarResId(finalAvatarRes);
}
dialog.show();
});
@ -81,8 +115,14 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
// 检查关注状态
checkFollowStatus();
// 关注/取消关注按钮点击事件
binding.addFriendButton.setOnClickListener(v -> {
// 检查好友状态
checkFriendStatus();
// 关注按钮点击事件
binding.followButton.setOnClickListener(v -> {
if (!AuthHelper.requireLogin(this, "关注需要登录")) {
return;
}
if (TextUtils.isEmpty(currentUserId)) {
Toast.makeText(this, "用户信息缺失", Toast.LENGTH_SHORT).show();
return;
@ -94,6 +134,26 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
followUser();
}
});
// 加好友按钮点击事件
binding.addFriendButton.setOnClickListener(v -> {
if (!AuthHelper.requireLogin(this, "加好友需要登录")) {
return;
}
if (TextUtils.isEmpty(currentUserId)) {
Toast.makeText(this, "用户信息缺失", Toast.LENGTH_SHORT).show();
return;
}
if (isFriend) {
// 已是好友跳转到私聊
openChat();
} else if (hasPendingRequest) {
Toast.makeText(this, "已发送好友请求,请等待对方处理", Toast.LENGTH_SHORT).show();
} else {
sendFriendRequest();
}
});
}
private void checkFollowStatus() {
@ -216,14 +276,178 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
private void updateFollowButton() {
if (binding == null) return;
if (isFollowing) {
binding.addFriendButton.setText("已关注");
binding.followButton.setText("已关注");
binding.followButton.setSelected(true);
binding.followButton.setTextColor(getResources().getColor(android.R.color.darker_gray, null));
} else {
binding.followButton.setText("关注");
binding.followButton.setSelected(false);
binding.followButton.setTextColor(getResources().getColor(R.color.red_primary, null));
}
}
/**
* 检查好友状态
*/
private void checkFriendStatus() {
if (TextUtils.isEmpty(currentUserId)) return;
String token = AuthStore.getToken(this);
if (token == null) return;
try {
int userId = Integer.parseInt(currentUserId);
ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = apiService.checkFriendStatus(userId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Map<String, Object> data = apiResponse.getData();
Boolean friend = (Boolean) data.get("isFriend");
Boolean pending = (Boolean) data.get("hasPendingRequest");
isFriend = friend != null && friend;
hasPendingRequest = pending != null && pending;
updateAddFriendButton();
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 忽略错误使用默认状态
}
});
} catch (NumberFormatException e) {
// 忽略错误
}
}
/**
* 发送好友请求
*/
private void sendFriendRequest() {
if (TextUtils.isEmpty(currentUserId)) return;
try {
int userId = Integer.parseInt(currentUserId);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("targetUserId", userId);
requestBody.put("message", "我想加你为好友");
ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Boolean>> call = apiService.sendFriendRequest(requestBody);
call.enqueue(new Callback<ApiResponse<Boolean>>() {
@Override
public void onResponse(Call<ApiResponse<Boolean>> call,
Response<ApiResponse<Boolean>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Boolean> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
hasPendingRequest = true;
updateAddFriendButton();
Toast.makeText(UserProfileReadOnlyActivity.this, "好友请求已发送", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(UserProfileReadOnlyActivity.this,
apiResponse.getMessage() != null ? apiResponse.getMessage() : "发送失败",
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(UserProfileReadOnlyActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Boolean>> call, Throwable t) {
Toast.makeText(UserProfileReadOnlyActivity.this, "网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
} catch (NumberFormatException e) {
Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show();
}
}
/**
* 更新加好友按钮状态
*/
private void updateAddFriendButton() {
if (binding == null) return;
if (isFriend) {
binding.addFriendButton.setText("发消息");
binding.addFriendButton.setBackgroundResource(R.drawable.bg_purple_999);
} else if (hasPendingRequest) {
binding.addFriendButton.setText("已申请");
binding.addFriendButton.setAlpha(0.7f);
} else {
binding.addFriendButton.setText("关注");
binding.addFriendButton.setText("加好友");
binding.addFriendButton.setAlpha(1f);
}
}
/**
* 打开私聊
*/
private void openChat() {
if (TextUtils.isEmpty(currentUserId)) return;
String friendName = currentUserName != null ? currentUserName : "好友";
// 先获取或创建与好友的会话再打开私聊页面
String token = com.example.livestreaming.net.AuthStore.getToken(this);
if (token == null) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/with/" + currentUserId;
okhttp3.OkHttpClient httpClient = new okhttp3.OkHttpClient();
okhttp3.Request request = new okhttp3.Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.post(okhttp3.RequestBody.create("", okhttp3.MediaType.parse("application/json")))
.build();
httpClient.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, java.io.IOException e) {
runOnUiThread(() -> {
Toast.makeText(UserProfileReadOnlyActivity.this, "打开会话失败", Toast.LENGTH_SHORT).show();
});
}
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws java.io.IOException {
String body = response.body() != null ? response.body().string() : "";
runOnUiThread(() -> {
try {
org.json.JSONObject json = new org.json.JSONObject(body);
if (json.optInt("code", -1) == 200) {
org.json.JSONObject data = json.optJSONObject("data");
String conversationId = data != null ? data.optString("conversationId", "") : "";
if (!conversationId.isEmpty()) {
ConversationActivity.start(UserProfileReadOnlyActivity.this, conversationId, friendName);
} else {
Toast.makeText(UserProfileReadOnlyActivity.this, "获取会话ID失败", Toast.LENGTH_SHORT).show();
}
} else {
String msg = json.optString("message", "打开会话失败");
Toast.makeText(UserProfileReadOnlyActivity.this, msg, Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Toast.makeText(UserProfileReadOnlyActivity.this, "打开会话失败", Toast.LENGTH_SHORT).show();
}
});
}
});
}
private void setupTabsAndWorks() {
if (binding == null) return;

View File

@ -1,18 +1,33 @@
package com.example.livestreaming;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ActivityVoiceMatchBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
/**
* 语音匹配页面 - 通过语音快速匹配志同道合的朋友
*/
public class VoiceMatchActivity extends AppCompatActivity {
private ActivityVoiceMatchBinding binding;
private Handler handler = new Handler(Looper.getMainLooper());
private AnimatorSet pulseAnimator;
private boolean isMatching = false;
private Runnable matchingRunnable;
public static void start(Context context) {
Intent intent = new Intent(context, VoiceMatchActivity.class);
@ -26,15 +41,161 @@ public class VoiceMatchActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupUI();
loadUserAvatar();
}
private void setupUI() {
binding.backButton.setOnClickListener(v -> finish());
// 开始匹配按钮
binding.btnStartMatch.setOnClickListener(v -> startMatching());
// 取消匹配按钮
binding.btnCancelMatch.setOnClickListener(v -> cancelMatching());
setupBottomNav();
}
private void loadUserAvatar() {
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
String avatarUrl = prefs.getString("profile_avatar", "");
if (!avatarUrl.isEmpty()) {
Glide.with(this)
.load(avatarUrl)
.placeholder(R.drawable.wish_tree_checker_backup)
.error(R.drawable.wish_tree_checker_backup)
.circleCrop()
.into(binding.userAvatar);
}
}
private void startMatching() {
if (isMatching) return;
isMatching = true;
// 更新UI
binding.btnStartMatch.setVisibility(View.GONE);
binding.btnCancelMatch.setVisibility(View.VISIBLE);
binding.statusText.setText("正在匹配中...");
binding.matchingHint.setVisibility(View.VISIBLE);
// 开始脉冲动画
startPulseAnimation();
// 模拟匹配过程
matchingRunnable = () -> {
if (isMatching) {
// 模拟匹配成功
onMatchSuccess();
}
};
handler.postDelayed(matchingRunnable, 5000 + (long) (Math.random() * 5000));
}
private void cancelMatching() {
if (!isMatching) return;
isMatching = false;
// 取消匹配任务
if (matchingRunnable != null) {
handler.removeCallbacks(matchingRunnable);
}
// 停止动画
stopPulseAnimation();
// 更新UI
binding.btnStartMatch.setVisibility(View.VISIBLE);
binding.btnCancelMatch.setVisibility(View.GONE);
binding.statusText.setText("点击开始匹配");
binding.matchingHint.setVisibility(View.GONE);
}
private void onMatchSuccess() {
isMatching = false;
stopPulseAnimation();
// 更新UI
binding.statusText.setText("匹配成功!");
binding.matchingHint.setText("正在连接...");
// 显示匹配成功对话框
handler.postDelayed(() -> {
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("🎉 匹配成功")
.setMessage("已为你匹配到一位小伙伴!\n是否开始语音通话")
.setPositiveButton("开始通话", (dialog, which) -> {
// TODO: 开始语音通话
Toast.makeText(this, "语音通话功能开发中", Toast.LENGTH_SHORT).show();
resetUI();
})
.setNegativeButton("继续匹配", (dialog, which) -> {
resetUI();
startMatching();
})
.setOnCancelListener(dialog -> resetUI())
.show();
}, 1000);
}
private void resetUI() {
binding.btnStartMatch.setVisibility(View.VISIBLE);
binding.btnCancelMatch.setVisibility(View.GONE);
binding.statusText.setText("点击开始匹配");
binding.matchingHint.setVisibility(View.GONE);
}
private void startPulseAnimation() {
// 外圈脉冲动画
ObjectAnimator scaleX1 = ObjectAnimator.ofFloat(binding.pulseRing1, "scaleX", 1f, 1.3f);
ObjectAnimator scaleY1 = ObjectAnimator.ofFloat(binding.pulseRing1, "scaleY", 1f, 1.3f);
ObjectAnimator alpha1 = ObjectAnimator.ofFloat(binding.pulseRing1, "alpha", 1f, 0f);
ObjectAnimator scaleX2 = ObjectAnimator.ofFloat(binding.pulseRing2, "scaleX", 1f, 1.5f);
ObjectAnimator scaleY2 = ObjectAnimator.ofFloat(binding.pulseRing2, "scaleY", 1f, 1.5f);
ObjectAnimator alpha2 = ObjectAnimator.ofFloat(binding.pulseRing2, "alpha", 1f, 0f);
pulseAnimator = new AnimatorSet();
pulseAnimator.playTogether(scaleX1, scaleY1, alpha1, scaleX2, scaleY2, alpha2);
pulseAnimator.setDuration(1500);
pulseAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
pulseAnimator.addListener(new android.animation.AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(android.animation.Animator animation) {
if (isMatching) {
// 重置并重新开始
binding.pulseRing1.setScaleX(1f);
binding.pulseRing1.setScaleY(1f);
binding.pulseRing1.setAlpha(1f);
binding.pulseRing2.setScaleX(1f);
binding.pulseRing2.setScaleY(1f);
binding.pulseRing2.setAlpha(1f);
pulseAnimator.start();
}
}
});
pulseAnimator.start();
}
private void stopPulseAnimation() {
if (pulseAnimator != null) {
pulseAnimator.cancel();
}
// 重置状态
binding.pulseRing1.setScaleX(1f);
binding.pulseRing1.setScaleY(1f);
binding.pulseRing1.setAlpha(1f);
binding.pulseRing2.setScaleX(1f);
binding.pulseRing2.setScaleY(1f);
binding.pulseRing2.setAlpha(1f);
}
private void setupBottomNav() {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation);
bottomNavigation.setOnItemSelectedListener(item -> {
@ -44,6 +205,11 @@ public class VoiceMatchActivity extends AppCompatActivity {
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
finish();
@ -69,9 +235,26 @@ public class VoiceMatchActivity extends AppCompatActivity {
if (binding != null) {
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
bottomNav.setSelectedItemId(R.id.nav_friends);
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNav);
}
}
@Override
protected void onPause() {
super.onPause();
// 离开页面时取消匹配
if (isMatching) {
cancelMatching();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (matchingRunnable != null) {
handler.removeCallbacks(matchingRunnable);
}
stopPulseAnimation();
}
}

View File

@ -71,6 +71,12 @@ public class WishTreeActivity extends AppCompatActivity {
apiService = ApiClient.getService(this);
// 打印当前API地址和登录状态
String baseUrl = com.example.livestreaming.net.ApiClient.getCurrentBaseUrl(this);
String token = AuthStore.getToken(this);
android.util.Log.d("WishTree", "API地址: " + baseUrl);
android.util.Log.d("WishTree", "Token: " + (token != null && !token.isEmpty() ? "已登录" : "未登录"));
startBannerCountdown();
setupBottomNav();
setupWishCards();
@ -144,20 +150,41 @@ public class WishTreeActivity extends AppCompatActivity {
* 从服务端加载我的心愿
*/
private void loadMyWishes() {
android.util.Log.d("WishTree", "开始加载我的心愿列表");
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();
android.util.Log.d("WishTree", "加载心愿响应: code=" + response.code());
if (response.isSuccessful() && response.body() != null) {
android.util.Log.d("WishTree", "响应body: code=" + response.body().getCode());
if (response.body().getData() != null) {
myWishes = response.body().getData().list;
if (myWishes == null) myWishes = new ArrayList<>();
android.util.Log.d("WishTree", "加载到 " + myWishes.size() + " 条心愿");
updateWishCards();
updateWishCount();
} else {
android.util.Log.w("WishTree", "响应数据为空");
myWishes = new ArrayList<>();
updateWishCards();
updateWishCount();
}
} else {
android.util.Log.e("WishTree", "加载心愿失败: " + response.code());
try {
if (response.errorBody() != null) {
android.util.Log.e("WishTree", "错误响应: " + response.errorBody().string());
}
} catch (Exception e) {
android.util.Log.e("WishTree", "读取错误响应失败", e);
}
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call, @NonNull Throwable t) {
android.util.Log.e("WishTree", "加载心愿网络错误", t);
Toast.makeText(WishTreeActivity.this, "加载心愿失败", Toast.LENGTH_SHORT).show();
}
});
@ -271,31 +298,64 @@ public class WishTreeActivity extends AppCompatActivity {
* 发布心愿到服务端
*/
private void publishWish(String content, Dialog dialog) {
// 检查登录状态
String token = AuthStore.getToken(this);
if (token == null || token.isEmpty()) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, LoginActivity.class));
return;
}
Integer festivalId = currentFestival != null ? currentFestival.id : null;
WishtreeRequest.PublishWish request = new WishtreeRequest.PublishWish(festivalId, content, null);
android.util.Log.d("WishTree", "发布心愿请求: festivalId=" + festivalId + ", content=" + content);
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) {
android.util.Log.d("WishTree", "发布心愿响应: code=" + response.code());
if (response.isSuccessful() && response.body() != null) {
if (response.body().getCode() == 200) {
android.util.Log.d("WishTree", "响应body: code=" + response.body().getCode() + ", msg=" + response.body().getMessage());
if (response.body().isOk()) {
dialog.dismiss();
showSuccessDialog();
loadMyWishes(); // 重新加载心愿列表
} else {
String msg = response.body().getMessage();
Toast.makeText(WishTreeActivity.this,
response.body().getMessage() != null ? response.body().getMessage() : "发布失败",
msg != null && !msg.isEmpty() ? msg : "发布失败",
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(WishTreeActivity.this, "发布失败", Toast.LENGTH_SHORT).show();
// 尝试读取错误响应
String errorMsg = "发布失败";
try {
if (response.errorBody() != null) {
String errorBody = response.errorBody().string();
android.util.Log.e("WishTree", "错误响应: " + errorBody);
errorMsg = "发布失败: " + response.code();
}
} catch (Exception e) {
android.util.Log.e("WishTree", "读取错误响应失败", e);
}
Toast.makeText(WishTreeActivity.this, errorMsg, 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();
android.util.Log.e("WishTree", "发布心愿网络错误", t);
String errorMsg = "网络错误";
if (t instanceof java.net.UnknownHostException) {
errorMsg = "无法连接服务器,请检查网络";
} else if (t instanceof java.net.SocketTimeoutException) {
errorMsg = "连接超时,请重试";
} else if (t.getMessage() != null) {
errorMsg = "网络错误: " + t.getMessage();
}
Toast.makeText(WishTreeActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
}
});
}
@ -387,7 +447,7 @@ public class WishTreeActivity extends AppCompatActivity {
@Override
public void onResponse(@NonNull Call<ApiResponse<String>> call,
@NonNull Response<ApiResponse<String>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getCode() == 200) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
dialog.dismiss();
if (isComplete) {
showCompleteSuccessDialog();

View File

@ -152,6 +152,12 @@ public interface ApiService {
@Path("requestId") long requestId,
@Body Map<String, Object> body);
/**
* 检查好友状态是否是好友是否有待处理的好友请求
*/
@GET("api/front/friends/check/{userId}")
Call<ApiResponse<Map<String, Object>>> checkFriendStatus(@Path("userId") int userId);
// ==================== 文件上传 ====================
@Multipart
@ -510,4 +516,84 @@ public interface ApiService {
*/
@GET("api/front/streamer/applications")
Call<ApiResponse<List<Map<String, Object>>>> getStreamerApplications();
// ==================== 社区/缘池接口 ====================
/**
* 获取社区板块列表
*/
@GET("api/front/community/categories")
Call<ApiResponse<List<CommunityResponse.Category>>> getCommunityCategories();
/**
* 获取社区消息列表
*/
@GET("api/front/community/messages")
Call<ApiResponse<CommunityResponse.MessagePage>> getCommunityMessages(
@Query("categoryId") int categoryId,
@Query("page") int page,
@Query("limit") int limit);
/**
* 发布社区消息
*/
@POST("api/front/community/messages")
Call<ApiResponse<CommunityResponse.Message>> publishCommunityMessage(@Body Map<String, Object> body);
/**
* 点赞社区消息
*/
@POST("api/front/community/messages/{id}/like")
Call<ApiResponse<String>> likeCommunityMessage(@Path("id") long id);
/**
* 取消点赞社区消息
*/
@DELETE("api/front/community/messages/{id}/like")
Call<ApiResponse<String>> unlikeCommunityMessage(@Path("id") long id);
/**
* 获取附近用户缘池功能
*/
@GET("api/front/community/nearby-users")
Call<ApiResponse<CommunityResponse.NearbyUserList>> getNearbyUsers(
@Query("latitude") double latitude,
@Query("longitude") double longitude,
@Query("radius") int radius,
@Query("limit") int limit);
/**
* 获取可匹配用户总数
*/
@GET("api/front/community/user-count")
Call<ApiResponse<Integer>> getMatchableUserCount();
// ==================== 心动信号/匹配接口 ====================
/**
* 获取推荐匹配用户列表
*/
@GET("api/front/match/recommend")
Call<ApiResponse<List<CommunityResponse.MatchUser>>> getRecommendUsers(
@Query("limit") int limit);
/**
* 喜欢用户右滑
*/
@POST("api/front/match/like")
Call<ApiResponse<CommunityResponse.MatchResult>> likeUser(@Body Map<String, Object> body);
/**
* 跳过用户左滑
*/
@POST("api/front/match/skip")
Call<ApiResponse<String>> skipUser(@Body Map<String, Object> body);
/**
* 获取匹配列表互相喜欢的用户
*/
@GET("api/front/match/list")
Call<ApiResponse<List<CommunityResponse.MatchUser>>> getMatchList(
@Query("page") int page,
@Query("limit") int limit);
}

View File

@ -0,0 +1,150 @@
package com.example.livestreaming.net;
import java.util.List;
/**
* 社区相关响应数据类
*/
public class CommunityResponse {
/**
* 板块/分类
*/
public static class Category {
public int id;
public String name;
public String icon;
public String type; // quick=快捷入口 card=功能卡片
public String jumpPage; // 跳转页面
public String description;
public int sort;
public int status;
public int getId() { return id; }
public String getName() { return name; }
public String getIcon() { return icon; }
public String getType() { return type; }
public String getJumpPage() { return jumpPage; }
}
/**
* 消息/帖子
*/
public static class Message {
public long id;
public int userId;
public String nickname;
public String avatar;
public int categoryId;
public String categoryName;
public String content;
public String images; // 图片URL逗号分隔的字符串
public int likeCount;
public int commentCount;
public int viewCount;
public int status;
public String createTime;
public String updateTime;
public boolean isLiked; // 当前用户是否已点赞
/**
* 获取图片列表
*/
public List<String> getImageList() {
List<String> list = new java.util.ArrayList<>();
if (images != null && !images.isEmpty()) {
String[] arr = images.split(",");
for (String url : arr) {
String trimmed = url.trim();
if (!trimmed.isEmpty()) {
list.add(trimmed);
}
}
}
return list;
}
}
/**
* 消息分页
*/
public static class MessagePage {
public List<Message> list;
public int total;
public int page;
public int limit;
}
/**
* 附近用户
*/
public static class NearbyUser {
public int id;
public String nickname;
public String avatar;
public String gender;
public int age;
public String location;
public String bio;
public double distance; // 距离
public boolean isOnline;
public String lastActiveTime;
public int getUserId() {
return id;
}
public String getNickname() {
return nickname;
}
public String getAvatar() {
return avatar;
}
public String getLocation() {
return location;
}
public boolean isOnline() {
return isOnline;
}
}
/**
* 附近用户列表
*/
public static class NearbyUserList {
public List<NearbyUser> list;
public int total;
public List<NearbyUser> getUsers() {
return list;
}
}
/**
* 匹配用户心动信号
*/
public static class MatchUser {
public int id;
public String nickname;
public String avatar;
public String gender;
public int age;
public String location;
public String bio;
public List<String> photos; // 用户照片列表
public List<String> tags; // 用户标签
public double matchScore; // 匹配度
}
/**
* 匹配结果
*/
public static class MatchResult {
public boolean isMatch; // 是否互相喜欢
public MatchUser matchedUser;
public long conversationId; // 如果匹配成功返回会话ID
}
}

View File

@ -10,6 +10,18 @@ public class UserEditRequest {
@SerializedName("avatar")
private String avatar;
@SerializedName("birthday")
private String birthday;
@SerializedName("sex")
private Integer sex; // 0=未知, 1=, 2=, 3=保密
@SerializedName("addres")
private String addres; // 地址
@SerializedName("mark")
private String mark; // 个人签名/备注
public UserEditRequest() {}
public UserEditRequest(String nickname, String avatar) {
@ -19,6 +31,15 @@ public class UserEditRequest {
public void setNickname(String nickname) { this.nickname = nickname; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public void setBirthday(String birthday) { this.birthday = birthday; }
public void setSex(Integer sex) { this.sex = sex; }
public void setAddres(String addres) { this.addres = addres; }
public void setMark(String mark) { this.mark = mark; }
public String getNickname() { return nickname; }
public String getAvatar() { return avatar; }
public String getBirthday() { return birthday; }
public Integer getSex() { return sex; }
public String getAddres() { return addres; }
public String getMark() { return mark; }
}

View File

@ -53,6 +53,18 @@ public class UserInfoResponse {
@SerializedName("collectCount")
private Integer collectCount;
@SerializedName("mark")
private String mark; // 个性签名
@SerializedName("addres")
private String addres; // 地址
@SerializedName("birthday")
private String birthday; // 生日
@SerializedName("sex")
private Integer sex; // 性别0=未知1=2=3=保密
public Integer getUid() { return uid; }
public String getNickname() { return nickname; }
public String getAvatar() { return avatar; }
@ -69,4 +81,8 @@ public class UserInfoResponse {
public String getVipName() { return vipName; }
public Boolean getRechargeSwitch() { return rechargeSwitch; }
public Integer getCollectCount() { return collectCount; }
public String getMark() { return mark; }
public String getAddres() { return addres; }
public String getBirthday() { return birthday; }
public Integer getSex() { return sex; }
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@android:color/transparent" />
<!-- 使用淡紫色背景,与页面主题协调 -->
<solid android:color="#E8D5F0" />
</shape>

View File

@ -0,0 +1,9 @@
<?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="14dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 已关注状态 -->
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="22dp" />
<stroke android:width="1dp" android:color="#DDDDDD" />
</shape>
</item>
<!-- 未关注状态 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="22dp" />
<stroke android:width="1dp" android:color="#FF4757" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="#99000000"
android:startColor="#00000000" />
</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="#FF4757" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="#55FFFFFF" />
<solid android:color="#00000000" />
</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="#FFECF0" />
<corners android:radius="12dp" />
</shape>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆形渐变背景 -->
<item>
<shape android:shape="oval">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#63B3ED"
android:endColor="#4299E1" />
</shape>
</item>
<!-- 用户图标(小圆点代表头部) -->
<item
android:top="16dp"
android:bottom="28dp"
android:left="24dp"
android:right="24dp">
<shape android:shape="oval">
<solid android:color="#FFFFFF" />
</shape>
</item>
<!-- 用户图标(半圆代表身体) -->
<item
android:top="36dp"
android:bottom="12dp"
android:left="16dp"
android:right="16dp">
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:topLeftRadius="20dp" android:topRightRadius="20dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆形渐变背景 -->
<item>
<shape android:shape="oval">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#F687B3"
android:endColor="#ED64A6" />
</shape>
</item>
<!-- 用户图标(小圆点代表头部) -->
<item
android:top="16dp"
android:bottom="28dp"
android:left="24dp"
android:right="24dp">
<shape android:shape="oval">
<solid android:color="#FFFFFF" />
</shape>
</item>
<!-- 用户图标(半圆代表身体) -->
<item
android:top="36dp"
android:bottom="12dp"
android:left="16dp"
android:right="16dp">
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:topLeftRadius="20dp" android:topRightRadius="20dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆形渐变背景 -->
<item>
<shape android:shape="oval">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#B794F6"
android:endColor="#9F7AEA" />
</shape>
</item>
<!-- 用户图标 -->
<item
android:top="18dp"
android:bottom="18dp"
android:left="18dp"
android:right="18dp">
<shape android:shape="oval">
<solid android:color="#FFFFFF" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#20B2AA"
android:pathData="M4,4h4v4H4V4zM10,4h4v4h-4V4zM16,4h4v4h-4V4zM4,10h4v4H4v-4zM10,10h4v4h-4v-4zM16,10h4v4h-4v-4zM4,16h4v4H4v-4zM10,16h4v4h-4v-4zM16,16h4v4h-4v-4z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00CED1"
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2V7h-2v2z"/>
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<stroke
android:width="4dp"
android:color="#4CAF50" />
<corners android:radius="8dp" />
</shape>
</item>
<item
android:bottom="30dp"
android:left="10dp"
android:right="10dp"
android:top="30dp">
<shape android:shape="rectangle">
<solid android:color="#4CAF50" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#999999">
<path
android:fillColor="#FF000000"
android:pathData="M21,6h-7.59l3.29,-3.29L16,2l-4,4 -4,-4 -0.71,0.71L10.59,6H3c-1.1,0 -2,0.89 -2,2v12c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V8c0,-1.11 -0.9,-2 -2,-2zM21,20H3V8h18v12zM9,10v8l7,-4z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8H8.9V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#9C27B0"
android:pathData="M20,2H4C2.9,2 2.01,2.9 2.01,4L2,22l4,-4h14c1.1,0 2,-0.9 2,-2V4C22,2.9 21.1,2 20,2zM6,9h12v2H6V9zM14,14H6v-2h8V14zM18,8H6V6h12V8z" />
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<stroke
android:width="4dp"
android:color="#F44336" />
<corners android:radius="8dp" />
</shape>
</item>
<item
android:bottom="30dp"
android:left="10dp"
android:right="10dp"
android:top="30dp">
<shape android:shape="rectangle">
<solid android:color="#F44336" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF6347"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF8C00"
android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFD700"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 外圈 -->
<path
android:fillColor="#FF8C00"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
<!-- 中圈 -->
<path
android:fillColor="#FF8C00"
android:pathData="M12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z" />
<!-- 中心点圆形用path表示 -->
<path
android:fillColor="#FF8C00"
android:pathData="M12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4757"
android:pathData="M1,21h4V9H1v12zM23,10c0,-1.1 -0.9,-2 -2,-2h-6.31l0.95,-4.57 0.03,-0.32c0,-0.41 -0.17,-0.79 -0.44,-1.06L14.17,1 7.59,7.59C7.22,7.95 7,8.45 7,9v10c0,1.1 0.9,2 2,2h9c0.83,0 1.54,-0.5 1.84,-1.22l3.02,-7.05c0.09,-0.23 0.14,-0.47 0.14,-0.73v-2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF69B4"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4757"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<!-- 顶部栏 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="0dp"
android:layout_height="56dp"
android:background="@color/white"
android:elevation="2dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/backButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="返回"
android:src="@drawable/ic_arrow_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="#333333" />
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="动态社区"
android:textColor="#111111"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 下拉刷新 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="80dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 空状态 -->
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:alpha="0.5"
android:src="@drawable/ic_message_24"
app:tint="#CCCCCC" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无动态"
android:textColor="#999999"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="快来发布第一条动态吧"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
<!-- 发布按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabPublish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="80dp"
android:contentDescription="发布"
android:src="@drawable/ic_add_24"
app:backgroundTint="@color/purple_500"
app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
<!-- 底部导航 -->
<include
android:id="@+id/bottomNavInclude"
layout="@layout/include_bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -98,6 +98,7 @@
android:id="@+id/orbitRing"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="36dp"
android:background="@drawable/bg_orbit_ring"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -483,83 +484,29 @@
android:layout_height="wrap_content"
android:padding="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/actionLeft"
<!-- 动态加载的板块RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/categoryRecyclerView"
android:layout_width="0dp"
android:layout_height="44dp"
android:background="@drawable/bg_pill_left"
android:clickable="true"
android:focusable="true"
android:paddingStart="14dp"
android:paddingEnd="14dp"
app:layout_constraintEnd_toStartOf="@id/actionRight"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/actionLeftIcon"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_voice_24"
android:tint="#C218F0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/actionLeftText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="语音 ~ 匹配 ~"
android:textColor="#C218F0"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/actionLeftIcon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/actionRight"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginStart="12dp"
android:background="@drawable/bg_pill_right"
android:clickable="true"
android:focusable="true"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/actionLeft"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/actionRightIcon"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_heart_24"
android:tint="#FF2D77"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/actionRightText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="心动信号 ~"
android:textColor="#FF2D77"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/actionRightIcon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 加载中提示 -->
<ProgressBar
android:id="@+id/categoryLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 保留旧的GridLayout作为备用隐藏 -->
<GridLayout
android:id="@+id/grid"
android:layout_width="0dp"
@ -567,9 +514,10 @@
android:layout_marginTop="10dp"
android:columnCount="3"
android:rowCount="2"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionLeft">
app:layout_constraintTop_toBottomOf="@id/categoryRecyclerView">
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"

View File

@ -27,14 +27,15 @@
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backButton" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="32dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="发现让你心动的那个TA"
android:textColor="#666666"
@ -43,6 +44,122 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText" />
<!-- 卡片容器 -->
<FrameLayout
android:id="@+id/cardContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@id/actionButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleText" />
<!-- 空状态 -->
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/actionButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleText">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:alpha="0.5"
android:src="@drawable/ic_heart_24"
app:tint="#FFCCCC" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无更多推荐"
android:textColor="#999999"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="刷新推荐"
app:backgroundTint="#FF4081" />
</LinearLayout>
<!-- 加载中 -->
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/actionButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleText" />
<!-- 操作按钮 -->
<LinearLayout
android:id="@+id/actionButtons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- 跳过按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="跳过"
android:src="@drawable/ic_close_24"
app:backgroundTint="#FFFFFF"
app:elevation="4dp"
app:fabSize="normal"
app:tint="#F44336" />
<!-- 超级喜欢按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnSuperLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="超级喜欢"
android:src="@drawable/ic_star_24"
app:backgroundTint="#FFFFFF"
app:elevation="4dp"
app:fabSize="mini"
app:tint="#2196F3" />
<!-- 喜欢按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="喜欢"
android:src="@drawable/ic_heart_24"
app:backgroundTint="#FFFFFF"
app:elevation="4dp"
app:fabSize="normal"
app:tint="#4CAF50" />
</LinearLayout>
<include
android:id="@+id/bottomNavInclude"
layout="@layout/include_bottom_nav"

View File

@ -147,80 +147,127 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/categoryTabs"
<!-- 分类标签容器 - 包含标签和展开箭头 -->
<LinearLayout
android:id="@+id/categoryTabsContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="gone"
app:tabIndicatorColor="#FF4757"
app:tabIndicatorFullWidth="false"
app:tabMode="scrollable"
app:tabSelectedTextColor="#333333"
app:tabTextColor="#999999"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/searchContainer">
app:layout_constraintTop_toBottomOf="@id/topTabs">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
<!-- 分类标签 - 显示5个标签 -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/categoryTabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="推荐" />
android:layout_weight="1"
android:paddingStart="8dp"
app:tabIndicatorColor="#FF4757"
app:tabIndicatorFullWidth="false"
app:tabMode="scrollable"
app:tabGravity="start"
app:tabSelectedTextColor="#333333"
app:tabTextColor="#999999">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="游戏" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="推荐" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="才艺" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="直播" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="户外" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="视频" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="音乐" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="音乐" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="美食" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="游戏" />
</com.google.android.material.tabs.TabLayout>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="聊天" />
</com.google.android.material.tabs.TabLayout>
<!-- 展开箭头按钮 -->
<ImageView
android:id="@+id/btnExpandCategories"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:padding="8dp"
android:src="@drawable/ic_expand_more_24"
android:contentDescription="展开更多分类"
app:tint="#666666" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
<!-- Tab内容容器 -->
<FrameLayout
android:id="@+id/tabContentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomsRecyclerView"
<!-- 关注Tab内容 -->
<include
android:id="@+id/followTabContent"
layout="@layout/layout_follow_tab"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="88dp" />
android:visibility="gone" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 发现Tab内容 -->
<include
android:id="@+id/discoverTabContent"
layout="@layout/layout_discover_tab"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<!-- 附近Tab内容 -->
<include
android:id="@+id/nearbyTabContent"
layout="@layout/layout_nearby_tab"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<!-- 原有的下拉刷新和列表(发现页面使用) -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="88dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
<ProgressBar
android:id="@+id/loading"
@ -238,6 +285,23 @@
android:visibility="gone"
/>
<!-- 发布按钮 - 红色白十字,小圆角长方形 -->
<TextView
android:id="@+id/btnPublish"
android:layout_width="56dp"
android:layout_height="36dp"
android:layout_gravity="bottom|start"
android:layout_marginStart="16dp"
android:layout_marginBottom="96dp"
android:background="@drawable/bg_publish_button"
android:gravity="center"
android:text=""
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold"
android:visibility="gone"
android:elevation="4dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddLive"
android:layout_width="56dp"

View File

@ -70,8 +70,9 @@
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toStartOf="@id/topActionClock"
app:layout_constraintStart_toEndOf="@id/avatarRing"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name">
<TextView
@ -137,12 +138,11 @@
android:id="@+id/idRow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/topActionClock"
app:layout_constraintStart_toEndOf="@id/avatarRing"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/chipsRow">
<ImageView
@ -231,7 +231,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="12\n关注"
android:text="0\n关注"
android:textColor="#111111"
android:textStyle="bold" />
@ -269,81 +269,89 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/statsRow" />
<LinearLayout
android:id="@+id/tagRow"
<HorizontalScrollView
android:id="@+id/tagScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bioText">
<TextView
android:id="@+id/tagLocation"
<LinearLayout
android:id="@+id/tagRow"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="IP广西"
android:textColor="#666666"
android:textSize="11sp" />
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tagGender"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="H"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagLocation"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="IP广西"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagAge"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="25岁"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagGender"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="男"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagConstellation"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="金牛座"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagAge"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="25岁"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagPersonality"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="性格测试"
android:textColor="#666666"
android:textSize="11sp" />
<TextView
android:id="@+id/tagConstellation"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="金牛座"
android:textColor="#666666"
android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/tagPersonality"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="性格测试"
android:textColor="#666666"
android:textSize="11sp" />
</LinearLayout>
</HorizontalScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/quickActions"
@ -352,169 +360,162 @@
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tagRow">
app:layout_constraintTop_toBottomOf="@id/tagScrollView">
<LinearLayout
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:scrollbars="none">
<LinearLayout
android:id="@+id/action1"
android:layout_width="0dp"
android:layout_height="72dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<LinearLayout
android:id="@+id/action1"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_crosshair_24" />
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
</FrameLayout>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_crosshair_24" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:orientation="vertical">
</FrameLayout>
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="公园勋章"
android:textColor="#111111"
android:textSize="15sp"
android:textStyle="bold" />
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="公园勋章"
android:textColor="#111111"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="暂无勋章"
android:textColor="#999999"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/action2"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_grid_24" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:text="暂无勋章"
android:textColor="#999999"
android:textSize="12sp" />
android:layout_marginStart="8dp"
android:orientation="vertical">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="观看历史"
android:textColor="#111111"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="看过的作品"
android:textColor="#999999"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/action3"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_heart_24" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的挚友"
android:textColor="#111111"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/friendsCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="0人"
android:textColor="#999999"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/action2"
android:layout_width="0dp"
android:layout_height="72dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_grid_24" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="观看历史"
android:textColor="#111111"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:text="看过的作品等"
android:textColor="#999999"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/action3"
android:layout_width="0dp"
android:layout_height="72dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_heart_24" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="我的挚友"
android:textColor="#111111"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:text="0人"
android:textColor="#999999"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@ -847,18 +848,20 @@
android:layout_gravity="bottom" />
<!-- 发布作品悬浮按钮 -->
<ImageButton
<TextView
android:id="@+id/fabPublishWork"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_height="36dp"
android:layout_gravity="bottom|start"
android:layout_marginStart="16dp"
android:layout_marginBottom="100dp"
android:background="@drawable/bg_fab_publish"
android:contentDescription="发布作品"
android:src="@drawable/ic_add_24_black"
android:scaleType="center"
android:elevation="6dp"
android:stateListAnimator="@null" />
android:gravity="center"
android:text=""
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold"
android:elevation="6dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -261,20 +261,45 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bio" />
<TextView
android:id="@+id/addFriendButton"
<!-- 按钮容器:关注和加好友 -->
<LinearLayout
android:id="@+id/buttonsContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_purple_999"
android:gravity="center"
android:text="加好友"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintHeight_percent="0.055"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/space_addFriend_margin_top" />
app:layout_constraintTop_toBottomOf="@id/space_addFriend_margin_top">
<!-- 关注按钮 -->
<TextView
android:id="@+id/followButton"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:background="@drawable/bg_follow_button"
android:gravity="center"
android:text="关注"
android:textColor="#FF4757"
android:textSize="15sp"
android:textStyle="bold" />
<!-- 加好友按钮 -->
<TextView
android:id="@+id/addFriendButton"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:layout_marginStart="6dp"
android:background="@drawable/bg_purple_999"
android:gravity="center"
android:text="加好友"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<Space
android:id="@+id/space_statsRow_margin_top"
@ -282,7 +307,7 @@
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.025"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addFriendButton" />
app:layout_constraintTop_toBottomOf="@id/buttonsContainer" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/statsRow"

View File

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F7F2FF">
android:background="@drawable/bg_fishpond_gradient">
<ImageView
android:id="@+id/backButton"
@ -14,7 +14,8 @@
android:contentDescription="返回"
android:src="@drawable/ic_arrow_back_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/white" />
<TextView
android:id="@+id/titleText"
@ -22,27 +23,146 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="语音匹配"
android:textColor="#111111"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backButton" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="32dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="通过语音快速匹配志同道合的朋友"
android:textColor="#666666"
android:textColor="#CCFFFFFF"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText" />
<!-- 匹配状态区域 -->
<LinearLayout
android:id="@+id/matchingArea"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/actionArea"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleText">
<!-- 匹配动画圆圈 -->
<FrameLayout
android:layout_width="200dp"
android:layout_height="200dp">
<!-- 外圈动画 -->
<View
android:id="@+id/pulseRing1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_pulse_ring" />
<View
android:id="@+id/pulseRing2"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="center"
android:background="@drawable/bg_pulse_ring" />
<!-- 中心头像 -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/userAvatar"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:src="@drawable/wish_tree_checker_backup"
app:civ_border_color="#FFFFFF"
app:civ_border_width="3dp" />
</FrameLayout>
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="点击开始匹配"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/matchingHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="正在寻找附近的小伙伴..."
android:textColor="#AAFFFFFF"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
<!-- 操作区域 -->
<LinearLayout
android:id="@+id/actionArea"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- 开始匹配按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStartMatch"
android:layout_width="200dp"
android:layout_height="56dp"
android:text="开始匹配"
android:textColor="@color/purple_500"
android:textSize="16sp"
android:textStyle="bold"
app:backgroundTint="@color/white"
app:cornerRadius="28dp"
app:icon="@drawable/ic_voice_24"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconTint="@color/purple_500" />
<!-- 取消匹配按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelMatch"
android:layout_width="200dp"
android:layout_height="56dp"
android:text="取消匹配"
android:textColor="@color/white"
android:textSize="16sp"
android:visibility="gone"
app:backgroundTint="#33FFFFFF"
app:cornerRadius="28dp"
app:strokeColor="@color/white"
app:strokeWidth="1dp" />
<TextView
android:id="@+id/onlineCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="当前在线: 128人"
android:textColor="#AAFFFFFF"
android:textSize="12sp" />
</LinearLayout>
<include
android:id="@+id/bottomNavInclude"
layout="@layout/include_bottom_nav"

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:paddingBottom="24dp">
<!-- 顶部标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="我的频道"
android:textSize="16sp"
android:textColor="#333333"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击进入频道"
android:textSize="12sp"
android:textColor="#999999"
android:layout_marginEnd="16dp" />
<TextView
android:id="@+id/btnEditChannels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="进入编辑"
android:textSize="13sp"
android:textColor="#333333"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:background="@drawable/bg_channel_edit_button" />
<ImageView
android:id="@+id/btnCloseChannelManager"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_expand_more_24"
android:rotation="180"
android:contentDescription="收起"
app:tint="#999999" />
</LinearLayout>
<!-- 我的频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myChannelsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:background="#F0F0F0" />
<!-- 推荐频道标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="推荐频道"
android:textSize="16sp"
android:textColor="#333333"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击添加频道"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
<!-- 推荐频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recommendChannelsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:clipToPadding="false" />
</LinearLayout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题栏 -->
<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_centerInParent="true"
android:text="全部板块"
android:textColor="#333333"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/dialogCloseBtn"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:src="@drawable/ic_close_24"
android:contentDescription="关闭" />
</RelativeLayout>
<!-- 板块列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dialogCategoryRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="400dp"
android:clipToPadding="false"
android:overScrollMode="never" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
app:cardBackgroundColor="#CCF7F2FF"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeColor="#33FFFFFF"
app:strokeWidth="1dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:padding="8dp">
<ImageView
android:id="@+id/categoryIcon"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/bg_diamond_rounded"
android:padding="10dp"
android:src="@drawable/ic_grid3x3_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/categoryName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:text="板块名称"
android:textColor="#333333"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/categoryIcon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<TextView
android:id="@+id/channelName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:text="频道名"
android:textSize="14sp"
android:textColor="#333333"
android:background="@drawable/bg_channel_tag_normal" />
<!-- 固定标识前4个频道显示 -->
<ImageView
android:id="@+id/fixedIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="top|end"
android:layout_marginTop="-4dp"
android:layout_marginEnd="-4dp"
android:src="@drawable/ic_lock_12"
android:visibility="gone"
app:tint="#999999" />
<!-- 删除按钮(编辑模式显示) -->
<ImageView
android:id="@+id/deleteIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="top|end"
android:layout_marginTop="-6dp"
android:layout_marginEnd="-6dp"
android:src="@drawable/ic_close_circle_16"
android:visibility="gone" />
<!-- 添加按钮(推荐频道显示) -->
<ImageView
android:id="@+id/addIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="top|start"
android:layout_marginTop="6dp"
android:layout_marginStart="4dp"
android:src="@drawable/ic_add_12"
android:visibility="gone"
app:tint="#999999" />
</FrameLayout>

View File

@ -54,6 +54,15 @@
android:layout_marginTop="8dp"
android:textSize="14sp" />
<!-- 图片容器 -->
<LinearLayout
android:id="@+id/images_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 推荐用户列表项 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:background="?attr/selectableItemBackground">
<!-- 头像 -->
<ImageView
android:id="@+id/userAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/bg_avatar_circle"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_account_circle_24" />
<!-- 用户信息 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名"
android:textSize="15sp"
android:textColor="#333333"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<!-- 认证标识 -->
<ImageView
android:id="@+id/verifiedIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="4dp"
android:src="@drawable/ic_verified"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/userDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="用户简介"
android:textSize="12sp"
android:textColor="#999999"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- 关注按钮 -->
<TextView
android:id="@+id/btnFollow"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:paddingHorizontal="16dp"
android:gravity="center"
android:text="关注"
android:textSize="13sp"
android:textColor="#FF4757"
android:background="@drawable/bg_follow_button" />
<!-- 关闭按钮 -->
<ImageView
android:id="@+id/btnRemove"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:padding="4dp"
android:src="@drawable/ic_close_24"
app:tint="#CCCCCC" />
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More