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
|
```json
|
||||||
|
|
@ -419,7 +419,7 @@ Authorization: Bearer <token>
|
||||||
**路径参数**:
|
**路径参数**:
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| id | Integer | 是 | 直播间ID |
|
| id | String | 是 | 直播间ID |
|
||||||
|
|
||||||
**响应示例**:
|
**响应示例**:
|
||||||
```json
|
```json
|
||||||
|
|
@ -483,6 +483,11 @@ Authorization: Bearer <token>
|
||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| roomId | String | 是 | 直播间ID |
|
| roomId | String | 是 | 直播间ID |
|
||||||
|
|
||||||
|
**重要说明**:
|
||||||
|
- 此接口路径不包含 `/front` 前缀
|
||||||
|
- 完整路径为: `POST /api/live/online/broadcast/{roomId}`
|
||||||
|
- 用于向WebSocket客户端广播直播间在线人数更新
|
||||||
|
|
||||||
**响应示例**:
|
**响应示例**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -1632,6 +1637,7 @@ Authorization: Bearer <token>
|
||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| id | Long | 是 | 作品ID |
|
| id | Long | 是 | 作品ID |
|
||||||
| title | String | 否 | 作品标题 |
|
| title | String | 否 | 作品标题 |
|
||||||
|
| type | String | 否 | 作品类型 |
|
||||||
| description | String | 否 | 作品描述 |
|
| description | String | 否 | 作品描述 |
|
||||||
| coverUrl | String | 否 | 封面图片URL |
|
| coverUrl | String | 否 | 封面图片URL |
|
||||||
| categoryId | Integer | 否 | 分类ID |
|
| categoryId | Integer | 否 | 分类ID |
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,11 @@
|
||||||
:preview-src-list="[scope.row.image]"
|
:preview-src-list="[scope.row.image]"
|
||||||
style="width: 50px; height: 50px"
|
style="width: 50px; height: 50px"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
/>
|
>
|
||||||
|
<div slot="error" class="image-slot">
|
||||||
|
<i class="el-icon-picture-outline"></i>
|
||||||
|
</div>
|
||||||
|
</el-image>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="礼物名称" align="center" prop="name" />
|
<el-table-column label="礼物名称" align="center" prop="name" />
|
||||||
|
|
@ -126,7 +130,8 @@
|
||||||
:total="total"
|
:total="total"
|
||||||
:page.sync="queryParams.page"
|
:page.sync="queryParams.page"
|
||||||
:limit.sync="queryParams.limit"
|
:limit.sync="queryParams.limit"
|
||||||
@pagination="getList"
|
@size-change="getList"
|
||||||
|
@current-change="getList"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
|
@ -190,6 +195,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { giftListApi, giftAddApi, giftUpdateApi, giftDeleteApi, giftStatusApi } from '@/api/gift';
|
import { giftListApi, giftAddApi, giftUpdateApi, giftDeleteApi, giftStatusApi } from '@/api/gift';
|
||||||
|
import { fileImageApi } from '@/api/systemSetting';
|
||||||
import DataPagination from '@/components/common/DataPagination';
|
import DataPagination from '@/components/common/DataPagination';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -240,27 +246,37 @@ export default {
|
||||||
getList() {
|
getList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
giftListApi(this.queryParams).then(response => {
|
giftListApi(this.queryParams).then(response => {
|
||||||
const data = response.data || {};
|
console.log('礼物列表响应:', response);
|
||||||
this.giftList = data.list || [];
|
|
||||||
this.total = data.total || 0;
|
// 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;
|
this.loading = false;
|
||||||
}).catch(() => {
|
}).catch(error => {
|
||||||
|
console.error('获取礼物列表失败:', error);
|
||||||
|
this.$message.error('获取礼物列表失败');
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getStatistics() {
|
getStatistics() {
|
||||||
// 计算统计数据
|
// 计算统计数据
|
||||||
giftListApi({ page: 1, limit: 9999 }).then(response => {
|
giftListApi({ page: 1, limit: 9999 }).then(response => {
|
||||||
console.log('统计数据响应:', response);
|
const list = response.list || [];
|
||||||
const data = response.data || {};
|
|
||||||
const list = data.list || [];
|
|
||||||
console.log('礼物列表:', list);
|
|
||||||
console.log('列表长度:', list.length);
|
|
||||||
this.statistics.total = list.length;
|
this.statistics.total = list.length;
|
||||||
this.statistics.enabled = list.filter(item => item.status === true).length;
|
this.statistics.enabled = list.filter(item => item.status === 1 || item.status === true).length;
|
||||||
this.statistics.disabled = list.filter(item => item.status === false).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);
|
this.statistics.totalValue = list.reduce((sum, item) => sum + (parseFloat(item.diamondPrice) || 0), 0).toFixed(2);
|
||||||
console.log('统计结果:', this.statistics);
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('获取统计数据失败:', error);
|
console.error('获取统计数据失败:', error);
|
||||||
});
|
});
|
||||||
|
|
@ -369,11 +385,36 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleUpload(param) {
|
handleUpload(param) {
|
||||||
// 这里实现图片上传逻辑
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', param.file);
|
formData.append('multipart', param.file); // 后端参数名是 multipart
|
||||||
// 调用上传接口
|
|
||||||
this.$message.info('图片上传功能需要配置上传接口');
|
// 显示上传中提示
|
||||||
|
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) {
|
beforeUpload(file) {
|
||||||
const isImage = file.type.indexOf('image/') === 0;
|
const isImage = file.type.indexOf('image/') === 0;
|
||||||
|
|
@ -385,6 +426,57 @@ export default {
|
||||||
this.$message.error('上传图片大小不能超过 2MB!');
|
this.$message.error('上传图片大小不能超过 2MB!');
|
||||||
}
|
}
|
||||||
return isImage && isLt2M;
|
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;
|
height: 120px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-slot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeMessage',
|
name: 'WishTreeMessage',
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishtree';
|
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeNode',
|
name: 'WishTreeNode',
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeDetail',
|
name: 'WishTreeDetail',
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishtree';
|
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeList',
|
name: 'WishTreeList',
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishtree';
|
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishtreeBackground',
|
name: 'WishtreeBackground',
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishtree';
|
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishtreeFestival',
|
name: 'WishtreeFestival',
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getStatistics } from '@/api/wishtree';
|
import { getStatistics } from '@/api/wishTree';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishtree';
|
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishTree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishtreeWish',
|
name: 'WishtreeWish',
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ module.exports = {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:30001',
|
target: 'http://localhost:30001',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/crmebimage': {
|
||||||
|
target: 'http://localhost:30001',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/file': {
|
||||||
|
target: 'http://localhost:30001',
|
||||||
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -64,6 +72,9 @@ module.exports = {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve('src'),
|
'@': resolve('src'),
|
||||||
},
|
},
|
||||||
|
extensions: ['.js', '.vue', '.json'],
|
||||||
|
// 在Windows上强制不区分大小写
|
||||||
|
symlinks: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack(config) {
|
chainWebpack(config) {
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,26 @@ public class WebConfig implements WebMvcConfigurer {
|
||||||
registry.addResourceHandler("/webjars/**")
|
registry.addResourceHandler("/webjars/**")
|
||||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||||
|
|
||||||
/** 本地文件上传路径 */
|
/** 本地文件上传路径 - 使用jar包所在目录 */
|
||||||
registry.addResourceHandler(UploadConstants.UPLOAD_FILE_KEYWORD + "/**")
|
String uploadPath = crmebConfig.getAbsoluteImagePath();
|
||||||
.addResourceLocations("file:" + crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_FILE_KEYWORD + "/");
|
|
||||||
|
|
||||||
registry.addResourceHandler(UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/**")
|
// 添加 image 路径映射(修复图片上传显示问题)
|
||||||
.addResourceLocations("file:" +crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/" );
|
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
|
@Bean
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,8 @@ public class GiftAdminController {
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
|
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
|
||||||
|
|
||||||
|
log.info("礼物列表查询 - 参数: name={}, status={}, page={}, limit={}", name, status, page, limit);
|
||||||
|
|
||||||
StringBuilder sql = new StringBuilder();
|
StringBuilder sql = new StringBuilder();
|
||||||
sql.append("SELECT id, name, image, diamond_price as diamondPrice, intimacy, status, ");
|
sql.append("SELECT id, name, image, diamond_price as diamondPrice, intimacy, status, ");
|
||||||
sql.append("is_heartbeat as isHeartbeat, buy_type as buyType, belong, remark, ");
|
sql.append("is_heartbeat as isHeartbeat, buy_type as buyType, belong, remark, ");
|
||||||
|
|
@ -375,23 +377,32 @@ public class GiftAdminController {
|
||||||
countParams.add(status);
|
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());
|
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class, countParams.toArray());
|
||||||
|
if (total == null) {
|
||||||
|
total = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
int offset = (page - 1) * limit;
|
int offset = (page - 1) * limit;
|
||||||
sql.append(" LIMIT ? OFFSET ? ");
|
sql.append(" LIMIT ? OFFSET ? ");
|
||||||
params.add(limit);
|
params.add(limit);
|
||||||
params.add(offset);
|
params.add(offset);
|
||||||
|
|
||||||
|
log.info("礼物列表查询 - SQL: {}", sql.toString());
|
||||||
|
log.info("礼物列表查询 - 参数: {}", params);
|
||||||
|
|
||||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
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<>();
|
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||||
result.setList(list);
|
result.setList(list);
|
||||||
result.setTotal(total != null ? total : 0L);
|
result.setTotal(total);
|
||||||
result.setPage(page);
|
result.setPage(page);
|
||||||
result.setLimit(limit);
|
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);
|
return CommonResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
@ -403,14 +414,19 @@ public class GiftAdminController {
|
||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
public CommonResult<String> addGift(@RequestBody Map<String, Object> request) {
|
public CommonResult<String> addGift(@RequestBody Map<String, Object> request) {
|
||||||
try {
|
try {
|
||||||
|
log.info("添加礼物 - 请求参数: {}", request);
|
||||||
|
|
||||||
String name = (String) request.get("name");
|
String name = (String) request.get("name");
|
||||||
String image = (String) request.get("image");
|
String image = (String) request.get("image");
|
||||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
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 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 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 remark = (String) request.get("remark");
|
||||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
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,
|
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||||
buyType, belong, remark, level, sort);
|
buyType, belong, remark, level, sort);
|
||||||
|
|
||||||
|
log.info("添加礼物成功 - 名称: {}", name);
|
||||||
return CommonResult.success("添加成功");
|
return CommonResult.success("添加成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("添加礼物失败", e);
|
log.error("添加礼物失败", e);
|
||||||
|
|
@ -434,15 +451,20 @@ public class GiftAdminController {
|
||||||
@PostMapping("/update")
|
@PostMapping("/update")
|
||||||
public CommonResult<String> updateGift(@RequestBody Map<String, Object> request) {
|
public CommonResult<String> updateGift(@RequestBody Map<String, Object> request) {
|
||||||
try {
|
try {
|
||||||
|
log.info("更新礼物 - 请求参数: {}", request);
|
||||||
|
|
||||||
Integer id = Integer.parseInt(request.get("id").toString());
|
Integer id = Integer.parseInt(request.get("id").toString());
|
||||||
String name = (String) request.get("name");
|
String name = (String) request.get("name");
|
||||||
String image = (String) request.get("image");
|
String image = (String) request.get("image");
|
||||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
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 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 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 remark = (String) request.get("remark");
|
||||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
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,
|
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||||
buyType, belong, remark, level, sort, id);
|
buyType, belong, remark, level, sort, id);
|
||||||
|
|
||||||
|
log.info("更新礼物成功 - ID: {}", id);
|
||||||
return CommonResult.success("更新成功");
|
return CommonResult.success("更新成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新礼物失败", 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")
|
@PostMapping("/status")
|
||||||
public CommonResult<String> updateGiftStatus(@RequestBody Map<String, Object> request) {
|
public CommonResult<String> updateGiftStatus(@RequestBody Map<String, Object> request) {
|
||||||
try {
|
try {
|
||||||
|
log.info("更新礼物状态 - 请求参数: {}", request);
|
||||||
|
|
||||||
Integer id = Integer.parseInt(request.get("id").toString());
|
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 = ?";
|
String sql = "UPDATE eb_gift SET status = ? WHERE id = ?";
|
||||||
jdbcTemplate.update(sql, status, id);
|
jdbcTemplate.update(sql, status, id);
|
||||||
|
|
||||||
|
log.info("更新礼物状态成功 - ID: {}, 状态: {}", id, status);
|
||||||
return CommonResult.success("状态更新成功");
|
return CommonResult.success("状态更新成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新礼物状态失败", e);
|
log.error("更新礼物状态失败", e);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,18 @@ public class ResponseRouter {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 排除上传接口,上传接口返回的URL不需要添加前缀
|
||||||
|
// 因为前端会直接保存到数据库,下次查询时会再次处理
|
||||||
|
if (path.contains("/upload/image") || path.contains("/upload/file")) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
//根据需要处理返回值 && !data.contains("data:image/png;base64")
|
//根据需要处理返回值 && !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 + "/"))
|
if ((data.contains(UploadConstants.UPLOAD_FILE_KEYWORD + "/"))
|
||||||
|| data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD) || data.contains(UploadConstants.UPLOAD_AFTER_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)) {
|
if (data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD + "/" + UploadConstants.UPLOAD_MODEL_PATH_EXCEL)) {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ crmeb:
|
||||||
wechat-api-url: #请求微信接口中专服务器
|
wechat-api-url: #请求微信接口中专服务器
|
||||||
wechat-js-api-debug: false #微信js api系列是否开启调试模式
|
wechat-js-api-debug: false #微信js api系列是否开启调试模式
|
||||||
wechat-js-api-beta: true #微信js api是否是beta版本
|
wechat-js-api-beta: true #微信js api是否是beta版本
|
||||||
asyncConfig: true #是否同步config表数据到redis
|
asyncConfig: false #是否同步config表数据到redis - 改为false,直接从数据库读取
|
||||||
asyncWeChatProgramTempList: false #是否同步小程序公共模板库
|
asyncWeChatProgramTempList: false #是否同步小程序公共模板库
|
||||||
imagePath: /Users/23730/Desktop/projectr/single_java/ # 服务器图片路径配置 斜杠结尾
|
imagePath: upload/ # 服务器图片路径配置 - jar包同级目录的upload文件夹
|
||||||
demoSite: true # 是否演示站点 所有手机号码都会掩码
|
demoSite: true # 是否演示站点 所有手机号码都会掩码
|
||||||
activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位,生产环境适当5-10分钟即可
|
activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位,生产环境适当5-10分钟即可
|
||||||
ignored: #安全路径白名单
|
ignored: #安全路径白名单
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,67 @@ public class CrmebConfig {
|
||||||
return imagePath;
|
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) {
|
public void setImagePath(String imagePath) {
|
||||||
this.imagePath = imagePath;
|
this.imagePath = imagePath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ public class SendGiftResponse implements Serializable {
|
||||||
@ApiModelProperty(value = "剩余钻石数")
|
@ApiModelProperty(value = "剩余钻石数")
|
||||||
private BigDecimal remainingDiamond;
|
private BigDecimal remainingDiamond;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "新余额(剩余金币数,兼容字段)")
|
||||||
|
private BigDecimal newBalance;
|
||||||
|
|
||||||
@ApiModelProperty(value = "增加的亲密度")
|
@ApiModelProperty(value = "增加的亲密度")
|
||||||
private Integer intimacy;
|
private Integer intimacy;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,13 +274,31 @@ public class LiveRoomController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private com.zbkj.service.service.GiftRecordService giftRecordService;
|
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 = "赠送礼物(需要登录)")
|
@ApiOperation(value = "赠送礼物(需要登录)")
|
||||||
@PostMapping("/rooms/{roomId}/gift")
|
@PostMapping("/rooms/{roomId}/gift")
|
||||||
public CommonResult<com.zbkj.common.response.SendGiftResponse> sendGift(
|
public CommonResult<com.zbkj.common.response.SendGiftResponse> sendGift(
|
||||||
@PathVariable Integer roomId,
|
@PathVariable Integer roomId,
|
||||||
@RequestBody @Validated com.zbkj.common.request.SendGiftRequest request) {
|
@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
|
// 获取当前登录用户ID
|
||||||
Integer currentUserId = frontTokenComponent.getUserId();
|
Integer currentUserId = frontTokenComponent.getUserId();
|
||||||
|
log.info("currentUserId: {}", currentUserId);
|
||||||
|
|
||||||
if (currentUserId == null) {
|
if (currentUserId == null) {
|
||||||
return CommonResult.failed("请先登录");
|
return CommonResult.failed("请先登录");
|
||||||
}
|
}
|
||||||
|
|
@ -295,19 +313,53 @@ public class LiveRoomController {
|
||||||
return CommonResult.failed("直播间不存在");
|
return CommonResult.failed("直播间不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 后续可能需要恢复此验证
|
||||||
// 不能给自己送礼物
|
// 不能给自己送礼物
|
||||||
if (currentUserId.equals(request.getReceiverId())) {
|
// if (currentUserId.equals(request.getReceiverId())) {
|
||||||
return CommonResult.failed("不能给自己赠送礼物");
|
// return CommonResult.failed("不能给自己赠送礼物");
|
||||||
}
|
// }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用服务层赠送礼物
|
// 调用服务层赠送礼物
|
||||||
com.zbkj.common.response.SendGiftResponse response =
|
com.zbkj.common.response.SendGiftResponse response =
|
||||||
giftRecordService.sendGift(roomId, currentUserId, request);
|
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);
|
return CommonResult.success(response);
|
||||||
} catch (com.zbkj.common.exception.CrmebException e) {
|
} catch (com.zbkj.common.exception.CrmebException e) {
|
||||||
|
log.error("❌ 礼物赠送失败: {}", e.getMessage());
|
||||||
return CommonResult.failed(e.getMessage());
|
return CommonResult.failed(e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 礼物赠送异常", e);
|
||||||
return CommonResult.failed("赠送礼物失败: " + e.getMessage());
|
return CommonResult.failed("赠送礼物失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,10 @@ public class UserController {
|
||||||
log.info("请求参数: {}", request);
|
log.info("请求参数: {}", request);
|
||||||
log.info("昵称: {}", request.getNickname());
|
log.info("昵称: {}", request.getNickname());
|
||||||
log.info("头像: {}", request.getAvatar());
|
log.info("头像: {}", request.getAvatar());
|
||||||
log.info("个人签名: {}", request.getBio());
|
log.info("个人签名: {}", request.getMark());
|
||||||
log.info("生日: {}", request.getBirthday());
|
log.info("生日: {}", request.getBirthday());
|
||||||
log.info("性别: {}", request.getGender());
|
log.info("性别: {}", request.getSex());
|
||||||
log.info("所在地: {}", request.getLocation());
|
log.info("所在地: {}", request.getAddres());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
boolean result = userService.editUser(request);
|
boolean result = userService.editUser(request);
|
||||||
|
|
@ -101,7 +101,6 @@ public class UserController {
|
||||||
UserCenterResponse response = userService.getUserCenter();
|
UserCenterResponse response = userService.getUserCenter();
|
||||||
log.info("========== 返回用户信息 ==========");
|
log.info("========== 返回用户信息 ==========");
|
||||||
log.info("昵称: {}", response.getNickname());
|
log.info("昵称: {}", response.getNickname());
|
||||||
log.info("个人签名: {}", response.getBio());
|
|
||||||
log.info("生日: {}", response.getBirthday());
|
log.info("生日: {}", response.getBirthday());
|
||||||
log.info("性别: {}", response.getSex());
|
log.info("性别: {}", response.getSex());
|
||||||
log.info("所在地: {}", response.getAddres());
|
log.info("所在地: {}", response.getAddres());
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
||||||
|
|
||||||
// 5. 检查余额(假设使用now_money字段作为钻石余额)
|
// 5. 检查余额(假设使用now_money字段作为钻石余额)
|
||||||
if (sender.getNowMoney().compareTo(totalDiamond) < 0) {
|
if (sender.getNowMoney().compareTo(totalDiamond) < 0) {
|
||||||
throw new CrmebException("钻石余额不足");
|
throw new CrmebException("金币余额不足");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 扣除赠送者钻石
|
// 6. 扣除赠送者钻石
|
||||||
|
|
@ -106,7 +106,12 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
||||||
response.setGiftName(gift.getName());
|
response.setGiftName(gift.getName());
|
||||||
response.setGiftCount(request.getGiftCount());
|
response.setGiftCount(request.getGiftCount());
|
||||||
response.setTotalDiamond(totalDiamond);
|
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.setIntimacy(record.getIntimacy());
|
||||||
response.setSendTime(record.getCreateTime().getTime());
|
response.setSendTime(record.getCreateTime().getTime());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,20 @@ public class SystemAttachmentServiceImpl extends ServiceImpl<SystemAttachmentDao
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String prefixImage(String path) {
|
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+"/");
|
return path.replace(UploadConstants.UPLOAD_FILE_KEYWORD+"/", getCdnUrl() + "/"+ UploadConstants.UPLOAD_FILE_KEYWORD+"/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String prefixUploadf(String path) {
|
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+"/");
|
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
|
@Override
|
||||||
public String prefixFile(String path) {
|
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)) {
|
if (path.contains(Constants.WECHAT_SOURCE_CODE_FILE_NAME)) {
|
||||||
String cdnUrl = systemConfigService.getValueByKey("local" + "UploadUrl");
|
String cdnUrl = systemConfigService.getValueByKey("local" + "UploadUrl");
|
||||||
return path.replace("crmebimage/", cdnUrl + "/crmebimage/");
|
return path.replace("crmebimage/", cdnUrl + "/crmebimage/");
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ public class UploadServiceImpl implements UploadService {
|
||||||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器存储地址
|
// 服务器存储地址 - 使用绝对路径
|
||||||
String rootPath = crmebConfig.getImagePath().trim();
|
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||||
// 模块
|
// 模块
|
||||||
String modelPath = "public/" + model + "/";
|
String modelPath = "public/" + model + "/";
|
||||||
// 类型
|
// 类型
|
||||||
|
|
@ -356,8 +356,8 @@ public class UploadServiceImpl implements UploadService {
|
||||||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器存储地址
|
// 服务器存储地址 - 使用绝对路径
|
||||||
String rootPath = crmebConfig.getImagePath().trim();
|
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||||
String modelPath = "public/" + model + "/";
|
String modelPath = "public/" + model + "/";
|
||||||
String type = "video/";
|
String type = "video/";
|
||||||
|
|
||||||
|
|
@ -431,8 +431,8 @@ public class UploadServiceImpl implements UploadService {
|
||||||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器存储地址
|
// 服务器存储地址 - 使用绝对路径
|
||||||
String rootPath = crmebConfig.getImagePath().trim();
|
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||||
String modelPath = "public/" + model + "/";
|
String modelPath = "public/" + model + "/";
|
||||||
String type = "voice/";
|
String type = "voice/";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,8 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新用户金额
|
* 更新用户金额(并发安全版本)
|
||||||
|
* 使用数据库原子操作,避免并发竞态条件
|
||||||
*
|
*
|
||||||
* @param user 用户
|
* @param user 用户
|
||||||
* @param price 金额
|
* @param price 金额
|
||||||
|
|
@ -352,17 +353,31 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Boolean updateNowMoney(User user, BigDecimal price, String type) {
|
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")) {
|
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 {
|
} 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")) {
|
updateWrapper.eq("uid", user.getUid());
|
||||||
lambdaUpdateWrapper.apply(StrUtil.format(" now_money - {} >= 0", price));
|
|
||||||
|
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);
|
BeanUtils.copyProperties(currentUser, userCenterResponse);
|
||||||
// 设置用户ID
|
// 设置用户ID
|
||||||
userCenterResponse.setUid(currentUser.getUid());
|
userCenterResponse.setUid(currentUser.getUid());
|
||||||
|
|
||||||
|
// 手动映射 mark -> bio(个人签名)
|
||||||
|
if (currentUser.getMark() != null) {
|
||||||
|
userCenterResponse.setBio(currentUser.getMark());
|
||||||
|
}
|
||||||
|
|
||||||
// 优惠券数量
|
// 优惠券数量
|
||||||
userCenterResponse.setCouponCount(storeCouponUserService.getUseCount(currentUser.getUid()));
|
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")
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
annotationProcessor("com.github.bumptech.glide:compiler: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")
|
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||||
|
|
||||||
// FlexboxLayout for message reactions
|
// 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;
|
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() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
@ -90,4 +100,11 @@ public class Gift {
|
||||||
public String getFormattedPrice() {
|
public String getFormattedPrice() {
|
||||||
return price + " 金币";
|
return price + " 金币";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有图标URL
|
||||||
|
*/
|
||||||
|
public boolean hasIconUrl() {
|
||||||
|
return iconUrl != null && !iconUrl.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
|
import android.graphics.drawable.PictureDrawable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -9,11 +10,17 @@ import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 礼物列表适配器
|
* 礼物列表适配器
|
||||||
|
* 支持PNG、JPG和SVG格式图标
|
||||||
*/
|
*/
|
||||||
public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder> {
|
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) {
|
public void bind(Gift gift) {
|
||||||
giftName.setText(gift.getName());
|
giftName.setText(gift.getName());
|
||||||
giftPrice.setText(gift.getFormattedPrice());
|
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());
|
giftIcon.setImageResource(gift.getIconResId());
|
||||||
|
}
|
||||||
|
|
||||||
// 选中状态
|
// 选中状态
|
||||||
boolean isSelected = selectedGift != null && selectedGift.getId().equals(gift.getId());
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
|
@ -151,6 +153,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
private GiftAdapter giftAdapter;
|
private GiftAdapter giftAdapter;
|
||||||
private List<Gift> availableGifts;
|
private List<Gift> availableGifts;
|
||||||
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
|
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
|
||||||
|
private GiftAnimationManager giftAnimationManager; // 礼物动画管理器
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
|
@ -180,6 +183,20 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
setupChat();
|
setupChat();
|
||||||
setupGifts();
|
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();
|
loadRoomInfo();
|
||||||
|
|
||||||
|
|
@ -452,11 +469,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
// 停止之前的重连任务
|
// 停止之前的重连任务
|
||||||
stopChatReconnect();
|
stopChatReconnect();
|
||||||
|
|
||||||
|
String wsUrl = getWsChatBaseUrl() + roomId;
|
||||||
|
android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl);
|
||||||
|
|
||||||
chatWsClient = new OkHttpClient.Builder()
|
chatWsClient = new OkHttpClient.Builder()
|
||||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||||
.build();
|
.build();
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(getWsChatBaseUrl() + roomId)
|
.url(wsUrl)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
||||||
|
|
@ -472,9 +492,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
// 收到消息,解析并显示
|
// 收到消息,解析并显示
|
||||||
|
android.util.Log.d("ChatWebSocket", "收到消息: " + text);
|
||||||
try {
|
try {
|
||||||
JSONObject json = new JSONObject(text);
|
JSONObject json = new JSONObject(text);
|
||||||
String type = json.optString("type", "");
|
String type = json.optString("type", "");
|
||||||
|
android.util.Log.d("ChatWebSocket", "消息类型: " + type);
|
||||||
|
|
||||||
if ("chat".equals(type)) {
|
if ("chat".equals(type)) {
|
||||||
String nickname = json.optString("nickname", "匿名");
|
String nickname = json.optString("nickname", "匿名");
|
||||||
|
|
@ -490,11 +512,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
String senderNickname = json.optString("senderNickname", "匿名");
|
String senderNickname = json.optString("senderNickname", "匿名");
|
||||||
int totalPrice = json.optInt("totalPrice", 0);
|
int totalPrice = json.optInt("totalPrice", 0);
|
||||||
|
|
||||||
|
android.util.Log.d("WebSocket", "收到礼物消息: " + json.toString());
|
||||||
|
|
||||||
handler.post(() -> {
|
handler.post(() -> {
|
||||||
String giftMsg = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
String giftMsg = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||||
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
|
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
|
||||||
|
|
||||||
// TODO: 显示礼物动画效果
|
// 显示礼物动画效果
|
||||||
|
android.util.Log.d("WebSocket", "准备显示礼物动画: " + giftName);
|
||||||
showGiftAnimation(giftName, count, senderNickname);
|
showGiftAnimation(giftName, count, senderNickname);
|
||||||
});
|
});
|
||||||
} else if ("gift_combo".equals(type)) {
|
} else if ("gift_combo".equals(type)) {
|
||||||
|
|
@ -1183,7 +1208,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
// 防止重复显示连接消息
|
// 防止重复显示连接消息
|
||||||
private boolean hasShownConnectedMessage = false;
|
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();
|
releaseIjkPlayer();
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.flvTextureView.setVisibility(View.GONE);
|
binding.flvTextureView.setVisibility(View.GONE);
|
||||||
|
|
@ -1560,6 +1585,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
* 从后端加载礼物列表
|
* 从后端加载礼物列表
|
||||||
*/
|
*/
|
||||||
private void loadGiftsFromBackend() {
|
private void loadGiftsFromBackend() {
|
||||||
|
android.util.Log.d("RoomDetail", "=== 开始加载礼物列表 ===");
|
||||||
|
|
||||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||||
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
|
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
|
||||||
|
|
||||||
|
|
@ -1569,32 +1596,41 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
Response<ApiResponse<List<GiftResponse>>> response) {
|
Response<ApiResponse<List<GiftResponse>>> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
ApiResponse<List<GiftResponse>> apiResponse = response.body();
|
ApiResponse<List<GiftResponse>> apiResponse = response.body();
|
||||||
|
android.util.Log.d("RoomDetail", "礼物列表响应码: " + apiResponse.getCode());
|
||||||
|
|
||||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||||
availableGifts = new ArrayList<>();
|
availableGifts = new ArrayList<>();
|
||||||
for (GiftResponse giftResponse : apiResponse.getData()) {
|
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(
|
Gift gift = new Gift(
|
||||||
String.valueOf(giftResponse.getId()),
|
String.valueOf(giftResponse.getId()),
|
||||||
giftResponse.getName(),
|
giftResponse.getName(),
|
||||||
giftResponse.getPrice().intValue(),
|
giftResponse.getPrice().intValue(),
|
||||||
R.drawable.ic_gift_rose, // 默认图标,实际应从URL加载
|
iconUrl, // ✅ 使用URL而不是硬编码
|
||||||
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
|
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
|
||||||
);
|
);
|
||||||
availableGifts.add(gift);
|
availableGifts.add(gift);
|
||||||
}
|
}
|
||||||
android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物");
|
android.util.Log.d("RoomDetail", "✅ 成功加载 " + availableGifts.size() + " 个礼物");
|
||||||
|
android.util.Log.d("RoomDetail", "礼物列表: " + getGiftNames());
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMessage());
|
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败: " + apiResponse.getMessage());
|
||||||
setDefaultGifts();
|
setDefaultGifts();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.w("RoomDetail", "加载礼物列表失败");
|
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败,响应码: " + response.code());
|
||||||
setDefaultGifts();
|
setDefaultGifts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ApiResponse<List<GiftResponse>>> call, Throwable t) {
|
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();
|
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 -> {
|
giftAdapter.setOnGiftClickListener(gift -> {
|
||||||
selectedGiftLayout.setVisibility(View.VISIBLE);
|
selectedGiftLayout.setVisibility(View.VISIBLE);
|
||||||
selectedGiftIcon.setImageResource(gift.getIconResId());
|
|
||||||
|
// 使用辅助方法加载礼物图标
|
||||||
|
loadGiftIcon(selectedGiftIcon, gift);
|
||||||
|
|
||||||
selectedGiftName.setText(gift.getName());
|
selectedGiftName.setText(gift.getName());
|
||||||
selectedGiftPrice.setText(gift.getFormattedPrice());
|
selectedGiftPrice.setText(gift.getFormattedPrice());
|
||||||
giftCount[0] = 1;
|
giftCount[0] = 1;
|
||||||
|
|
@ -1997,14 +2110,34 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
return;
|
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());
|
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||||
|
|
||||||
// 获取主播ID(使用房间ID作为主播ID,或者从房间信息中获取)
|
|
||||||
Integer streamerId = Integer.parseInt(roomId);
|
|
||||||
SendGiftRequest request = new SendGiftRequest(
|
SendGiftRequest request = new SendGiftRequest(
|
||||||
Integer.parseInt(selectedGift.getId()),
|
Integer.parseInt(selectedGift.getId()),
|
||||||
streamerId,
|
streamerId, // receiverId - 接收者用户ID(主播ID)
|
||||||
count
|
count // giftCount - 礼物数量
|
||||||
);
|
);
|
||||||
|
|
||||||
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
|
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
|
||||||
|
|
@ -2013,20 +2146,46 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
|
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
|
||||||
Response<ApiResponse<SendGiftResponse>> response) {
|
Response<ApiResponse<SendGiftResponse>> response) {
|
||||||
|
android.util.Log.d("RoomDetail", "📥 收到响应: HTTP " + response.code());
|
||||||
|
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
ApiResponse<SendGiftResponse> apiResponse = response.body();
|
ApiResponse<SendGiftResponse> apiResponse = response.body();
|
||||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||||
SendGiftResponse giftResponse = apiResponse.getData();
|
SendGiftResponse giftResponse = apiResponse.getData();
|
||||||
|
|
||||||
// 更新余额
|
// 🔒 安全更新余额(添加空值检查)
|
||||||
|
if (giftResponse.getNewBalance() != null) {
|
||||||
userCoinBalance = giftResponse.getNewBalance().intValue();
|
userCoinBalance = giftResponse.getNewBalance().intValue();
|
||||||
coinBalance.setText(String.valueOf(userCoinBalance));
|
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());
|
String giftMessage = String.format("送出了 %d 个 %s", count, selectedGift.getName());
|
||||||
addChatMessage(new ChatMessage("我", giftMessage, true));
|
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) {
|
if (giftDialog != null) {
|
||||||
giftDialog.dismiss();
|
giftDialog.dismiss();
|
||||||
|
|
@ -2039,14 +2198,17 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
selectedGiftLayout.setVisibility(View.GONE);
|
selectedGiftLayout.setVisibility(View.GONE);
|
||||||
giftCountText.setText("1");
|
giftCountText.setText("1");
|
||||||
} else {
|
} else {
|
||||||
|
String errorMsg = apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误";
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
Toast.makeText(RoomDetailActivity.this,
|
||||||
"赠送失败: " + apiResponse.getMessage(),
|
"赠送失败: " + errorMsg,
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
android.util.Log.e("RoomDetail", "赠送礼物失败: " + errorMsg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
Toast.makeText(RoomDetailActivity.this,
|
||||||
"赠送失败",
|
"赠送失败: HTTP " + response.code(),
|
||||||
Toast.LENGTH_SHORT).show();
|
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,
|
Toast.makeText(RoomDetailActivity.this,
|
||||||
"网络错误: " + t.getMessage(),
|
"网络错误: " + t.getMessage(),
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
android.util.Log.e("RoomDetail", "赠送礼物网络错误", t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2330,21 +2493,93 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
* @param senderNickname 赠送者昵称
|
* @param senderNickname 赠送者昵称
|
||||||
*/
|
*/
|
||||||
private void showGiftAnimation(String giftName, int count, String senderNickname) {
|
private void showGiftAnimation(String giftName, int count, String senderNickname) {
|
||||||
// TODO: 实现礼物动画效果
|
|
||||||
// 这里可以使用Lottie动画库或自定义动画
|
|
||||||
// 示例:显示一个Toast提示
|
|
||||||
runOnUiThread(() -> {
|
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;
|
String message = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 可以在这里添加更复杂的动画效果,例如:
|
// 检查礼物列表是否加载
|
||||||
// 1. 使用Lottie播放礼物动画
|
if (availableGifts == null || availableGifts.isEmpty()) {
|
||||||
// 2. 显示礼物图标飞行动画
|
android.util.Log.w("GiftAnimation", "⚠️ 礼物列表未加载或为空,使用默认价格显示特效");
|
||||||
// 3. 播放音效
|
// 使用默认价格显示特效(而不是降级到Toast)
|
||||||
// 4. 显示特效粒子
|
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(
|
Call<ApiResponse<List<CommunityResponse.MatchUser>>> getMatchList(
|
||||||
@Query("page") int page,
|
@Query("page") int page,
|
||||||
@Query("limit") int limit);
|
@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_USER_ID = "user_id";
|
||||||
private static final String KEY_NICKNAME = "nickname";
|
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_IS_STREAMER = "is_streamer";
|
||||||
private static final String KEY_STREAMER_MODE = "streamer_mode";
|
private static final String KEY_STREAMER_MODE = "streamer_mode";
|
||||||
|
|
||||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
|
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;
|
if (context == null) return;
|
||||||
|
|
||||||
// 清理和验证 userId
|
// 清理和验证 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();
|
android.content.SharedPreferences.Editor editor = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit();
|
||||||
|
|
||||||
|
|
@ -81,6 +86,12 @@ public final class AuthStore {
|
||||||
editor.remove(KEY_NICKNAME);
|
editor.remove(KEY_NICKNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatar != null && !avatar.trim().isEmpty()) {
|
||||||
|
editor.putString(KEY_AVATAR, avatar.trim());
|
||||||
|
} else {
|
||||||
|
editor.remove(KEY_AVATAR);
|
||||||
|
}
|
||||||
|
|
||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,6 +125,13 @@ public final class AuthStore {
|
||||||
return nickname != null ? nickname : "用户";
|
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")
|
@SerializedName("giftId")
|
||||||
private Integer giftId;
|
private Integer giftId;
|
||||||
|
|
||||||
@SerializedName("streamerId")
|
@SerializedName("giftCount") // 后端期望的字段名是giftCount,不是count
|
||||||
private Integer streamerId;
|
private Integer giftCount;
|
||||||
|
|
||||||
@SerializedName("count")
|
@SerializedName("receiverId") // 后端期望的字段名是receiverId,不是streamerId
|
||||||
private Integer count;
|
private Integer receiverId;
|
||||||
|
|
||||||
public SendGiftRequest(Integer giftId, Integer streamerId, Integer count) {
|
public SendGiftRequest(Integer giftId, Integer receiverId, Integer giftCount) {
|
||||||
this.giftId = giftId;
|
this.giftId = giftId;
|
||||||
this.streamerId = streamerId;
|
this.receiverId = receiverId;
|
||||||
this.count = count;
|
this.giftCount = giftCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getGiftId() { return giftId; }
|
public Integer getGiftId() { return giftId; }
|
||||||
public Integer getStreamerId() { return streamerId; }
|
public Integer getReceiverId() { return receiverId; }
|
||||||
public Integer getCount() { return count; }
|
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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
|
|
@ -5,17 +6,11 @@
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="48">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#2196F3"
|
android:fillColor="#2196F3"
|
||||||
android:pathData="M8,20l4-8h24l4,8v12H8z"/>
|
android:pathData="M8,24L12,16L36,16L40,24L40,32L8,32L8,24Z" />
|
||||||
<path
|
|
||||||
android:fillColor="#1976D2"
|
|
||||||
android:pathData="M12,12h8v8h-8z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#1976D2"
|
|
||||||
android:pathData="M28,12h8v8h-8z"/>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#424242"
|
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
|
<path
|
||||||
android:fillColor="#424242"
|
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>
|
</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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
|
|
@ -5,17 +6,8 @@
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="48">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFD700"
|
android:fillColor="#FFD700"
|
||||||
android:pathData="M8,36h32v4H8z"/>
|
android:pathData="M8,32L8,36L40,36L40,32L35,20L30,26L24,16L18,26L13,20L8,32Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFC107"
|
android:fillColor="#FFA000"
|
||||||
android:pathData="M6,16l6,12h24l6-12-8,4-4-8-4,8-4-8-4,8-4-8z"/>
|
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" />
|
||||||
<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"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
android:viewportWidth="48"
|
android:viewportWidth="48"
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="48">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#F44336"
|
android:fillColor="#FF4444"
|
||||||
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: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>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
|
|
@ -5,11 +6,11 @@
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="48">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF5722"
|
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
|
<path
|
||||||
android:fillColor="#FFF"
|
android:fillColor="#FFC107"
|
||||||
android:pathData="M24,15a3,3 0,1,1 0,6 3,3 0,1,1 0,-6z"/>
|
android:pathData="M18,28L18,36L22,40L22,28L18,28Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF9800"
|
android:fillColor="#FFC107"
|
||||||
android:pathData="M20,38l-2,6 2-2 4,2 4-2 2,2-2-6z"/>
|
android:pathData="M30,28L30,36L26,40L26,28L30,28Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
|
|
@ -5,11 +6,11 @@
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="48">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#E91E63"
|
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
|
<path
|
||||||
android:fillColor="#4CAF50"
|
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
|
<path
|
||||||
android:fillColor="#4CAF50"
|
android:fillColor="#8BC34A"
|
||||||
android:pathData="M22,25v15h4V25z"/>
|
android:pathData="M22,28C18,28 15,30 15,32C15,34 18,36 22,36L22,28Z" />
|
||||||
</vector>
|
</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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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>
|
</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>
|
<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_500">#6200EE</color>
|
||||||
<color name="purple_700">#3700B3</color>
|
<color name="purple_700">#3700B3</color>
|
||||||
<color name="teal_200">#03DAC5</color>
|
<color name="teal_200">#03DAC5</color>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
plugins {
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=600000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user