feat: update gift system and IM features
This commit is contained in:
parent
909d1994a1
commit
b4a0eee625
|
|
@ -393,7 +393,7 @@ Authorization: Bearer <token>
|
|||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Integer | 是 | 直播间ID |
|
||||
| id | String | 是 | 直播间ID |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
|
|
@ -419,7 +419,7 @@ Authorization: Bearer <token>
|
|||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Integer | 是 | 直播间ID |
|
||||
| id | String | 是 | 直播间ID |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
|
|
@ -483,6 +483,11 @@ Authorization: Bearer <token>
|
|||
|--------|------|------|------|
|
||||
| roomId | String | 是 | 直播间ID |
|
||||
|
||||
**重要说明**:
|
||||
- 此接口路径不包含 `/front` 前缀
|
||||
- 完整路径为: `POST /api/live/online/broadcast/{roomId}`
|
||||
- 用于向WebSocket客户端广播直播间在线人数更新
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
|
|
@ -1632,6 +1637,7 @@ Authorization: Bearer <token>
|
|||
|--------|------|------|------|
|
||||
| id | Long | 是 | 作品ID |
|
||||
| title | String | 否 | 作品标题 |
|
||||
| type | String | 否 | 作品类型 |
|
||||
| description | String | 否 | 作品描述 |
|
||||
| coverUrl | String | 否 | 封面图片URL |
|
||||
| categoryId | Integer | 否 | 分类ID |
|
||||
|
|
|
|||
|
|
@ -84,7 +84,11 @@
|
|||
:preview-src-list="[scope.row.image]"
|
||||
style="width: 50px; height: 50px"
|
||||
fit="cover"
|
||||
/>
|
||||
>
|
||||
<div slot="error" class="image-slot">
|
||||
<i class="el-icon-picture-outline"></i>
|
||||
</div>
|
||||
</el-image>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="礼物名称" align="center" prop="name" />
|
||||
|
|
@ -126,7 +130,8 @@
|
|||
:total="total"
|
||||
:page.sync="queryParams.page"
|
||||
:limit.sync="queryParams.limit"
|
||||
@pagination="getList"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
|
|
@ -190,6 +195,7 @@
|
|||
|
||||
<script>
|
||||
import { giftListApi, giftAddApi, giftUpdateApi, giftDeleteApi, giftStatusApi } from '@/api/gift';
|
||||
import { fileImageApi } from '@/api/systemSetting';
|
||||
import DataPagination from '@/components/common/DataPagination';
|
||||
|
||||
export default {
|
||||
|
|
@ -240,27 +246,37 @@ export default {
|
|||
getList() {
|
||||
this.loading = true;
|
||||
giftListApi(this.queryParams).then(response => {
|
||||
const data = response.data || {};
|
||||
this.giftList = data.list || [];
|
||||
this.total = data.total || 0;
|
||||
console.log('礼物列表响应:', response);
|
||||
|
||||
// axios 拦截器已经返回了 res.data,所以 response 就是 CommonPage 对象
|
||||
// response = { list: [], total: 0, page: 1, limit: 10, totalPage: 1 }
|
||||
const list = response.list || [];
|
||||
|
||||
// 确保 status 字段是数字类型(1 或 0)
|
||||
this.giftList = list.map(item => ({
|
||||
...item,
|
||||
status: Number(item.status) // 确保是数字类型
|
||||
}));
|
||||
this.total = response.total || 0;
|
||||
|
||||
console.log('礼物列表数据:', this.giftList);
|
||||
console.log('总数:', this.total);
|
||||
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
}).catch(error => {
|
||||
console.error('获取礼物列表失败:', error);
|
||||
this.$message.error('获取礼物列表失败');
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getStatistics() {
|
||||
// 计算统计数据
|
||||
giftListApi({ page: 1, limit: 9999 }).then(response => {
|
||||
console.log('统计数据响应:', response);
|
||||
const data = response.data || {};
|
||||
const list = data.list || [];
|
||||
console.log('礼物列表:', list);
|
||||
console.log('列表长度:', list.length);
|
||||
const list = response.list || [];
|
||||
this.statistics.total = list.length;
|
||||
this.statistics.enabled = list.filter(item => item.status === true).length;
|
||||
this.statistics.disabled = list.filter(item => item.status === false).length;
|
||||
this.statistics.totalValue = list.reduce((sum, item) => sum + (parseFloat(item.diamondPrice) || 0), 0);
|
||||
console.log('统计结果:', this.statistics);
|
||||
this.statistics.enabled = list.filter(item => item.status === 1 || item.status === true).length;
|
||||
this.statistics.disabled = list.filter(item => item.status === 0 || item.status === false).length;
|
||||
this.statistics.totalValue = list.reduce((sum, item) => sum + (parseFloat(item.diamondPrice) || 0), 0).toFixed(2);
|
||||
}).catch(error => {
|
||||
console.error('获取统计数据失败:', error);
|
||||
});
|
||||
|
|
@ -369,11 +385,36 @@ export default {
|
|||
});
|
||||
},
|
||||
handleUpload(param) {
|
||||
// 这里实现图片上传逻辑
|
||||
const formData = new FormData();
|
||||
formData.append('file', param.file);
|
||||
// 调用上传接口
|
||||
this.$message.info('图片上传功能需要配置上传接口');
|
||||
formData.append('multipart', param.file); // 后端参数名是 multipart
|
||||
|
||||
// 显示上传中提示
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '上传中...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
fileImageApi(formData, { model: 'gift', pid: 0 })
|
||||
.then(response => {
|
||||
loading.close();
|
||||
console.log('上传响应:', response);
|
||||
|
||||
// 根据响应格式设置图片地址
|
||||
if (response) {
|
||||
// response 可能是 FileResultVo 对象或直接是 URL
|
||||
this.form.image = response.url || response.fileUrl || response;
|
||||
this.$message.success('上传成功');
|
||||
} else {
|
||||
this.$message.error('上传失败,返回数据格式错误');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.close();
|
||||
console.error('上传失败:', error);
|
||||
this.$message.error('上传失败: ' + (error.message || '未知错误'));
|
||||
});
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isImage = file.type.indexOf('image/') === 0;
|
||||
|
|
@ -385,6 +426,57 @@ export default {
|
|||
this.$message.error('上传图片大小不能超过 2MB!');
|
||||
}
|
||||
return isImage && isLt2M;
|
||||
},
|
||||
getImageUrl(url) {
|
||||
if (!url) {
|
||||
console.log('图片URL为空');
|
||||
return '';
|
||||
}
|
||||
|
||||
console.log('原始图片URL:', url);
|
||||
console.log('URL类型:', typeof url);
|
||||
|
||||
// 转换为字符串(防止是对象)
|
||||
let urlStr = String(url).trim();
|
||||
console.log('转换后的URL:', urlStr);
|
||||
|
||||
// 修复:如果URL被重复拼接,提取正确的路径
|
||||
// 例如:http://domain/http://domain/crmebimage/... -> crmebimage/...
|
||||
if (urlStr.indexOf('http://') > 0 || urlStr.indexOf('https://') > 0) {
|
||||
console.log('检测到重复拼接的URL,尝试修复');
|
||||
// 找到第二个 http:// 或 https:// 的位置
|
||||
const secondHttpIndex = urlStr.indexOf('http://', 1);
|
||||
const secondHttpsIndex = urlStr.indexOf('https://', 1);
|
||||
|
||||
if (secondHttpIndex > 0) {
|
||||
urlStr = urlStr.substring(secondHttpIndex);
|
||||
console.log('修复后的URL:', urlStr);
|
||||
} else if (secondHttpsIndex > 0) {
|
||||
urlStr = urlStr.substring(secondHttpsIndex);
|
||||
console.log('修复后的URL:', urlStr);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经是完整的 URL(http:// 或 https://),直接返回
|
||||
if (urlStr.indexOf('http://') === 0 || urlStr.indexOf('https://') === 0) {
|
||||
console.log('检测到完整URL,直接返回');
|
||||
return urlStr;
|
||||
}
|
||||
|
||||
// 如果是相对路径,拼接服务器地址
|
||||
// 移除开头的 / 避免重复
|
||||
let path = urlStr;
|
||||
if (path.charAt(0) === '/') {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
// 使用当前页面的协议和主机名
|
||||
const baseUrl = window.location.origin;
|
||||
const fullUrl = baseUrl + '/' + path;
|
||||
|
||||
console.log('拼接后的完整URL:', fullUrl);
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -448,4 +540,15 @@ export default {
|
|||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeMessage',
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeNode',
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeDetail',
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeList',
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishtree';
|
||||
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeBackground',
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishtree';
|
||||
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeFestival',
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getStatistics } from '@/api/wishtree';
|
||||
import { getStatistics } from '@/api/wishTree';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishtree';
|
||||
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeWish',
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ module.exports = {
|
|||
'/api': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/crmebimage': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/file': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -64,6 +72,9 @@ module.exports = {
|
|||
alias: {
|
||||
'@': resolve('src'),
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
// 在Windows上强制不区分大小写
|
||||
symlinks: false,
|
||||
},
|
||||
},
|
||||
chainWebpack(config) {
|
||||
|
|
|
|||
|
|
@ -92,13 +92,26 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
|
||||
/** 本地文件上传路径 */
|
||||
registry.addResourceHandler(UploadConstants.UPLOAD_FILE_KEYWORD + "/**")
|
||||
.addResourceLocations("file:" + crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_FILE_KEYWORD + "/");
|
||||
/** 本地文件上传路径 - 使用jar包所在目录 */
|
||||
String uploadPath = crmebConfig.getAbsoluteImagePath();
|
||||
|
||||
registry.addResourceHandler(UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/**")
|
||||
.addResourceLocations("file:" +crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/" );
|
||||
// 添加 image 路径映射(修复图片上传显示问题)
|
||||
registry.addResourceHandler("/image/**")
|
||||
.addResourceLocations("file:" + uploadPath + "image" + File.separator);
|
||||
|
||||
// 添加 video 路径映射
|
||||
registry.addResourceHandler("/video/**")
|
||||
.addResourceLocations("file:" + uploadPath + "video" + File.separator);
|
||||
|
||||
// 添加 voice 路径映射
|
||||
registry.addResourceHandler("/voice/**")
|
||||
.addResourceLocations("file:" + uploadPath + "voice" + File.separator);
|
||||
|
||||
registry.addResourceHandler("/crmebimage/**")
|
||||
.addResourceLocations("file:" + uploadPath + "crmebimage" + File.separator);
|
||||
|
||||
registry.addResourceHandler("/file/**")
|
||||
.addResourceLocations("file:" + uploadPath + "file" + File.separator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@ public class GiftAdminController {
|
|||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
|
||||
|
||||
log.info("礼物列表查询 - 参数: name={}, status={}, page={}, limit={}", name, status, page, limit);
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT id, name, image, diamond_price as diamondPrice, intimacy, status, ");
|
||||
sql.append("is_heartbeat as isHeartbeat, buy_type as buyType, belong, remark, ");
|
||||
|
|
@ -375,23 +377,32 @@ public class GiftAdminController {
|
|||
countParams.add(status);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY sort ASC, id DESC ");
|
||||
sql.append(" ORDER BY sort ASC, id ASC ");
|
||||
|
||||
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class, countParams.toArray());
|
||||
if (total == null) {
|
||||
total = 0L;
|
||||
}
|
||||
|
||||
int offset = (page - 1) * limit;
|
||||
sql.append(" LIMIT ? OFFSET ? ");
|
||||
params.add(limit);
|
||||
params.add(offset);
|
||||
|
||||
log.info("礼物列表查询 - SQL: {}", sql.toString());
|
||||
log.info("礼物列表查询 - 参数: {}", params);
|
||||
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 打印日志用于调试
|
||||
log.info("礼物列表查询 - 总数: {}, 当前页: {}, 每页: {}, offset: {}, 列表大小: {}", total, page, limit, offset, list.size());
|
||||
|
||||
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||
result.setList(list);
|
||||
result.setTotal(total != null ? total : 0L);
|
||||
result.setTotal(total);
|
||||
result.setPage(page);
|
||||
result.setLimit(limit);
|
||||
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
|
||||
result.setTotalPage((int) Math.ceil((double) total / limit));
|
||||
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
|
|
@ -403,14 +414,19 @@ public class GiftAdminController {
|
|||
@PostMapping("/add")
|
||||
public CommonResult<String> addGift(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("添加礼物 - 请求参数: {}", request);
|
||||
|
||||
String name = (String) request.get("name");
|
||||
String image = (String) request.get("image");
|
||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
||||
Integer level = request.get("level") != null ? Integer.parseInt(request.get("level").toString()) : 1;
|
||||
Integer isHeartbeat = request.get("isHeartbeat") != null ? Integer.parseInt(request.get("isHeartbeat").toString()) : 0;
|
||||
|
||||
// 处理布尔值或整数值
|
||||
Integer isHeartbeat = convertToInteger(request.get("isHeartbeat"), 0);
|
||||
Integer sort = request.get("sort") != null ? Integer.parseInt(request.get("sort").toString()) : 0;
|
||||
Integer status = request.get("status") != null ? Integer.parseInt(request.get("status").toString()) : 1;
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String remark = (String) request.get("remark");
|
||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
||||
|
|
@ -420,6 +436,7 @@ public class GiftAdminController {
|
|||
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||
buyType, belong, remark, level, sort);
|
||||
|
||||
log.info("添加礼物成功 - 名称: {}", name);
|
||||
return CommonResult.success("添加成功");
|
||||
} catch (Exception e) {
|
||||
log.error("添加礼物失败", e);
|
||||
|
|
@ -434,15 +451,20 @@ public class GiftAdminController {
|
|||
@PostMapping("/update")
|
||||
public CommonResult<String> updateGift(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("更新礼物 - 请求参数: {}", request);
|
||||
|
||||
Integer id = Integer.parseInt(request.get("id").toString());
|
||||
String name = (String) request.get("name");
|
||||
String image = (String) request.get("image");
|
||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
||||
Integer level = request.get("level") != null ? Integer.parseInt(request.get("level").toString()) : 1;
|
||||
Integer isHeartbeat = request.get("isHeartbeat") != null ? Integer.parseInt(request.get("isHeartbeat").toString()) : 0;
|
||||
|
||||
// 处理布尔值或整数值
|
||||
Integer isHeartbeat = convertToInteger(request.get("isHeartbeat"), 0);
|
||||
Integer sort = request.get("sort") != null ? Integer.parseInt(request.get("sort").toString()) : 0;
|
||||
Integer status = request.get("status") != null ? Integer.parseInt(request.get("status").toString()) : 1;
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String remark = (String) request.get("remark");
|
||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
||||
|
|
@ -452,6 +474,7 @@ public class GiftAdminController {
|
|||
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||
buyType, belong, remark, level, sort, id);
|
||||
|
||||
log.info("更新礼物成功 - ID: {}", id);
|
||||
return CommonResult.success("更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新礼物失败", e);
|
||||
|
|
@ -459,6 +482,35 @@ public class GiftAdminController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为整数(支持布尔值、字符串、整数)
|
||||
*/
|
||||
private Integer convertToInteger(Object value, Integer defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 如果是布尔值
|
||||
if (value instanceof Boolean) {
|
||||
return ((Boolean) value) ? 1 : 0;
|
||||
}
|
||||
|
||||
// 如果是字符串
|
||||
String strValue = value.toString().toLowerCase();
|
||||
if ("true".equals(strValue)) {
|
||||
return 1;
|
||||
} else if ("false".equals(strValue)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 尝试解析为整数
|
||||
try {
|
||||
return Integer.parseInt(strValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼物
|
||||
*/
|
||||
|
|
@ -490,12 +542,15 @@ public class GiftAdminController {
|
|||
@PostMapping("/status")
|
||||
public CommonResult<String> updateGiftStatus(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("更新礼物状态 - 请求参数: {}", request);
|
||||
|
||||
Integer id = Integer.parseInt(request.get("id").toString());
|
||||
Integer status = Integer.parseInt(request.get("status").toString());
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String sql = "UPDATE eb_gift SET status = ? WHERE id = ?";
|
||||
jdbcTemplate.update(sql, status, id);
|
||||
|
||||
log.info("更新礼物状态成功 - ID: {}, 状态: {}", id, status);
|
||||
return CommonResult.success("状态更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新礼物状态失败", e);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,18 @@ public class ResponseRouter {
|
|||
return data;
|
||||
}
|
||||
|
||||
// 排除上传接口,上传接口返回的URL不需要添加前缀
|
||||
// 因为前端会直接保存到数据库,下次查询时会再次处理
|
||||
if (path.contains("/upload/image") || path.contains("/upload/file")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
//根据需要处理返回值 && !data.contains("data:image/png;base64")
|
||||
// 如果数据已经包含完整的URL(http://或https://),则不处理
|
||||
if (data.contains("http://") || data.contains("https://")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if ((data.contains(UploadConstants.UPLOAD_FILE_KEYWORD + "/"))
|
||||
|| data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD) || data.contains(UploadConstants.UPLOAD_AFTER_FILE_KEYWORD)) {
|
||||
if (data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD + "/" + UploadConstants.UPLOAD_MODEL_PATH_EXCEL)) {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ crmeb:
|
|||
wechat-api-url: #请求微信接口中专服务器
|
||||
wechat-js-api-debug: false #微信js api系列是否开启调试模式
|
||||
wechat-js-api-beta: true #微信js api是否是beta版本
|
||||
asyncConfig: true #是否同步config表数据到redis
|
||||
asyncConfig: false #是否同步config表数据到redis - 改为false,直接从数据库读取
|
||||
asyncWeChatProgramTempList: false #是否同步小程序公共模板库
|
||||
imagePath: /Users/23730/Desktop/projectr/single_java/ # 服务器图片路径配置 斜杠结尾
|
||||
imagePath: upload/ # 服务器图片路径配置 - jar包同级目录的upload文件夹
|
||||
demoSite: true # 是否演示站点 所有手机号码都会掩码
|
||||
activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位,生产环境适当5-10分钟即可
|
||||
ignored: #安全路径白名单
|
||||
|
|
|
|||
|
|
@ -107,6 +107,67 @@ public class CrmebConfig {
|
|||
return imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片存储的绝对路径
|
||||
* 如果配置的是相对路径,则转换为jar包所在目录的绝对路径
|
||||
*/
|
||||
public String getAbsoluteImagePath() {
|
||||
if (imagePath == null || imagePath.isEmpty()) {
|
||||
String result = getJarDirectory() + "/upload/";
|
||||
System.out.println("图片路径(默认): " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果是绝对路径(Windows: C:/ 或 Linux: /),直接返回
|
||||
if (imagePath.startsWith("/") || imagePath.matches("^[a-zA-Z]:.*")) {
|
||||
System.out.println("图片路径(绝对): " + imagePath);
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// 相对路径,转换为jar包所在目录的绝对路径
|
||||
String absolutePath = getJarDirectory() + "/" + imagePath;
|
||||
// 确保以斜杠结尾
|
||||
if (!absolutePath.endsWith("/") && !absolutePath.endsWith("\\")) {
|
||||
absolutePath += "/";
|
||||
}
|
||||
System.out.println("图片路径(相对转绝对): " + absolutePath);
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取jar包所在目录
|
||||
*/
|
||||
private String getJarDirectory() {
|
||||
try {
|
||||
String path = CrmebConfig.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
// 解码URL编码的路径
|
||||
path = java.net.URLDecoder.decode(path, "UTF-8");
|
||||
|
||||
System.out.println("=== 调试信息 ===");
|
||||
System.out.println("原始路径: " + path);
|
||||
|
||||
// 如果是jar包,去掉jar文件名
|
||||
if (path.endsWith(".jar")) {
|
||||
path = path.substring(0, path.lastIndexOf("/"));
|
||||
System.out.println("去掉jar文件名后: " + path);
|
||||
}
|
||||
// 去掉开头的斜杠(Windows)
|
||||
if (path.startsWith("/") && path.matches("^/[a-zA-Z]:.*")) {
|
||||
path = path.substring(1);
|
||||
System.out.println("去掉开头斜杠后: " + path);
|
||||
}
|
||||
|
||||
System.out.println("最终jar目录: " + path);
|
||||
System.out.println("===============");
|
||||
|
||||
return path;
|
||||
} catch (Exception e) {
|
||||
System.out.println("获取jar目录失败,使用user.dir: " + System.getProperty("user.dir"));
|
||||
// 如果获取失败,使用当前工作目录
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ public class SendGiftResponse implements Serializable {
|
|||
@ApiModelProperty(value = "剩余钻石数")
|
||||
private BigDecimal remainingDiamond;
|
||||
|
||||
@ApiModelProperty(value = "新余额(剩余金币数,兼容字段)")
|
||||
private BigDecimal newBalance;
|
||||
|
||||
@ApiModelProperty(value = "增加的亲密度")
|
||||
private Integer intimacy;
|
||||
|
||||
|
|
|
|||
|
|
@ -274,13 +274,31 @@ public class LiveRoomController {
|
|||
@Autowired
|
||||
private com.zbkj.service.service.GiftRecordService giftRecordService;
|
||||
|
||||
@Autowired
|
||||
private com.zbkj.front.service.GiftWebSocketService giftWebSocketService;
|
||||
|
||||
@Autowired
|
||||
private com.zbkj.service.service.GiftService giftService;
|
||||
|
||||
@ApiOperation(value = "赠送礼物(需要登录)")
|
||||
@PostMapping("/rooms/{roomId}/gift")
|
||||
public CommonResult<com.zbkj.common.response.SendGiftResponse> sendGift(
|
||||
@PathVariable Integer roomId,
|
||||
@RequestBody @Validated com.zbkj.common.request.SendGiftRequest request) {
|
||||
|
||||
// 🔍 添加调试日志
|
||||
log.info("========================================");
|
||||
log.info("🎁 收到礼物赠送请求");
|
||||
log.info("roomId: {}", roomId);
|
||||
log.info("request.giftId: {}", request.getGiftId());
|
||||
log.info("request.receiverId: {}", request.getReceiverId());
|
||||
log.info("request.giftCount: {}", request.getGiftCount());
|
||||
log.info("========================================");
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Integer currentUserId = frontTokenComponent.getUserId();
|
||||
log.info("currentUserId: {}", currentUserId);
|
||||
|
||||
if (currentUserId == null) {
|
||||
return CommonResult.failed("请先登录");
|
||||
}
|
||||
|
|
@ -295,19 +313,53 @@ public class LiveRoomController {
|
|||
return CommonResult.failed("直播间不存在");
|
||||
}
|
||||
|
||||
// TODO: 后续可能需要恢复此验证
|
||||
// 不能给自己送礼物
|
||||
if (currentUserId.equals(request.getReceiverId())) {
|
||||
return CommonResult.failed("不能给自己赠送礼物");
|
||||
}
|
||||
// if (currentUserId.equals(request.getReceiverId())) {
|
||||
// return CommonResult.failed("不能给自己赠送礼物");
|
||||
// }
|
||||
|
||||
try {
|
||||
// 调用服务层赠送礼物
|
||||
com.zbkj.common.response.SendGiftResponse response =
|
||||
giftRecordService.sendGift(roomId, currentUserId, request);
|
||||
log.info("✅ 礼物赠送成功");
|
||||
|
||||
// 🎁 通过WebSocket推送礼物消息到直播间
|
||||
try {
|
||||
// 获取用户信息
|
||||
com.zbkj.common.model.user.User sender = userService.getById(currentUserId);
|
||||
com.zbkj.common.model.user.User receiver = userService.getById(request.getReceiverId());
|
||||
com.zbkj.common.model.gift.Gift gift = giftService.getGiftById(request.getGiftId());
|
||||
|
||||
if (sender != null && receiver != null && gift != null) {
|
||||
giftWebSocketService.broadcastGift(
|
||||
String.valueOf(roomId),
|
||||
gift.getId(),
|
||||
gift.getName(),
|
||||
request.getGiftCount(),
|
||||
currentUserId,
|
||||
sender.getNickname(),
|
||||
request.getReceiverId(),
|
||||
receiver.getNickname(),
|
||||
response.getTotalDiamond().intValue()
|
||||
);
|
||||
log.info("✅ 礼物WebSocket推送成功: roomId={}, giftName={}, sender={}",
|
||||
roomId, gift.getName(), sender.getNickname());
|
||||
} else {
|
||||
log.warn("⚠️ 无法推送礼物消息:用户或礼物信息不完整");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 礼物WebSocket推送失败", e);
|
||||
// 不影响主流程,继续返回成功
|
||||
}
|
||||
|
||||
return CommonResult.success(response);
|
||||
} catch (com.zbkj.common.exception.CrmebException e) {
|
||||
log.error("❌ 礼物赠送失败: {}", e.getMessage());
|
||||
return CommonResult.failed(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 礼物赠送异常", e);
|
||||
return CommonResult.failed("赠送礼物失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ public class UserController {
|
|||
log.info("请求参数: {}", request);
|
||||
log.info("昵称: {}", request.getNickname());
|
||||
log.info("头像: {}", request.getAvatar());
|
||||
log.info("个人签名: {}", request.getBio());
|
||||
log.info("个人签名: {}", request.getMark());
|
||||
log.info("生日: {}", request.getBirthday());
|
||||
log.info("性别: {}", request.getGender());
|
||||
log.info("所在地: {}", request.getLocation());
|
||||
log.info("性别: {}", request.getSex());
|
||||
log.info("所在地: {}", request.getAddres());
|
||||
|
||||
try {
|
||||
boolean result = userService.editUser(request);
|
||||
|
|
@ -101,7 +101,6 @@ public class UserController {
|
|||
UserCenterResponse response = userService.getUserCenter();
|
||||
log.info("========== 返回用户信息 ==========");
|
||||
log.info("昵称: {}", response.getNickname());
|
||||
log.info("个人签名: {}", response.getBio());
|
||||
log.info("生日: {}", response.getBirthday());
|
||||
log.info("性别: {}", response.getSex());
|
||||
log.info("所在地: {}", response.getAddres());
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
|||
|
||||
// 5. 检查余额(假设使用now_money字段作为钻石余额)
|
||||
if (sender.getNowMoney().compareTo(totalDiamond) < 0) {
|
||||
throw new CrmebException("钻石余额不足");
|
||||
throw new CrmebException("金币余额不足");
|
||||
}
|
||||
|
||||
// 6. 扣除赠送者钻石
|
||||
|
|
@ -106,7 +106,12 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
|||
response.setGiftName(gift.getName());
|
||||
response.setGiftCount(request.getGiftCount());
|
||||
response.setTotalDiamond(totalDiamond);
|
||||
response.setRemainingDiamond(sender.getNowMoney().subtract(totalDiamond));
|
||||
|
||||
// 计算剩余余额
|
||||
BigDecimal remainingBalance = sender.getNowMoney().subtract(totalDiamond);
|
||||
response.setRemainingDiamond(remainingBalance);
|
||||
response.setNewBalance(remainingBalance); // 兼容 Android 客户端
|
||||
|
||||
response.setIntimacy(record.getIntimacy());
|
||||
response.setSendTime(record.getCreateTime().getTime());
|
||||
|
||||
|
|
|
|||
|
|
@ -86,12 +86,20 @@ public class SystemAttachmentServiceImpl extends ServiceImpl<SystemAttachmentDao
|
|||
*/
|
||||
@Override
|
||||
public String prefixImage(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
// 如果那些域名不需要加,则跳过
|
||||
return path.replace(UploadConstants.UPLOAD_FILE_KEYWORD+"/", getCdnUrl() + "/"+ UploadConstants.UPLOAD_FILE_KEYWORD+"/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prefixUploadf(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
// 如果那些域名不需要加,则跳过
|
||||
return path.replace("crmebimage/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD+"/", getCdnUrl() + "/" +"crmebimage/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD+"/");
|
||||
}
|
||||
|
|
@ -103,6 +111,10 @@ public class SystemAttachmentServiceImpl extends ServiceImpl<SystemAttachmentDao
|
|||
*/
|
||||
@Override
|
||||
public String prefixFile(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
if (path.contains(Constants.WECHAT_SOURCE_CODE_FILE_NAME)) {
|
||||
String cdnUrl = systemConfigService.getValueByKey("local" + "UploadUrl");
|
||||
return path.replace("crmebimage/", cdnUrl + "/crmebimage/");
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
// 模块
|
||||
String modelPath = "public/" + model + "/";
|
||||
// 类型
|
||||
|
|
@ -356,8 +356,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
String modelPath = "public/" + model + "/";
|
||||
String type = "video/";
|
||||
|
||||
|
|
@ -431,8 +431,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
String modelPath = "public/" + model + "/";
|
||||
String type = "voice/";
|
||||
|
||||
|
|
|
|||
|
|
@ -343,7 +343,8 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
}
|
||||
|
||||
/**
|
||||
* 更新用户金额
|
||||
* 更新用户金额(并发安全版本)
|
||||
* 使用数据库原子操作,避免并发竞态条件
|
||||
*
|
||||
* @param user 用户
|
||||
* @param price 金额
|
||||
|
|
@ -352,17 +353,31 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
*/
|
||||
@Override
|
||||
public Boolean updateNowMoney(User user, BigDecimal price, String type) {
|
||||
LambdaUpdateWrapper<User> lambdaUpdateWrapper = Wrappers.lambdaUpdate();
|
||||
// 🔒 使用 UpdateWrapper 直接在 SQL 层面进行原子操作
|
||||
// 这样可以避免并发时的竞态条件
|
||||
// 原理:UPDATE eb_user SET now_money = now_money +/- price WHERE uid = ?
|
||||
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
|
||||
|
||||
if (type.equals("add")) {
|
||||
lambdaUpdateWrapper.set(User::getNowMoney, user.getNowMoney().add(price));
|
||||
// SQL: UPDATE eb_user SET now_money = now_money + price WHERE uid = ?
|
||||
updateWrapper.setSql(StrUtil.format("now_money = now_money + {}", price));
|
||||
} else {
|
||||
lambdaUpdateWrapper.set(User::getNowMoney, user.getNowMoney().subtract(price));
|
||||
// SQL: UPDATE eb_user SET now_money = now_money - price WHERE uid = ? AND now_money - price >= 0
|
||||
updateWrapper.setSql(StrUtil.format("now_money = now_money - {}", price));
|
||||
// 确保余额不会变成负数(在 SQL 层面检查)
|
||||
updateWrapper.last(StrUtil.format(" AND (now_money - {} >= 0)", price));
|
||||
}
|
||||
lambdaUpdateWrapper.eq(User::getUid, user.getUid());
|
||||
if (type.equals("sub")) {
|
||||
lambdaUpdateWrapper.apply(StrUtil.format(" now_money - {} >= 0", price));
|
||||
|
||||
updateWrapper.eq("uid", user.getUid());
|
||||
|
||||
boolean result = update(updateWrapper);
|
||||
|
||||
// 如果是扣减操作且更新失败,说明余额不足
|
||||
if (!result && type.equals("sub")) {
|
||||
logger.warn("⚠️ 用户 {} 余额不足,无法扣除 {} 元", user.getUid(), price);
|
||||
}
|
||||
return update(lambdaUpdateWrapper);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -576,6 +591,12 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
BeanUtils.copyProperties(currentUser, userCenterResponse);
|
||||
// 设置用户ID
|
||||
userCenterResponse.setUid(currentUser.getUid());
|
||||
|
||||
// 手动映射 mark -> bio(个人签名)
|
||||
if (currentUser.getMark() != null) {
|
||||
userCenterResponse.setBio(currentUser.getMark());
|
||||
}
|
||||
|
||||
// 优惠券数量
|
||||
userCenterResponse.setCouponCount(storeCouponUserService.getUseCount(currentUser.getUid()));
|
||||
// 收藏数量
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,13 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS `eb_live_room` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`uid` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`streamer_name` varchar(255) NOT NULL,
|
||||
`stream_key` varchar(64) NOT NULL,
|
||||
`is_live` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`started_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_stream_key` (`stream_key`),
|
||||
KEY `idx_uid` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
-- 好友关系表
|
||||
CREATE TABLE IF NOT EXISTS `eb_friend` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`friend_id` int(11) NOT NULL COMMENT '好友ID',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 1=正常, 0=已删除',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_friend_id` (`friend_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表';
|
||||
|
||||
-- 好友请求表
|
||||
CREATE TABLE IF NOT EXISTS `eb_friend_request` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`from_user_id` int(11) NOT NULL COMMENT '发送者用户ID',
|
||||
`to_user_id` int(11) NOT NULL COMMENT '接收者用户ID',
|
||||
`message` varchar(255) DEFAULT '' COMMENT '请求消息',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态: 0=待处理, 1=已接受, 2=已拒绝',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_from_user` (`from_user_id`),
|
||||
KEY `idx_to_user` (`to_user_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表';
|
||||
|
||||
-- 私聊会话表
|
||||
CREATE TABLE IF NOT EXISTS `eb_conversation` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user1_id` int(11) NOT NULL COMMENT '用户1 ID (较小的ID)',
|
||||
`user2_id` int(11) NOT NULL COMMENT '用户2 ID (较大的ID)',
|
||||
`last_message_id` int(11) DEFAULT NULL COMMENT '最后一条消息ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_users` (`user1_id`, `user2_id`),
|
||||
KEY `idx_user1` (`user1_id`),
|
||||
KEY `idx_user2` (`user2_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊会话表';
|
||||
|
||||
-- 私聊消息表
|
||||
CREATE TABLE IF NOT EXISTS `eb_conversation_message` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`conversation_id` int(11) NOT NULL COMMENT '会话ID',
|
||||
`sender_id` int(11) NOT NULL COMMENT '发送者ID',
|
||||
`content` text NOT NULL COMMENT '消息内容',
|
||||
`msg_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '消息类型: 1=文本, 2=图片, 3=语音',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态: 0=未读, 1=已读',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_conversation` (`conversation_id`),
|
||||
KEY `idx_sender` (`sender_id`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊消息表';
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
-- 创建 zhibo 数据库
|
||||
CREATE DATABASE IF NOT EXISTS `zhibo` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- 使用 zhibo 数据库
|
||||
USE `zhibo`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -103,6 +103,9 @@ dependencies {
|
|||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||
|
||||
// SVG支持
|
||||
implementation("com.caverock:androidsvg-aar:1.4")
|
||||
|
||||
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||
|
||||
// FlexboxLayout for message reactions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
/**
|
||||
* 余额页面的ViewPager适配器
|
||||
* 包含充值记录和消费记录两个Tab
|
||||
*/
|
||||
public class BalancePagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
public BalancePagerAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new RechargeRecordFragment();
|
||||
case 1:
|
||||
return new ConsumeRecordFragment();
|
||||
default:
|
||||
return new RechargeRecordFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 2; // 充值记录 + 消费记录
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* 消费记录Fragment
|
||||
*/
|
||||
public class ConsumeRecordFragment extends Fragment {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TextView tvEmpty;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_record_list, container, false);
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
tvEmpty = view.findViewById(R.id.tv_empty);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
// TODO: 加载消费记录数据
|
||||
showEmpty();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void showEmpty() {
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
tvEmpty.setVisibility(View.VISIBLE);
|
||||
tvEmpty.setText("暂无消费记录");
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,16 @@ public class Gift {
|
|||
this.level = level;
|
||||
}
|
||||
|
||||
// 新增构造函数 - 支持URL
|
||||
public Gift(String id, String name, int price, String iconUrl, int level) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
this.iconUrl = iconUrl;
|
||||
this.iconResId = R.drawable.ic_gift_24; // 默认占位图
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -90,4 +100,11 @@ public class Gift {
|
|||
public String getFormattedPrice() {
|
||||
return price + " 金币";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有图标URL
|
||||
*/
|
||||
public boolean hasIconUrl() {
|
||||
return iconUrl != null && !iconUrl.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -9,11 +10,17 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.example.livestreaming.glide.SvgSoftwareLayerSetter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 礼物列表适配器
|
||||
* 支持PNG、JPG和SVG格式图标
|
||||
*/
|
||||
public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder> {
|
||||
|
||||
|
|
@ -77,7 +84,23 @@ public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder
|
|||
public void bind(Gift gift) {
|
||||
giftName.setText(gift.getName());
|
||||
giftPrice.setText(gift.getFormattedPrice());
|
||||
|
||||
if (gift.hasIconUrl()) {
|
||||
String iconUrl = gift.getIconUrl();
|
||||
android.util.Log.d("GiftAdapter", "加载图标: " + gift.getName() + ", URL: " + iconUrl);
|
||||
|
||||
// 检查是否是SVG格式
|
||||
if (iconUrl.toLowerCase().endsWith(".svg") || iconUrl.toLowerCase().contains(".svg?")) {
|
||||
// 加载SVG格式
|
||||
loadSvgImage(iconUrl, gift.getName());
|
||||
} else {
|
||||
// 加载PNG/JPG等格式
|
||||
loadRegularImage(iconUrl, gift.getName());
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("GiftAdapter", "没有iconUrl: " + gift.getName());
|
||||
giftIcon.setImageResource(gift.getIconResId());
|
||||
}
|
||||
|
||||
// 选中状态
|
||||
boolean isSelected = selectedGift != null && selectedGift.getId().equals(gift.getId());
|
||||
|
|
@ -93,5 +116,69 @@ public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载SVG格式图片
|
||||
*/
|
||||
private void loadSvgImage(String iconUrl, String giftName) {
|
||||
RequestBuilder<PictureDrawable> requestBuilder = Glide.with(itemView.getContext())
|
||||
.as(PictureDrawable.class)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<PictureDrawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<PictureDrawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("GiftAdapter", "SVG加载失败: " + giftName + ", URL: " + iconUrl, e);
|
||||
// 加载失败时显示默认图标
|
||||
giftIcon.setImageResource(R.drawable.ic_gift_24);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(PictureDrawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<PictureDrawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("GiftAdapter", "SVG加载成功: " + giftName);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
requestBuilder.load(iconUrl).into(new SvgSoftwareLayerSetter(giftIcon));
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载常规格式图片(PNG/JPG等)
|
||||
*/
|
||||
private void loadRegularImage(String iconUrl, String giftName) {
|
||||
Glide.with(itemView.getContext())
|
||||
.load(iconUrl)
|
||||
.placeholder(R.drawable.ic_gift_24)
|
||||
.error(R.drawable.ic_gift_24)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("GiftAdapter", "图片加载失败: " + giftName + ", URL: " + iconUrl, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("GiftAdapter", "图片加载成功: " + giftName);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(giftIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,625 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 礼物动画管理器
|
||||
* 负责显示不同等级的礼物特效
|
||||
*/
|
||||
public class GiftAnimationManager {
|
||||
|
||||
private final Context context;
|
||||
private final View rootView;
|
||||
private final Handler handler;
|
||||
private final Queue<GiftAnimationData> animationQueue;
|
||||
private boolean isAnimating = false;
|
||||
private final Random random = new Random();
|
||||
|
||||
// 礼物等级定义
|
||||
private static final int LEVEL_BASIC = 1; // 0-20金币
|
||||
private static final int LEVEL_NORMAL = 2; // 21-100金币
|
||||
private static final int LEVEL_ADVANCED = 3; // 101-1000金币
|
||||
private static final int LEVEL_PREMIUM = 4; // 1001-3000金币
|
||||
private static final int LEVEL_LEGENDARY = 5; // 3000+金币
|
||||
|
||||
public GiftAnimationManager(Context context, View rootView) {
|
||||
this.context = context;
|
||||
this.rootView = rootView;
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
this.animationQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物动画
|
||||
*/
|
||||
public void showGiftAnimation(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl) {
|
||||
showGiftAnimation(giftName, giftPrice, count, senderName, giftIconUrl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物动画(带头像)
|
||||
*/
|
||||
public void showGiftAnimation(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl, String senderAvatarUrl) {
|
||||
GiftAnimationData data = new GiftAnimationData(
|
||||
giftName, giftPrice, count, senderName, giftIconUrl, senderAvatarUrl
|
||||
);
|
||||
|
||||
animationQueue.offer(data);
|
||||
|
||||
if (!isAnimating) {
|
||||
processNextAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下一个动画
|
||||
*/
|
||||
private void processNextAnimation() {
|
||||
if (animationQueue.isEmpty()) {
|
||||
isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isAnimating = true;
|
||||
GiftAnimationData data = animationQueue.poll();
|
||||
|
||||
int level = getGiftLevel(data.giftPrice);
|
||||
|
||||
switch (level) {
|
||||
case LEVEL_BASIC:
|
||||
playBasicAnimation(data);
|
||||
break;
|
||||
case LEVEL_NORMAL:
|
||||
playNormalAnimation(data);
|
||||
break;
|
||||
case LEVEL_ADVANCED:
|
||||
playAdvancedAnimation(data);
|
||||
break;
|
||||
case LEVEL_PREMIUM:
|
||||
playPremiumAnimation(data);
|
||||
break;
|
||||
case LEVEL_LEGENDARY:
|
||||
playLegendaryAnimation(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物等级
|
||||
*/
|
||||
private int getGiftLevel(int price) {
|
||||
if (price <= 20) return LEVEL_BASIC;
|
||||
if (price <= 100) return LEVEL_NORMAL;
|
||||
if (price <= 1000) return LEVEL_ADVANCED;
|
||||
if (price <= 3000) return LEVEL_PREMIUM;
|
||||
return LEVEL_LEGENDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础动画 (0-20金币) - 简单滑入滑出
|
||||
*/
|
||||
private void playBasicAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-300f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -300f, 0f)
|
||||
);
|
||||
animSet.setDuration(400);
|
||||
animSet.setInterpolator(new DecelerateInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 2000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通动画 (21-100金币) - 滑入+缩放+粒子
|
||||
*/
|
||||
private void playNormalAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-300f);
|
||||
giftIcon.setScaleX(0.5f);
|
||||
giftIcon.setScaleY(0.5f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -300f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleX", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleY", 0.5f, 1.2f, 1f)
|
||||
);
|
||||
animSet.setDuration(500);
|
||||
animSet.setInterpolator(new OvershootInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createSimpleParticles();
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 2500);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级动画 (101-1000金币) - 华丽滑入+旋转+大量粒子
|
||||
*/
|
||||
private void playAdvancedAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-400f);
|
||||
giftIcon.setScaleX(0.3f);
|
||||
giftIcon.setScaleY(0.3f);
|
||||
giftIcon.setRotation(-180f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -400f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleX", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleY", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -180f, 0f)
|
||||
);
|
||||
animSet.setDuration(600);
|
||||
animSet.setInterpolator(new OvershootInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createAdvancedParticles();
|
||||
pulseAnimation(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 豪华动画 (1001-3000金币) - 震撼登场+闪光+爆炸粒子
|
||||
*/
|
||||
private void playPremiumAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
FrameLayout fullscreenEffect = rootView.findViewById(R.id.fullscreenEffectContainer);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
// 显示全屏闪光效果
|
||||
fullscreenEffect.setVisibility(View.VISIBLE);
|
||||
fullscreenEffect.setBackgroundColor(Color.argb(100, 255, 215, 0));
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setScaleX(0.5f);
|
||||
effectLayout.setScaleY(0.5f);
|
||||
giftIcon.setRotation(-360f);
|
||||
|
||||
// 闪光动画
|
||||
ObjectAnimator flashAnim = ObjectAnimator.ofFloat(fullscreenEffect, "alpha", 0.8f, 0f);
|
||||
flashAnim.setDuration(500);
|
||||
flashAnim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fullscreenEffect.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
flashAnim.start();
|
||||
|
||||
// 主动画
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleX", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleY", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -360f, 0f)
|
||||
);
|
||||
animSet.setDuration(700);
|
||||
animSet.setInterpolator(new OvershootInterpolator(1.5f));
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createPremiumParticles();
|
||||
continuousPulse(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 3500);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 传说动画 (3000+金币) - 超级震撼全屏特效
|
||||
*/
|
||||
private void playLegendaryAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
FrameLayout fullscreenEffect = rootView.findViewById(R.id.fullscreenEffectContainer);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
// 全屏彩虹渐变效果
|
||||
fullscreenEffect.setVisibility(View.VISIBLE);
|
||||
|
||||
ValueAnimator colorAnim = ValueAnimator.ofArgb(
|
||||
Color.argb(150, 255, 0, 0),
|
||||
Color.argb(150, 255, 165, 0),
|
||||
Color.argb(150, 255, 255, 0),
|
||||
Color.argb(150, 0, 255, 0),
|
||||
Color.argb(150, 0, 0, 255),
|
||||
Color.argb(150, 139, 0, 255)
|
||||
);
|
||||
colorAnim.setDuration(1000);
|
||||
colorAnim.addUpdateListener(animation -> {
|
||||
fullscreenEffect.setBackgroundColor((int) animation.getAnimatedValue());
|
||||
});
|
||||
colorAnim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(fullscreenEffect, "alpha", 1f, 0f);
|
||||
fadeOut.setDuration(500);
|
||||
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fullscreenEffect.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
fadeOut.start();
|
||||
}
|
||||
});
|
||||
colorAnim.start();
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setScaleX(0.3f);
|
||||
effectLayout.setScaleY(0.3f);
|
||||
effectLayout.setRotation(-180f);
|
||||
giftIcon.setRotation(-720f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleX", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleY", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "rotation", -180f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -720f, 0f)
|
||||
);
|
||||
animSet.setDuration(1000);
|
||||
animSet.setInterpolator(new OvershootInterpolator(2f));
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createLegendaryParticles();
|
||||
explosivePulse(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 4000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置礼物信息
|
||||
*/
|
||||
private void setupGiftInfo(GiftAnimationData data) {
|
||||
TextView senderName = rootView.findViewById(R.id.senderName);
|
||||
TextView giftName = rootView.findViewById(R.id.giftName);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
ImageView senderAvatar = rootView.findViewById(R.id.senderAvatar);
|
||||
TextView comboCount = rootView.findViewById(R.id.comboCount);
|
||||
|
||||
senderName.setText(data.senderName);
|
||||
giftName.setText(data.giftName);
|
||||
|
||||
// 加载礼物图标(优先使用网络图片)
|
||||
if (data.giftIconUrl != null && !data.giftIconUrl.isEmpty()) {
|
||||
// 使用Glide加载网络图片
|
||||
com.bumptech.glide.Glide.with(context)
|
||||
.load(data.giftIconUrl)
|
||||
.placeholder(getGiftIconResource(data.giftName))
|
||||
.error(getGiftIconResource(data.giftName))
|
||||
.into(giftIcon);
|
||||
} else {
|
||||
// 使用本地资源
|
||||
giftIcon.setImageResource(getGiftIconResource(data.giftName));
|
||||
}
|
||||
|
||||
// 加载用户头像(如果有的话)
|
||||
if (data.senderAvatarUrl != null && !data.senderAvatarUrl.isEmpty()) {
|
||||
com.bumptech.glide.Glide.with(context)
|
||||
.load(data.senderAvatarUrl)
|
||||
.placeholder(R.drawable.ic_user_24)
|
||||
.error(R.drawable.ic_user_24)
|
||||
.circleCrop()
|
||||
.into(senderAvatar);
|
||||
} else {
|
||||
senderAvatar.setImageResource(R.drawable.ic_user_24);
|
||||
}
|
||||
|
||||
if (data.count > 1) {
|
||||
comboCount.setVisibility(View.VISIBLE);
|
||||
comboCount.setText("x" + data.count);
|
||||
} else {
|
||||
comboCount.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物图标资源
|
||||
*/
|
||||
private int getGiftIconResource(String giftName) {
|
||||
// 根据礼物名称返回对应的图标资源
|
||||
// 这里简化处理,实际应该从网络加载
|
||||
switch (giftName) {
|
||||
case "小心心": return R.drawable.ic_gift_heart;
|
||||
case "玫瑰花": return R.drawable.ic_gift_rose;
|
||||
case "皇冠": return R.drawable.ic_gift_crown;
|
||||
case "火箭": return R.drawable.ic_gift_rocket;
|
||||
case "跑车": return R.drawable.ic_gift_car;
|
||||
case "游艇": return R.drawable.ic_gift_yacht;
|
||||
case "城堡": return R.drawable.ic_gift_castle;
|
||||
default: return R.drawable.ic_gift_24;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏动画
|
||||
*/
|
||||
private void hideAnimation(View effectLayout) {
|
||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(effectLayout, "alpha", 1f, 0f);
|
||||
fadeOut.setDuration(300);
|
||||
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
effectLayout.setVisibility(View.GONE);
|
||||
processNextAnimation();
|
||||
}
|
||||
});
|
||||
fadeOut.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建简单粒子效果
|
||||
*/
|
||||
private void createSimpleParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
createParticle(particleContainer, Color.argb(200, 255, 215, 0), 20);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高级粒子效果
|
||||
*/
|
||||
private void createAdvancedParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(200, 255, 215, 0),
|
||||
Color.argb(200, 255, 140, 0),
|
||||
Color.argb(200, 255, 69, 0)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 30);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建豪华粒子效果
|
||||
*/
|
||||
private void createPremiumParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(220, 255, 215, 0),
|
||||
Color.argb(220, 255, 0, 0),
|
||||
Color.argb(220, 255, 105, 180),
|
||||
Color.argb(220, 138, 43, 226)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 40);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建传说粒子效果
|
||||
*/
|
||||
private void createLegendaryParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(255, 255, 0, 0),
|
||||
Color.argb(255, 255, 165, 0),
|
||||
Color.argb(255, 255, 255, 0),
|
||||
Color.argb(255, 0, 255, 0),
|
||||
Color.argb(255, 0, 0, 255),
|
||||
Color.argb(255, 139, 0, 255)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 80; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个粒子
|
||||
*/
|
||||
private void createParticle(FrameLayout container, int color, int maxSize) {
|
||||
View particle = new View(context);
|
||||
int size = random.nextInt(maxSize - 10) + 10;
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
|
||||
particle.setLayoutParams(params);
|
||||
particle.setBackgroundColor(color);
|
||||
|
||||
// 随机起始位置
|
||||
int startX = random.nextInt(container.getWidth());
|
||||
int startY = container.getHeight() / 2;
|
||||
particle.setX(startX);
|
||||
particle.setY(startY);
|
||||
|
||||
container.addView(particle);
|
||||
|
||||
// 随机目标位置
|
||||
float targetX = startX + (random.nextFloat() - 0.5f) * 400;
|
||||
float targetY = startY - random.nextInt(300) - 100;
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(particle, "x", startX, targetX),
|
||||
ObjectAnimator.ofFloat(particle, "y", startY, targetY),
|
||||
ObjectAnimator.ofFloat(particle, "alpha", 1f, 0f),
|
||||
ObjectAnimator.ofFloat(particle, "rotation", 0f, random.nextInt(720) - 360)
|
||||
);
|
||||
animSet.setDuration(random.nextInt(1000) + 1000);
|
||||
animSet.setInterpolator(new DecelerateInterpolator());
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
container.removeView(particle);
|
||||
}
|
||||
});
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 脉冲动画
|
||||
*/
|
||||
private void pulseAnimation(View view) {
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.2f, 1f)
|
||||
);
|
||||
animSet.setDuration(500);
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 持续脉冲动画
|
||||
*/
|
||||
private void continuousPulse(View view) {
|
||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.15f, 1f);
|
||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.15f, 1f);
|
||||
|
||||
scaleX.setDuration(400);
|
||||
scaleX.setRepeatCount(3);
|
||||
scaleY.setDuration(400);
|
||||
scaleY.setRepeatCount(3);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(scaleX, scaleY);
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 爆炸式脉冲动画
|
||||
*/
|
||||
private void explosivePulse(View view) {
|
||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.3f, 1f);
|
||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.3f, 1f);
|
||||
ObjectAnimator rotation = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
|
||||
|
||||
scaleX.setDuration(600);
|
||||
scaleX.setRepeatCount(2);
|
||||
scaleY.setDuration(600);
|
||||
scaleY.setRepeatCount(2);
|
||||
rotation.setDuration(600);
|
||||
rotation.setRepeatCount(2);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(scaleX, scaleY, rotation);
|
||||
animSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼物动画数据类
|
||||
*/
|
||||
private static class GiftAnimationData {
|
||||
String giftName;
|
||||
int giftPrice;
|
||||
int count;
|
||||
String senderName;
|
||||
String giftIconUrl;
|
||||
String senderAvatarUrl;
|
||||
|
||||
GiftAnimationData(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl) {
|
||||
this.giftName = giftName;
|
||||
this.giftPrice = giftPrice;
|
||||
this.count = count;
|
||||
this.senderName = senderName;
|
||||
this.giftIconUrl = giftIconUrl;
|
||||
this.senderAvatarUrl = null;
|
||||
}
|
||||
|
||||
GiftAnimationData(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl, String senderAvatarUrl) {
|
||||
this.giftName = giftName;
|
||||
this.giftPrice = giftPrice;
|
||||
this.count = count;
|
||||
this.senderName = senderName;
|
||||
this.giftIconUrl = giftIconUrl;
|
||||
this.senderAvatarUrl = senderAvatarUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
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.cardview.widget.CardView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 充值套餐适配器
|
||||
*/
|
||||
public class RechargePackageAdapter extends RecyclerView.Adapter<RechargePackageAdapter.ViewHolder> {
|
||||
|
||||
private List<Map<String, Object>> packages;
|
||||
private int selectedPosition = -1;
|
||||
private OnItemClickListener listener;
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(Integer packageId);
|
||||
}
|
||||
|
||||
public RechargePackageAdapter(List<Map<String, Object>> packages) {
|
||||
this.packages = packages != null ? packages : new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(OnItemClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void updateData(List<Map<String, Object>> newPackages) {
|
||||
this.packages = newPackages != null ? newPackages : new ArrayList<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_recharge_package, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Map<String, Object> pkg = packages.get(position);
|
||||
|
||||
// 获取套餐信息
|
||||
Integer id = (Integer) pkg.get("id");
|
||||
String name = (String) pkg.get("name");
|
||||
Object amountObj = pkg.get("amount");
|
||||
Object priceObj = pkg.get("price");
|
||||
String label = (String) pkg.get("label");
|
||||
|
||||
double amount = amountObj instanceof Number ? ((Number) amountObj).doubleValue() : 0;
|
||||
double price = priceObj instanceof Number ? ((Number) priceObj).doubleValue() : 0;
|
||||
|
||||
// 设置数据
|
||||
holder.tvAmount.setText(String.format("%.0f", amount));
|
||||
holder.tvPrice.setText(String.format("¥%.2f", price));
|
||||
|
||||
if (label != null && !label.isEmpty()) {
|
||||
holder.tvLabel.setVisibility(View.VISIBLE);
|
||||
holder.tvLabel.setText(label);
|
||||
} else {
|
||||
holder.tvLabel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 设置选中状态
|
||||
boolean isSelected = position == selectedPosition;
|
||||
holder.cardView.setCardBackgroundColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? R.color.colorPrimary : android.R.color.white
|
||||
)
|
||||
);
|
||||
holder.tvAmount.setTextColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? android.R.color.white : R.color.colorPrimary
|
||||
)
|
||||
);
|
||||
holder.tvPrice.setTextColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? android.R.color.white : R.color.text_secondary
|
||||
)
|
||||
);
|
||||
|
||||
// 点击事件
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = holder.getAdapterPosition();
|
||||
|
||||
if (oldPosition != -1) {
|
||||
notifyItemChanged(oldPosition);
|
||||
}
|
||||
notifyItemChanged(selectedPosition);
|
||||
|
||||
if (listener != null && id != null) {
|
||||
listener.onItemClick(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return packages.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
CardView cardView;
|
||||
TextView tvAmount;
|
||||
TextView tvPrice;
|
||||
TextView tvLabel;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
cardView = (CardView) itemView;
|
||||
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||
tvPrice = itemView.findViewById(R.id.tv_price);
|
||||
tvLabel = itemView.findViewById(R.id.tv_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* 充值记录Fragment
|
||||
*/
|
||||
public class RechargeRecordFragment extends Fragment {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TextView tvEmpty;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_record_list, container, false);
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
tvEmpty = view.findViewById(R.id.tv_empty);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
// TODO: 加载充值记录数据
|
||||
showEmpty();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void showEmpty() {
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
tvEmpty.setVisibility(View.VISIBLE);
|
||||
tvEmpty.setText("暂无充值记录");
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,12 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
|
@ -151,6 +153,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
private GiftAdapter giftAdapter;
|
||||
private List<Gift> availableGifts;
|
||||
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
|
||||
private GiftAnimationManager giftAnimationManager; // 礼物动画管理器
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -180,6 +183,20 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
setupChat();
|
||||
setupGifts();
|
||||
|
||||
// 初始化礼物动画管理器
|
||||
android.util.Log.d("RoomDetail", "=== 初始化礼物动画管理器 ===");
|
||||
|
||||
// 从binding的根视图中查找
|
||||
View giftAnimationOverlay = binding.getRoot().findViewById(R.id.giftAnimationOverlay);
|
||||
android.util.Log.d("RoomDetail", "giftAnimationOverlay = " + giftAnimationOverlay);
|
||||
|
||||
if (giftAnimationOverlay != null) {
|
||||
giftAnimationManager = new GiftAnimationManager(this, giftAnimationOverlay);
|
||||
android.util.Log.d("RoomDetail", "✅ 礼物动画管理器初始化成功");
|
||||
} else {
|
||||
android.util.Log.e("RoomDetail", "❌ 找不到giftAnimationOverlay布局!");
|
||||
}
|
||||
|
||||
// 加载房间信息
|
||||
loadRoomInfo();
|
||||
|
||||
|
|
@ -452,11 +469,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 停止之前的重连任务
|
||||
stopChatReconnect();
|
||||
|
||||
String wsUrl = getWsChatBaseUrl() + roomId;
|
||||
android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl);
|
||||
|
||||
chatWsClient = new OkHttpClient.Builder()
|
||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||
.build();
|
||||
Request request = new Request.Builder()
|
||||
.url(getWsChatBaseUrl() + roomId)
|
||||
.url(wsUrl)
|
||||
.build();
|
||||
|
||||
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
||||
|
|
@ -472,9 +492,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
// 收到消息,解析并显示
|
||||
android.util.Log.d("ChatWebSocket", "收到消息: " + text);
|
||||
try {
|
||||
JSONObject json = new JSONObject(text);
|
||||
String type = json.optString("type", "");
|
||||
android.util.Log.d("ChatWebSocket", "消息类型: " + type);
|
||||
|
||||
if ("chat".equals(type)) {
|
||||
String nickname = json.optString("nickname", "匿名");
|
||||
|
|
@ -490,11 +512,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
String senderNickname = json.optString("senderNickname", "匿名");
|
||||
int totalPrice = json.optInt("totalPrice", 0);
|
||||
|
||||
android.util.Log.d("WebSocket", "收到礼物消息: " + json.toString());
|
||||
|
||||
handler.post(() -> {
|
||||
String giftMsg = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
|
||||
|
||||
// TODO: 显示礼物动画效果
|
||||
// 显示礼物动画效果
|
||||
android.util.Log.d("WebSocket", "准备显示礼物动画: " + giftName);
|
||||
showGiftAnimation(giftName, count, senderNickname);
|
||||
});
|
||||
} else if ("gift_combo".equals(type)) {
|
||||
|
|
@ -1183,7 +1208,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 防止重复显示连接消息
|
||||
private boolean hasShownConnectedMessage = false;
|
||||
|
||||
private void startHls(String url, @Nullable String altUrl) {
|
||||
@OptIn(markerClass = UnstableApi.class) private void startHls(String url, @Nullable String altUrl) {
|
||||
releaseIjkPlayer();
|
||||
if (binding != null) {
|
||||
binding.flvTextureView.setVisibility(View.GONE);
|
||||
|
|
@ -1560,6 +1585,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
* 从后端加载礼物列表
|
||||
*/
|
||||
private void loadGiftsFromBackend() {
|
||||
android.util.Log.d("RoomDetail", "=== 开始加载礼物列表 ===");
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
|
||||
|
||||
|
|
@ -1569,32 +1596,41 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
Response<ApiResponse<List<GiftResponse>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<List<GiftResponse>> apiResponse = response.body();
|
||||
android.util.Log.d("RoomDetail", "礼物列表响应码: " + apiResponse.getCode());
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
availableGifts = new ArrayList<>();
|
||||
for (GiftResponse giftResponse : apiResponse.getData()) {
|
||||
// 使用URL构造函数,从服务器加载图标
|
||||
String iconUrl = giftResponse.getIconUrl();
|
||||
android.util.Log.d("RoomDetail", "礼物: " + giftResponse.getName() +
|
||||
", 价格: " + giftResponse.getPrice() +
|
||||
", iconUrl: " + iconUrl);
|
||||
|
||||
Gift gift = new Gift(
|
||||
String.valueOf(giftResponse.getId()),
|
||||
giftResponse.getName(),
|
||||
giftResponse.getPrice().intValue(),
|
||||
R.drawable.ic_gift_rose, // 默认图标,实际应从URL加载
|
||||
iconUrl, // ✅ 使用URL而不是硬编码
|
||||
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
|
||||
);
|
||||
availableGifts.add(gift);
|
||||
}
|
||||
android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物");
|
||||
android.util.Log.d("RoomDetail", "✅ 成功加载 " + availableGifts.size() + " 个礼物");
|
||||
android.util.Log.d("RoomDetail", "礼物列表: " + getGiftNames());
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMessage());
|
||||
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败: " + apiResponse.getMessage());
|
||||
setDefaultGifts();
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "加载礼物列表失败");
|
||||
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败,响应码: " + response.code());
|
||||
setDefaultGifts();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<GiftResponse>>> call, Throwable t) {
|
||||
android.util.Log.e("RoomDetail", "加载礼物列表失败: " + t.getMessage());
|
||||
android.util.Log.e("RoomDetail", "❌ 加载礼物列表网络错误: " + t.getMessage(), t);
|
||||
setDefaultGifts();
|
||||
}
|
||||
});
|
||||
|
|
@ -1608,6 +1644,80 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 不再使用模拟数据,只从后端接口获取真实礼物数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载礼物图标到ImageView(支持PNG和SVG格式)
|
||||
* @param imageView 目标ImageView
|
||||
* @param gift 礼物对象
|
||||
*/
|
||||
private void loadGiftIcon(android.widget.ImageView imageView, Gift gift) {
|
||||
if (gift.hasIconUrl()) {
|
||||
String iconUrl = gift.getIconUrl();
|
||||
android.util.Log.d("RoomDetail", "加载礼物图标: " + gift.getName() + ", URL: " + iconUrl);
|
||||
|
||||
// 检查是否是SVG格式
|
||||
if (iconUrl.toLowerCase().endsWith(".svg") || iconUrl.toLowerCase().contains(".svg?")) {
|
||||
// 加载SVG格式
|
||||
com.bumptech.glide.RequestBuilder<android.graphics.drawable.PictureDrawable> requestBuilder =
|
||||
com.bumptech.glide.Glide.with(this)
|
||||
.as(android.graphics.drawable.PictureDrawable.class)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.PictureDrawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("RoomDetail", "SVG加载失败: " + gift.getName(), e);
|
||||
imageView.setImageResource(R.drawable.ic_gift_24);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.PictureDrawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("RoomDetail", "SVG加载成功: " + gift.getName());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
requestBuilder.load(iconUrl).into(new com.example.livestreaming.glide.SvgSoftwareLayerSetter(imageView));
|
||||
} else {
|
||||
// 加载PNG/JPG等格式
|
||||
com.bumptech.glide.Glide.with(this)
|
||||
.load(iconUrl)
|
||||
.placeholder(R.drawable.ic_gift_24)
|
||||
.error(R.drawable.ic_gift_24)
|
||||
.diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy.ALL)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("RoomDetail", "图片加载失败: " + gift.getName(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("RoomDetail", "图片加载成功: " + gift.getName());
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
} else {
|
||||
// 如果没有URL,使用本地资源
|
||||
imageView.setImageResource(gift.getIconResId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物选择弹窗
|
||||
*/
|
||||
|
|
@ -1651,7 +1761,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 礼物选择监听
|
||||
giftAdapter.setOnGiftClickListener(gift -> {
|
||||
selectedGiftLayout.setVisibility(View.VISIBLE);
|
||||
selectedGiftIcon.setImageResource(gift.getIconResId());
|
||||
|
||||
// 使用辅助方法加载礼物图标
|
||||
loadGiftIcon(selectedGiftIcon, gift);
|
||||
|
||||
selectedGiftName.setText(gift.getName());
|
||||
selectedGiftPrice.setText(gift.getFormattedPrice());
|
||||
giftCount[0] = 1;
|
||||
|
|
@ -1997,14 +2110,34 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// 获取主播用户ID
|
||||
Integer streamerId = room.getStreamerId();
|
||||
if (streamerId == null) {
|
||||
streamerId = room.getUid();
|
||||
}
|
||||
|
||||
if (streamerId == null) {
|
||||
Toast.makeText(this, "无法获取主播信息", Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "主播ID为空: roomId=" + roomId + ", room=" + room);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔍 详细日志
|
||||
android.util.Log.d("RoomDetail", "========================================");
|
||||
android.util.Log.d("RoomDetail", "📤 准备发送礼物请求");
|
||||
android.util.Log.d("RoomDetail", "roomId: " + roomId);
|
||||
android.util.Log.d("RoomDetail", "streamerId: " + streamerId);
|
||||
android.util.Log.d("RoomDetail", "giftId: " + selectedGift.getId());
|
||||
android.util.Log.d("RoomDetail", "count: " + count);
|
||||
android.util.Log.d("RoomDetail", "API URL: " + ApiClient.getCurrentBaseUrl(getApplicationContext()) + "api/front/live/rooms/" + roomId + "/gift");
|
||||
android.util.Log.d("RoomDetail", "========================================");
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
|
||||
// 获取主播ID(使用房间ID作为主播ID,或者从房间信息中获取)
|
||||
Integer streamerId = Integer.parseInt(roomId);
|
||||
SendGiftRequest request = new SendGiftRequest(
|
||||
Integer.parseInt(selectedGift.getId()),
|
||||
streamerId,
|
||||
count
|
||||
streamerId, // receiverId - 接收者用户ID(主播ID)
|
||||
count // giftCount - 礼物数量
|
||||
);
|
||||
|
||||
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
|
||||
|
|
@ -2013,20 +2146,46 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
|
||||
Response<ApiResponse<SendGiftResponse>> response) {
|
||||
android.util.Log.d("RoomDetail", "📥 收到响应: HTTP " + response.code());
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<SendGiftResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
SendGiftResponse giftResponse = apiResponse.getData();
|
||||
|
||||
// 更新余额
|
||||
// 🔒 安全更新余额(添加空值检查)
|
||||
if (giftResponse.getNewBalance() != null) {
|
||||
userCoinBalance = giftResponse.getNewBalance().intValue();
|
||||
coinBalance.setText(String.valueOf(userCoinBalance));
|
||||
android.util.Log.d("RoomDetail", "✅ 余额更新成功: " + userCoinBalance);
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "⚠️ newBalance 为 null,尝试重新加载余额");
|
||||
// 降级处理:重新加载余额
|
||||
loadUserBalance(coinBalance);
|
||||
}
|
||||
|
||||
// 在聊天区显示赠送消息
|
||||
String giftMessage = String.format("送出了 %d 个 %s", count, selectedGift.getName());
|
||||
addChatMessage(new ChatMessage("我", giftMessage, true));
|
||||
|
||||
Toast.makeText(RoomDetailActivity.this, "赠送成功!", Toast.LENGTH_SHORT).show();
|
||||
// ✨ 立即显示礼物特效动画
|
||||
String currentUserNickname = "我"; // 可以从用户信息中获取
|
||||
String currentUserAvatar = AuthStore.getAvatar(RoomDetailActivity.this); // 获取当前用户头像
|
||||
android.util.Log.d("RoomDetail", "🎁 礼物发送成功,显示特效动画");
|
||||
android.util.Log.d("RoomDetail", "礼物图标URL: " + selectedGift.getIconUrl());
|
||||
android.util.Log.d("RoomDetail", "用户头像URL: " + currentUserAvatar);
|
||||
|
||||
// 使用带头像的方法
|
||||
if (giftAnimationManager != null) {
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
selectedGift.getName(),
|
||||
selectedGift.getPrice(),
|
||||
count,
|
||||
currentUserNickname,
|
||||
selectedGift.getIconUrl(),
|
||||
currentUserAvatar
|
||||
);
|
||||
}
|
||||
|
||||
if (giftDialog != null) {
|
||||
giftDialog.dismiss();
|
||||
|
|
@ -2039,14 +2198,17 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
selectedGiftLayout.setVisibility(View.GONE);
|
||||
giftCountText.setText("1");
|
||||
} else {
|
||||
String errorMsg = apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误";
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"赠送失败: " + apiResponse.getMessage(),
|
||||
"赠送失败: " + errorMsg,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物失败: " + errorMsg);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"赠送失败",
|
||||
"赠送失败: HTTP " + response.code(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物HTTP错误: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2055,6 +2217,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
Toast.makeText(RoomDetailActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物网络错误", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -2330,21 +2493,93 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
* @param senderNickname 赠送者昵称
|
||||
*/
|
||||
private void showGiftAnimation(String giftName, int count, String senderNickname) {
|
||||
// TODO: 实现礼物动画效果
|
||||
// 这里可以使用Lottie动画库或自定义动画
|
||||
// 示例:显示一个Toast提示
|
||||
runOnUiThread(() -> {
|
||||
// 详细日志:诊断问题
|
||||
android.util.Log.d("GiftAnimation", "=== 开始显示礼物动画 ===");
|
||||
android.util.Log.d("GiftAnimation", "礼物名称: " + giftName);
|
||||
android.util.Log.d("GiftAnimation", "数量: " + count);
|
||||
android.util.Log.d("GiftAnimation", "赠送者: " + senderNickname);
|
||||
android.util.Log.d("GiftAnimation", "动画管理器状态: " + (giftAnimationManager != null ? "已初始化" : "未初始化"));
|
||||
android.util.Log.d("GiftAnimation", "礼物列表状态: " + (availableGifts != null ? "已加载(" + availableGifts.size() + "个)" : "未加载"));
|
||||
|
||||
// 检查动画管理器是否初始化
|
||||
if (giftAnimationManager == null) {
|
||||
android.util.Log.e("GiftAnimation", "❌ 礼物动画管理器未初始化!");
|
||||
String message = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 可以在这里添加更复杂的动画效果,例如:
|
||||
// 1. 使用Lottie播放礼物动画
|
||||
// 2. 显示礼物图标飞行动画
|
||||
// 3. 播放音效
|
||||
// 4. 显示特效粒子
|
||||
// 检查礼物列表是否加载
|
||||
if (availableGifts == null || availableGifts.isEmpty()) {
|
||||
android.util.Log.w("GiftAnimation", "⚠️ 礼物列表未加载或为空,使用默认价格显示特效");
|
||||
// 使用默认价格显示特效(而不是降级到Toast)
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
100, // 默认价格:普通特效
|
||||
count,
|
||||
senderNickname,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
android.util.Log.d("GiftAnimation", "显示礼物动画: " + message);
|
||||
// 查找礼物信息
|
||||
Gift gift = null;
|
||||
for (Gift g : availableGifts) {
|
||||
if (g.getName().equals(giftName)) {
|
||||
gift = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gift != null) {
|
||||
// 使用礼物动画管理器显示特效
|
||||
android.util.Log.d("GiftAnimation", "✅ 找到礼物信息: " + gift.getName() + ", 价格: " + gift.getPrice());
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
gift.getPrice(),
|
||||
count,
|
||||
senderNickname,
|
||||
gift.getIconUrl()
|
||||
);
|
||||
|
||||
android.util.Log.d("GiftAnimation",
|
||||
String.format("✅ 显示礼物动画: %s x%d (价格:%d) - %s",
|
||||
giftName, count, gift.getPrice(), senderNickname));
|
||||
} else {
|
||||
// 如果找不到礼物信息,使用默认价格显示特效
|
||||
android.util.Log.w("GiftAnimation", "⚠️ 未找到礼物信息: " + giftName);
|
||||
android.util.Log.w("GiftAnimation", "可用礼物列表: " + getGiftNames());
|
||||
|
||||
// 使用默认价格显示特效(而不是降级到Toast)
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
100, // 默认价格:普通特效
|
||||
count,
|
||||
senderNickname,
|
||||
null
|
||||
);
|
||||
|
||||
android.util.Log.d("GiftAnimation", "✅ 使用默认价格显示特效");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物名称列表(用于调试)
|
||||
*/
|
||||
private String getGiftNames() {
|
||||
if (availableGifts == null || availableGifts.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < availableGifts.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append(availableGifts.get(i).getName());
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Glide SVG模块 - 注册SVG支持
|
||||
*/
|
||||
@GlideModule
|
||||
public class GlideSvgModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
|
||||
@NonNull Registry registry) {
|
||||
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isManifestParsingEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* SVG解码器 - 将InputStream解码为SVG对象
|
||||
*/
|
||||
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
|
||||
@NonNull Options options) throws IOException {
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(source);
|
||||
if (width != Integer.MIN_VALUE) {
|
||||
svg.setDocumentWidth(width);
|
||||
}
|
||||
if (height != Integer.MIN_VALUE) {
|
||||
svg.setDocumentHeight(height);
|
||||
}
|
||||
return new SimpleResource<>(svg);
|
||||
} catch (SVGParseException ex) {
|
||||
throw new IOException("Cannot load SVG from stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
/**
|
||||
* SVG转换器 - 将SVG对象转换为PictureDrawable
|
||||
*/
|
||||
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
|
||||
@NonNull Options options) {
|
||||
SVG svg = toTranscode.get();
|
||||
Picture picture = svg.renderToPicture();
|
||||
PictureDrawable drawable = new PictureDrawable(picture);
|
||||
return new SimpleResource<>(drawable);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import android.widget.ImageView;
|
||||
import com.bumptech.glide.request.target.ImageViewTarget;
|
||||
|
||||
/**
|
||||
* SVG图层设置器 - 确保SVG正确渲染
|
||||
*/
|
||||
public class SvgSoftwareLayerSetter extends ImageViewTarget<PictureDrawable> {
|
||||
|
||||
public SvgSoftwareLayerSetter(ImageView view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setResource(PictureDrawable resource) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
view.setImageDrawable(resource);
|
||||
}
|
||||
}
|
||||
|
|
@ -685,4 +685,46 @@ public interface ApiService {
|
|||
Call<ApiResponse<List<CommunityResponse.MatchUser>>> getMatchList(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
// ==================== 虚拟货币和充值接口 ====================
|
||||
|
||||
/**
|
||||
* 获取虚拟货币余额
|
||||
*/
|
||||
@GET("api/front/virtual/balance")
|
||||
Call<ApiResponse<Map<String, Object>>> getVirtualBalance();
|
||||
|
||||
/**
|
||||
* 获取充值套餐列表
|
||||
*/
|
||||
@GET("api/front/virtual/recharge/packages")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getRechargePackages();
|
||||
|
||||
/**
|
||||
* 创建充值订单
|
||||
*/
|
||||
@POST("api/front/virtual/recharge/order")
|
||||
Call<ApiResponse<Map<String, Object>>> createRechargeOrder(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 模拟支付成功(测试用)
|
||||
*/
|
||||
@POST("api/front/virtual/recharge/mock-pay")
|
||||
Call<ApiResponse<String>> mockPaySuccess(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 获取充值记录
|
||||
*/
|
||||
@GET("api/front/virtual/recharge/records")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getRechargeRecords(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
/**
|
||||
* 获取消费记录
|
||||
*/
|
||||
@GET("api/front/virtual/consume/records")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getConsumeRecords(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
package com.example.livestreaming.net
|
||||
|
||||
|
|
@ -48,10 +48,15 @@ public final class AuthStore {
|
|||
|
||||
private static final String KEY_USER_ID = "user_id";
|
||||
private static final String KEY_NICKNAME = "nickname";
|
||||
private static final String KEY_AVATAR = "avatar";
|
||||
private static final String KEY_IS_STREAMER = "is_streamer";
|
||||
private static final String KEY_STREAMER_MODE = "streamer_mode";
|
||||
|
||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
|
||||
setUserInfo(context, userId, nickname, null);
|
||||
}
|
||||
|
||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname, @Nullable String avatar) {
|
||||
if (context == null) return;
|
||||
|
||||
// 清理和验证 userId
|
||||
|
|
@ -65,7 +70,7 @@ public final class AuthStore {
|
|||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "setUserInfo: userId=" + cleanUserId + ", nickname=" + nickname);
|
||||
Log.d(TAG, "setUserInfo: userId=" + cleanUserId + ", nickname=" + nickname + ", avatar=" + avatar);
|
||||
|
||||
android.content.SharedPreferences.Editor editor = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit();
|
||||
|
||||
|
|
@ -81,6 +86,12 @@ public final class AuthStore {
|
|||
editor.remove(KEY_NICKNAME);
|
||||
}
|
||||
|
||||
if (avatar != null && !avatar.trim().isEmpty()) {
|
||||
editor.putString(KEY_AVATAR, avatar.trim());
|
||||
} else {
|
||||
editor.remove(KEY_AVATAR);
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +125,13 @@ public final class AuthStore {
|
|||
return nickname != null ? nickname : "用户";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getAvatar(Context context) {
|
||||
if (context == null) return null;
|
||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY_AVATAR, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户是否是认证主播
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ public class SendGiftRequest {
|
|||
@SerializedName("giftId")
|
||||
private Integer giftId;
|
||||
|
||||
@SerializedName("streamerId")
|
||||
private Integer streamerId;
|
||||
@SerializedName("giftCount") // 后端期望的字段名是giftCount,不是count
|
||||
private Integer giftCount;
|
||||
|
||||
@SerializedName("count")
|
||||
private Integer count;
|
||||
@SerializedName("receiverId") // 后端期望的字段名是receiverId,不是streamerId
|
||||
private Integer receiverId;
|
||||
|
||||
public SendGiftRequest(Integer giftId, Integer streamerId, Integer count) {
|
||||
public SendGiftRequest(Integer giftId, Integer receiverId, Integer giftCount) {
|
||||
this.giftId = giftId;
|
||||
this.streamerId = streamerId;
|
||||
this.count = count;
|
||||
this.receiverId = receiverId;
|
||||
this.giftCount = giftCount;
|
||||
}
|
||||
|
||||
public Integer getGiftId() { return giftId; }
|
||||
public Integer getStreamerId() { return streamerId; }
|
||||
public Integer getCount() { return count; }
|
||||
public Integer getReceiverId() { return receiverId; }
|
||||
public Integer getGiftCount() { return giftCount; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +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="#CCCCCC" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 紫粉渐变,参考主流直播平台风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深紫到粉紫 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6663399"
|
||||
android:centerColor="#E68B4789"
|
||||
android:endColor="#E6B565A7"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:金色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFD700" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 蓝色渐变,清新风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深蓝到浅蓝 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E61E3A8A"
|
||||
android:centerColor="#E63B5998"
|
||||
android:endColor="#E65B7BB4"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:蓝色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#8087CEEB" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 金色渐变,奢华风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深金到亮金 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6B8860B"
|
||||
android:centerColor="#E6DAA520"
|
||||
android:endColor="#E6FFD700"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:白色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFFFFF" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 橙红渐变,热情风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从橙色到红色 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6FF6B35"
|
||||
android:centerColor="#E6FF5252"
|
||||
android:endColor="#E6FF4081"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:金色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFD700" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#FF6B9D"
|
||||
android:endColor="#C06C84"
|
||||
android:type="linear" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_back_24.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_back_24.xml
Normal 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="#333333"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,17 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#2196F3"
|
||||
android:pathData="M8,20l4-8h24l4,8v12H8z"/>
|
||||
<path
|
||||
android:fillColor="#1976D2"
|
||||
android:pathData="M12,12h8v8h-8z"/>
|
||||
<path
|
||||
android:fillColor="#1976D2"
|
||||
android:pathData="M28,12h8v8h-8z"/>
|
||||
android:pathData="M8,24L12,16L36,16L40,24L40,32L8,32L8,24Z" />
|
||||
<path
|
||||
android:fillColor="#424242"
|
||||
android:pathData="M14,28a4,4 0,1,1 0,8 4,4 0,1,1 0,-8z"/>
|
||||
android:pathData="M12,28C13.1,28 14,28.9 14,30C14,31.1 13.1,32 12,32C10.9,32 10,31.1 10,30C10,28.9 10.9,28 12,28Z" />
|
||||
<path
|
||||
android:fillColor="#424242"
|
||||
android:pathData="M34,28a4,4 0,1,1 0,8 4,4 0,1,1 0,-8z"/>
|
||||
android:pathData="M36,28C37.1,28 38,28.9 38,30C38,31.1 37.1,32 36,32C34.9,32 34,31.1 34,30C34,28.9 34.9,28 36,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
13
android-app/app/src/main/res/drawable/ic_gift_castle.xml
Normal file
13
android-app/app/src/main/res/drawable/ic_gift_castle.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#9C27B0"
|
||||
android:pathData="M8,16L8,36L40,36L40,16L36,16L36,12L32,12L32,16L28,16L28,12L24,12L24,16L20,16L20,12L16,12L16,16L12,16L12,12L8,12L8,16Z" />
|
||||
<path
|
||||
android:fillColor="#7B1FA2"
|
||||
android:pathData="M20,24L20,36L28,36L28,24L20,24Z" />
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,17 +6,8 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M8,36h32v4H8z"/>
|
||||
android:pathData="M8,32L8,36L40,36L40,32L35,20L30,26L24,16L18,26L13,20L8,32Z" />
|
||||
<path
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M6,16l6,12h24l6-12-8,4-4-8-4,8-4-8-4,8-4-8z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M24,10a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M12,14a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M36,14a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
android:fillColor="#FFA000"
|
||||
android:pathData="M24,12C25.1,12 26,12.9 26,14C26,15.1 25.1,16 24,16C22.9,16 22,15.1 22,14C22,12.9 22.9,12 24,12Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#F44336"
|
||||
android:pathData="M24,40l-2.55-2.32C12.4,29.8 6,24.22 6,17.5 6,12.42 10.02,8.4 15.1,8.4c2.83,0 5.55,1.31 7.4,3.38 1.85-2.07 4.57-3.38 7.4-3.38C35.98,8.4 40,12.42 40,17.5c0,6.72-6.4,12.3-15.45,20.18L24,40z"/>
|
||||
android:fillColor="#FF4444"
|
||||
android:pathData="M24,42L20,38C10,29 4,23.5 4,17C4,12 7.5,8.5 12,8.5C15,8.5 18,10 20,12.5C22,10 25,8.5 28,8.5C32.5,8.5 36,12 36,17C36,23.5 30,29 20,38L24,42Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,11 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#FF5722"
|
||||
android:pathData="M24,4c-4,0-8,4-10,10l-4,8 4,2 2-4c0,4 2,8 4,10v8l4,4 4-4v-8c2-2 4-6 4-10l2,4 4-2-4-8c-2-6-6-10-10-10z"/>
|
||||
android:pathData="M24,4L20,12L16,20L20,20L20,28L28,28L28,20L32,20L28,12L24,4Z" />
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M24,15a3,3 0,1,1 0,6 3,3 0,1,1 0,-6z"/>
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M18,28L18,36L22,40L22,28L18,28Z" />
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M20,38l-2,6 2-2 4,2 4-2 2,2-2-6z"/>
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M30,28L30,36L26,40L26,28L30,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,11 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#E91E63"
|
||||
android:pathData="M24,8c-3.31,0-6,2.69-6,6 0,1.66 0.68,3.16 1.77,4.24L24,22.48l4.23-4.24C29.32,17.16 30,15.66 30,14c0-3.31-2.69-6-6-6z"/>
|
||||
android:pathData="M24,8C20,8 17,11 17,15C17,19 20,22 24,22C28,22 31,19 31,15C31,11 28,8 24,8Z" />
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M24,22.48l-4.23,4.24c-0.39,0.39-0.39,1.02 0,1.41l0,0c0.39,0.39 1.02,0.39 1.41,0L24,25.31l2.82,2.82c0.39,0.39 1.02,0.39 1.41,0l0,0c0.39-0.39 0.39-1.02 0-1.41L24,22.48z"/>
|
||||
android:pathData="M22,22L22,40L26,40L26,22Z" />
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M22,25v15h4V25z"/>
|
||||
android:fillColor="#8BC34A"
|
||||
android:pathData="M22,28C18,28 15,30 15,32C15,34 18,36 22,36L22,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
16
android-app/app/src/main/res/drawable/ic_gift_yacht.xml
Normal file
16
android-app/app/src/main/res/drawable/ic_gift_yacht.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#03A9F4"
|
||||
android:pathData="M8,28L12,20L36,20L40,28L8,28Z" />
|
||||
<path
|
||||
android:fillColor="#0288D1"
|
||||
android:pathData="M6,28L6,32L42,32L42,28L6,28Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M24,8L24,20L28,20L24,8Z" />
|
||||
</vector>
|
||||
119
android-app/app/src/main/res/layout/activity_recharge.xml
Normal file
119
android-app/app/src/main/res/layout/activity_recharge.xml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?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="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<!-- 顶部标题栏 -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/colorPrimary"
|
||||
android:elevation="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back_24"
|
||||
android:contentDescription="返回" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="充值"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 充值套餐标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择充值套餐"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- 充值套餐列表 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_packages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
<!-- 支付方式标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择支付方式"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_alipay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="支付宝"
|
||||
android:textSize="16sp"
|
||||
app:cornerRadius="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_wechat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="微信支付"
|
||||
android:textSize="16sp"
|
||||
app:cornerRadius="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_margin="16dp"
|
||||
android:text="确认充值"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:enabled="false"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -363,4 +363,14 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 礼物特效覆盖层 - 显示在发送按钮上方 -->
|
||||
<include
|
||||
android:id="@+id/giftAnimationOverlay"
|
||||
layout="@layout/gift_animation_overlay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
24
android-app/app/src/main/res/layout/fragment_record_list.xml
Normal file
24
android-app/app/src/main/res/layout/fragment_record_list.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F5F5F5">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="暂无记录"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#999999"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
146
android-app/app/src/main/res/layout/gift_animation_overlay.xml
Normal file
146
android-app/app/src/main/res/layout/gift_animation_overlay.xml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?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:id="@+id/giftAnimationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<!-- 礼物特效主容器 -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/giftEffectLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<!-- 左侧礼物图标和信息 - 无背景版本 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/giftInfoCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<ImageView
|
||||
android:id="@+id/senderAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_user_24"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<!-- 礼物图标 -->
|
||||
<ImageView
|
||||
android:id="@+id/giftIcon"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<!-- 礼物信息 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/senderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="用户名"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="送出了"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="礼物名称"
|
||||
android:textColor="#FFD700"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 连击数 -->
|
||||
<TextView
|
||||
android:id="@+id/comboCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="x1"
|
||||
android:textColor="#FF4444"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="10" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 粒子特效容器 - 用于显示飘散的粒子 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/particleContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- 全屏特效容器 - 用于高级礼物的全屏动画 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/fullscreenEffectContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?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="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 标签 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/bg_hot_badge"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:text="热门"
|
||||
android:textSize="10sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 金额 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="100"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
|
||||
<!-- 虚拟币单位 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_amount"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="虚拟币"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#999999"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<!-- 价格 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="¥10.00"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
<resources>
|
||||
<color name="colorPrimary">#FF6B9D</color>
|
||||
<color name="colorPrimaryDark">#C06C84</color>
|
||||
<color name="colorAccent">#FF6B9D</color>
|
||||
|
||||
<color name="purple_500">#6200EE</color>
|
||||
<color name="purple_700">#3700B3</color>
|
||||
<color name="teal_200">#03DAC5</color>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
plugins {
|
||||
id("com.android.application") version "8.3.0" apply false
|
||||
id("com.android.application") version "8.1.2" apply false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.7-bin.zip
|
||||
distributionUrl=file:///D:/soft/gradle-8.1-bin.zip
|
||||
networkTimeout=600000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user