Compare commits

..

3 Commits

Author SHA1 Message Date
xiao12feng8
202c8452c5 优化:App整体主题优化 2026-01-06 14:24:42 +08:00
xiao12feng8
1124ce7d82 清理:移动冗余文件 2026-01-06 10:25:40 +08:00
xiao12feng8
a2287bccd1 清理:移动冗余文件 2026-01-06 10:25:22 +08:00
140 changed files with 7767 additions and 5258 deletions

View File

@ -1,285 +0,0 @@
# Android端API路径修复指南
## 🎯 目标
将Android端的观看历史API调用路径修改为与zhibo-h后台匹配。
---
## 📝 需要修改的文件
### 文件1: ApiService.java
**位置**: `android-app/app/src/main/java/com/zbkj/front/net/ApiService.java`
#### 修改内容
找到以下接口定义并修改:
```java
// ==================== 用户活动记录 ====================
/**
* 获取观看历史
*/
@GET("api/front/activity/view/history") // ← 修改这里view-history 改为 view/history
Call<ApiResponse<PageResponse<Map<String, Object>>>> getViewHistory(
@Query("targetType") String targetType, // ← 修改这里type 改为 targetType
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 清除观看历史
*/
@DELETE("api/front/activity/view/history") // ← 修改这里view-history 改为 view/history
Call<ApiResponse<String>> clearViewHistory(@Query("targetType") String targetType); // ← 修改这里type 改为 targetType
/**
* 获取点赞记录
*/
@GET("api/front/activity/like/records") // ← 修改这里like-records 改为 like/records
Call<ApiResponse<PageResponse<Map<String, Object>>>> getLikeRecords(
@Query("targetType") String targetType, // ← 修改这里type 改为 targetType
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取收藏的作品
*/
@GET("api/front/activity/collect/works") // ← 修改这里collected-works 改为 collect/works
Call<ApiResponse<PageResponse<Map<String, Object>>>> getCollectedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取关注记录
*/
@GET("api/front/activity/follow/records") // ← 修改这里follow-records 改为 follow/records
Call<ApiResponse<PageResponse<Map<String, Object>>>> getFollowRecords(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 记录观看历史(新版)
*/
@POST("api/front/activity/view/record") // ← 修改这里record-view 改为 view/record
Call<ApiResponse<Map<String, Object>>> recordViewHistoryNew(@Body Map<String, Object> body);
/**
* 调试Token
*/
@GET("api/front/activity/debug/token") // ← 修改这里debug-token 改为 debug/token
Call<ApiResponse<Map<String, Object>>> debugToken();
```
---
### 文件2: RoomDetailActivity.java
**位置**: `android-app/app/src/main/java/com/zbkj/front/livestreaming/RoomDetailActivity.java`
#### 修改内容
找到 `recordWatchHistory()` 方法,确保参数名正确:
```java
private void recordWatchHistory() {
try {
if (!AuthHelper.isLoggedIn(this)) {
return; // 未登录用户不记录
}
if (TextUtils.isEmpty(roomId)) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
// 使用新的统一观看历史API
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("targetType", "room"); // ✅ 正确targetType
body.put("targetId", roomId);
body.put("targetTitle", "直播间");
body.put("duration", 0); // ✅ 正确duration不是viewDuration
Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordViewHistoryNew(body);
call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "观看历史记录成功");
// 房间信息加载后更新观看历史
updateWatchHistoryWithRoomInfo();
} else {
android.util.Log.w("RoomDetail", "记录观看历史失败: " +
(response.body() != null ? response.body().getMessage() : "unknown"));
}
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
// 忽略错误,不影响直播观看
android.util.Log.w("RoomDetail", "记录观看历史失败: " + t.getMessage());
}
});
} catch (Exception e) {
// 忽略所有异常,不影响直播观看
android.util.Log.w("RoomDetail", "记录观看历史失败: " + e.getMessage());
}
}
```
找到 `updateWatchHistoryWithRoomInfo()` 方法,确保参数名正确:
```java
private void updateWatchHistoryWithRoomInfo() {
if (room == null || !AuthHelper.isLoggedIn(this)) {
return;
}
try {
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("targetType", "room"); // ✅ 正确targetType
body.put("targetId", roomId);
body.put("targetTitle", room.getTitle() != null ? room.getTitle() : "直播间");
body.put("duration", 0); // ✅ 正确duration不是viewDuration
apiService.recordViewHistoryNew(body).enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "观看历史更新成功");
}
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
// 忽略错误
}
});
} catch (Exception e) {
// 忽略所有异常
}
}
```
---
## 🔍 修改对比表
| 功能 | 旧路径(错误) | 新路径(正确) | 参数变化 |
|------|--------------|--------------|---------|
| 记录观看历史 | `/activity/record-view` | `/activity/view/record` | - |
| 获取观看历史 | `/activity/view-history` | `/activity/view/history` | `type``targetType` |
| 清除观看历史 | `/activity/view-history` | `/activity/view/history` | `type``targetType` |
| 获取点赞记录 | `/activity/like-records` | `/activity/like/records` | `type``targetType` |
| 获取收藏记录 | `/activity/collected-works` | `/activity/collect/works` | - |
| 获取关注记录 | `/activity/follow-records` | `/activity/follow/records` | - |
| Token调试 | `/activity/debug-token` | `/activity/debug/token` | - |
---
## ✅ 修改检查清单
- [ ] 修改 `ApiService.java` 中的所有API路径
- [ ] 修改 `ApiService.java` 中的参数名(`type` → `targetType`
- [ ] 检查 `RoomDetailActivity.java` 中的参数名(`duration` 不是 `viewDuration`
- [ ] 重新编译Android应用
- [ ] 测试记录观看历史功能
- [ ] 测试查看观看历史功能
- [ ] 检查日志输出
---
## 🧪 测试步骤
### 1. 编译应用
```bash
cd android-app
./gradlew assembleDebug
```
### 2. 安装并运行
```bash
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
### 3. 测试流程
1. **登录应用**
- 确保使用有效的账号登录
2. **进入直播间**
- 选择任意直播间进入
- 查看Logcat日志搜索 "观看历史"
3. **检查日志**
```
RoomDetail: 观看历史记录成功
RoomDetail: 观看历史更新成功
```
4. **查看历史记录**
- 返回首页
- 进入"我的" → "我的记录"
- 切换到"观看历史"标签页
- 应该能看到刚才观看的直播间
### 4. 调试Token如果有问题
使用Postman或curl测试
```bash
curl -X GET "http://1.15.149.240:8081/api/front/activity/debug/token" \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
## 🐛 常见问题
### 问题1401 未登录
**原因**: Token未正确传递
**解决**:
1. 检查 `ApiClient.java` 中的Token拦截器
2. 确保Token格式正确`Bearer {token}`
3. 使用 `/activity/debug/token` 接口验证
### 问题2404 Not Found
**原因**: API路径错误
**解决**:
1. 检查 `ApiService.java` 中的路径是否正确
2. 确保使用 `/` 而不是 `-`
3. 检查后台服务是否运行在8081端口
### 问题3参数错误
**原因**: 参数名不匹配
**解决**:
1. 确保使用 `targetType` 而不是 `type`
2. 确保使用 `duration` 而不是 `viewDuration`
3. 检查后台日志
---
## 📚 相关文档
- [观看历史功能-zhibo-h后台说明.md](./观看历史功能-zhibo-h后台说明.md)
- [观看历史功能-快速指南.md](./观看历史功能-快速指南.md)
---
**最后更新**: 2026-01-05
**状态**: 📝 待修改

View File

@ -20,4 +20,4 @@ APK: android-app/app/build/outputs/apk/release/
前端: Zhibo/admin/dist/
前端访问的服务改成8083本地改回8081
app部署改成8083本地开发改成8081

View File

@ -1,19 +0,0 @@
-- 添加社会动态测试数据
-- 先检查表是否存在
SELECT COUNT(*) as count FROM eb_dynamic;
-- 插入测试数据(使用已有用户)
INSERT INTO `eb_dynamic` (`uid`, `nickname`, `avatar`, `content`, `images`, `location`, `like_count`, `comment_count`, `share_count`, `view_count`, `is_top`, `status`, `create_time`) VALUES
(41, '夏至已至', 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKq2CRmib1mpu4hOFYtPNLkpU5ILwYTYsvVYB0WYQ0hL4sKAGicicO0OicYCT1/132', '今天天气真好,出来直播啦!大家快来看我~', '["https://picsum.photos/400/300?random=101"]', '北京市', 128, 32, 8, 560, 0, 1, NOW() - INTERVAL 2 HOUR),
(42, '测试用户', '', '分享一下我的日常生活,希望大家喜欢❤️', '["https://picsum.photos/400/300?random=102","https://picsum.photos/400/300?random=103"]', '上海市', 256, 45, 12, 890, 0, 1, NOW() - INTERVAL 5 HOUR),
(43, 'xiaofeng', '', '新的一天新的开始今晚8点准时开播不见不散~', '["https://picsum.photos/400/300?random=104"]', '广州市', 89, 18, 5, 320, 0, 1, NOW() - INTERVAL 1 DAY),
(100, '吉惟同学', 'https://img.zcool.cn/community/01a9a65d143edaa8012187f447cfef.jpg', '周末愉快!有没有想一起玩游戏的小伙伴?', '[]', '深圳市', 167, 28, 9, 450, 1, 1, NOW() - INTERVAL 3 HOUR),
(101, '国瑞哥', 'https://img.zcool.cn/community/01b72057a7e0790000018c1bf4fce0.png', '感谢大家的支持,粉丝破万啦!撒花🎉', '["https://picsum.photos/400/300?random=105","https://picsum.photos/400/300?random=106","https://picsum.photos/400/300?random=107"]', '成都市', 512, 89, 35, 1580, 0, 1, NOW() - INTERVAL 6 HOUR),
(102, '玖书小姐姐', '', '今天学了一首新歌,晚上直播间唱给大家听~', '["https://picsum.photos/400/300?random=108"]', '杭州市', 78, 15, 3, 210, 0, 1, NOW() - INTERVAL 8 HOUR);
-- 查看插入的数据
SELECT d.*, u.avatar as user_avatar
FROM eb_dynamic d
LEFT JOIN eb_user u ON d.uid = u.uid
ORDER BY d.id DESC
LIMIT 10;

View File

@ -71,8 +71,8 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {

View File

@ -4,6 +4,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@ -111,6 +114,11 @@
android:name="com.example.livestreaming.FishPondActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.FishPondWebViewActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name="com.example.livestreaming.VoiceMatchActivity"
android:exported="false" />
@ -180,6 +188,11 @@
android:name="com.example.livestreaming.WishTreeActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.WishTreeWebViewActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name="com.example.livestreaming.MessagesActivity"
android:exported="false" />

View File

@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>个人中心</title>
<link rel="stylesheet" href="./styles/profile.css" />
</head>
<body>
<div class="profile-app">
<!-- 背景层 -->
<div class="bg-layer">
<div class="avatar-blur" id="avatarBlur"></div>
<div class="gradient-overlay"></div>
<div class="grid-lines"></div>
</div>
<div class="profile-inner">
<!-- 顶部操作栏 -->
<header class="top-bar">
<div class="top-actions">
<button class="icon-btn glass" id="searchBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
</button>
<button class="icon-btn glass" id="historyBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
</button>
<button class="icon-btn glass" id="settingsBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/>
<line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/>
<line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/>
</svg>
</button>
</div>
</header>
<!-- 用户信息区 -->
<section class="user-section">
<div class="avatar-area">
<div class="avatar-ring">
<div class="ring-glow"></div>
<img class="avatar-img" id="avatar" src="" alt="头像">
</div>
<button class="edit-btn glass" id="editBtn">编辑资料</button>
</div>
<div class="user-info">
<h1 class="user-name" id="userName">加载中...</h1>
<div class="user-tags">
<span class="tag level" id="tagLevel">月亮 20</span>
<span class="tag fans" id="tagFans">星耀 100</span>
<span class="tag vip" id="tagVip">至尊</span>
</div>
<div class="user-id">
<span class="gender-icon" id="genderIcon"></span>
<span id="userId">ID: ------</span>
<button class="copy-btn" id="copyBtn">📋</button>
</div>
</div>
</section>
<!-- 钱包卡片 -->
<section class="wallet-card glass" id="walletCard">
<div class="wallet-glow"></div>
<div class="wallet-shine"></div>
<div class="wallet-content">
<div class="wallet-icon">💰</div>
<div class="wallet-info">
<span class="wallet-label">我的钱包</span>
<span class="wallet-value" id="walletBalance">0.00</span>
</div>
<div class="wallet-actions">
<button class="wallet-btn" id="rechargeBtn">充值</button>
<button class="wallet-btn outline" id="withdrawBtn">提现</button>
</div>
</div>
</section>
<!-- 统计数据 -->
<section class="stats-section glass">
<div class="stat-item" id="followingBtn">
<span class="stat-value" id="followingCount">0</span>
<span class="stat-label">关注</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item" id="fansBtn">
<span class="stat-value" id="fansCount">0</span>
<span class="stat-label">粉丝</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item" id="likesBtn">
<span class="stat-value" id="likesCount">0</span>
<span class="stat-label">获赞</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item" id="friendsBtn">
<span class="stat-value" id="friendsCount">0</span>
<span class="stat-label">好友</span>
</div>
</section>
<!-- 功能菜单 -->
<section class="menu-section">
<div class="menu-item glass" id="menuLiked">
<div class="menu-icon pink">❤️</div>
<div class="menu-text">
<span class="menu-title">我的收藏</span>
<span class="menu-desc">收藏的直播间</span>
</div>
<span class="menu-arrow"></span>
</div>
<div class="menu-item glass" id="menuRecords">
<div class="menu-icon cyan">📝</div>
<div class="menu-text">
<span class="menu-title">我的记录</span>
<span class="menu-desc">观看历史</span>
</div>
<span class="menu-arrow"></span>
</div>
<div class="menu-item glass" id="menuStreamer">
<div class="menu-icon purple">🎬</div>
<div class="menu-text">
<span class="menu-title">主播中心</span>
<span class="menu-desc">开始直播</span>
</div>
<span class="menu-arrow"></span>
</div>
<div class="menu-item glass" id="menuSettings">
<div class="menu-icon gray">⚙️</div>
<div class="menu-text">
<span class="menu-title">设置</span>
<span class="menu-desc">账号与隐私</span>
</div>
<span class="menu-arrow"></span>
</div>
</section>
</div>
</div>
<script src="./scripts/profile.js"></script>
</body>
</html>

View File

@ -0,0 +1,210 @@
// 个人中心 - 赛博梦幻版
const AndroidBridge = {
isAndroid: () => typeof Android !== 'undefined',
getUserProfile: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getUserProfile()); } catch (e) { return null; }
}
return null;
},
getWalletBalance: function() {
if (this.isAndroid()) {
try { return Android.getWalletBalance(); } catch (e) { return '0.00'; }
}
return '0.00';
},
onEditProfile: function() {
if (this.isAndroid()) Android.onEditProfile();
else console.log('编辑资料');
},
onWalletClick: function() {
if (this.isAndroid()) Android.onWalletClick();
else console.log('钱包');
},
onRecharge: function() {
if (this.isAndroid()) Android.onRecharge();
else console.log('充值');
},
onWithdraw: function() {
if (this.isAndroid()) Android.onWithdraw();
else console.log('提现');
},
onFollowingClick: function() {
if (this.isAndroid()) Android.onFollowingClick();
else console.log('关注');
},
onFansClick: function() {
if (this.isAndroid()) Android.onFansClick();
else console.log('粉丝');
},
onLikesClick: function() {
if (this.isAndroid()) Android.onLikesClick();
else console.log('获赞');
},
onFriendsClick: function() {
if (this.isAndroid()) Android.onFriendsClick();
else console.log('好友');
},
onMenuClick: function(menuId) {
if (this.isAndroid()) Android.onMenuClick(menuId);
else console.log('菜单:', menuId);
},
onSearchClick: function() {
if (this.isAndroid()) Android.onSearchClick();
},
onHistoryClick: function() {
if (this.isAndroid()) Android.onHistoryClick();
},
onSettingsClick: function() {
if (this.isAndroid()) Android.onSettingsClick();
},
copyToClipboard: function(text) {
if (this.isAndroid()) {
Android.copyToClipboard(text);
} else {
navigator.clipboard.writeText(text);
alert('已复制: ' + text);
}
},
showToast: function(msg) {
if (this.isAndroid()) Android.showToast(msg);
else console.log('Toast:', msg);
}
};
let userProfile = null;
document.addEventListener('DOMContentLoaded', initPage);
function initPage() {
setupEventListeners();
setTimeout(() => {
loadUserProfile();
loadWalletBalance();
}, 100);
}
function setupEventListeners() {
// 顶部按钮
document.getElementById('searchBtn').addEventListener('click', () => AndroidBridge.onSearchClick());
document.getElementById('historyBtn').addEventListener('click', () => AndroidBridge.onHistoryClick());
document.getElementById('settingsBtn').addEventListener('click', () => AndroidBridge.onSettingsClick());
// 编辑资料
document.getElementById('editBtn').addEventListener('click', () => AndroidBridge.onEditProfile());
// 复制ID
document.getElementById('copyBtn').addEventListener('click', () => {
const userId = document.getElementById('userId').textContent;
AndroidBridge.copyToClipboard(userId);
AndroidBridge.showToast('已复制ID');
});
// 钱包
document.getElementById('walletCard').addEventListener('click', () => AndroidBridge.onWalletClick());
document.getElementById('rechargeBtn').addEventListener('click', (e) => {
e.stopPropagation();
AndroidBridge.onRecharge();
});
document.getElementById('withdrawBtn').addEventListener('click', (e) => {
e.stopPropagation();
AndroidBridge.onWithdraw();
});
// 统计
document.getElementById('followingBtn').addEventListener('click', () => AndroidBridge.onFollowingClick());
document.getElementById('fansBtn').addEventListener('click', () => AndroidBridge.onFansClick());
document.getElementById('likesBtn').addEventListener('click', () => AndroidBridge.onLikesClick());
document.getElementById('friendsBtn').addEventListener('click', () => AndroidBridge.onFriendsClick());
// 菜单
document.getElementById('menuLiked').addEventListener('click', () => AndroidBridge.onMenuClick('liked'));
document.getElementById('menuRecords').addEventListener('click', () => AndroidBridge.onMenuClick('records'));
document.getElementById('menuStreamer').addEventListener('click', () => AndroidBridge.onMenuClick('streamer'));
document.getElementById('menuSettings').addEventListener('click', () => AndroidBridge.onMenuClick('settings'));
}
function loadUserProfile() {
const profile = AndroidBridge.getUserProfile();
if (profile) {
userProfile = profile;
renderProfile(profile);
} else {
// 默认数据
renderProfile({
name: '用户',
id: '12345678',
avatar: '',
gender: 'male',
level: '月亮 20',
fansLevel: '星耀 100',
vipLevel: '至尊',
following: 0,
fans: 0,
likes: 0,
friends: 0
});
}
}
function renderProfile(profile) {
document.getElementById('userName').textContent = profile.name || '用户';
document.getElementById('userId').textContent = 'ID: ' + (profile.id || '------');
if (profile.avatar) {
document.getElementById('avatar').src = profile.avatar;
}
document.getElementById('genderIcon').textContent = profile.gender === 'female' ? '♀' : '♂';
document.getElementById('genderIcon').style.color = profile.gender === 'female' ? 'var(--neon-pink)' : 'var(--neon-cyan)';
if (profile.level) document.getElementById('tagLevel').textContent = profile.level;
if (profile.fansLevel) document.getElementById('tagFans').textContent = profile.fansLevel;
if (profile.vipLevel) document.getElementById('tagVip').textContent = profile.vipLevel;
document.getElementById('followingCount').textContent = formatNumber(profile.following || 0);
document.getElementById('fansCount').textContent = formatNumber(profile.fans || 0);
document.getElementById('likesCount').textContent = formatNumber(profile.likes || 0);
document.getElementById('friendsCount').textContent = formatNumber(profile.friends || 0);
}
function loadWalletBalance() {
const balance = AndroidBridge.getWalletBalance();
document.getElementById('walletBalance').textContent = balance;
}
function formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
}
return num.toString();
}
// Android回调
function updateProfile(json) {
try {
const profile = JSON.parse(json);
renderProfile(profile);
} catch (e) {}
}
function updateWalletBalance(balance) {
document.getElementById('walletBalance').textContent = balance;
}

View File

@ -0,0 +1,234 @@
// 许愿树 - 赛博梦幻版
const AndroidBridge = {
isAndroid: () => typeof Android !== 'undefined',
getFestival: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getFestival()); } catch (e) { return null; }
}
return null;
},
getMyWishes: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getMyWishes()); } catch (e) { return []; }
}
return [];
},
publishWish: function(content) {
if (this.isAndroid()) {
Android.publishWish(content);
} else {
setTimeout(() => onPublishSuccess(), 500);
}
},
deleteWish: function(wishId, isComplete) {
if (this.isAndroid()) {
Android.deleteWish(wishId, isComplete);
} else {
setTimeout(() => onDeleteSuccess(isComplete), 500);
}
},
onSearchClick: function() {
if (this.isAndroid()) Android.onSearchClick();
},
onNotifyClick: function() {
if (this.isAndroid()) Android.onNotifyClick();
},
showToast: function(msg) {
if (this.isAndroid()) Android.showToast(msg);
else console.log('Toast:', msg);
},
checkLogin: function() {
if (this.isAndroid()) return Android.checkLogin();
return true;
}
};
let myWishes = [];
let currentFestival = null;
let currentViewingWishIndex = -1;
document.addEventListener('DOMContentLoaded', initPage);
function initPage() {
setupEventListeners();
startTimer();
setTimeout(() => {
loadFestival();
loadMyWishes();
}, 100);
}
function setupEventListeners() {
document.getElementById('searchBtn').addEventListener('click', () => AndroidBridge.onSearchClick());
document.getElementById('notifyBtn').addEventListener('click', () => AndroidBridge.onNotifyClick());
document.getElementById('addWishCard').addEventListener('click', showWishModal);
document.getElementById('makeWishBtn').addEventListener('click', showWishModal);
// 许愿弹窗
document.getElementById('modalClose').addEventListener('click', () => hideModal('wishModal'));
document.getElementById('btnCancel').addEventListener('click', () => hideModal('wishModal'));
document.getElementById('btnConfirm').addEventListener('click', () => {
const content = document.getElementById('wishInput').value.trim();
if (!content) { AndroidBridge.showToast('请输入心愿内容'); return; }
if (content.length > 50) { AndroidBridge.showToast('心愿内容不能超过50字'); return; }
AndroidBridge.publishWish(content);
});
// 查看弹窗
document.getElementById('viewModalClose').addEventListener('click', () => hideModal('viewModal'));
document.getElementById('btnDelete').addEventListener('click', () => {
if (currentViewingWishIndex >= 0 && currentViewingWishIndex < myWishes.length) {
if (confirm('确定要删除这个心愿吗?')) {
AndroidBridge.deleteWish(myWishes[currentViewingWishIndex].id, false);
}
}
});
document.getElementById('btnComplete').addEventListener('click', () => {
if (currentViewingWishIndex >= 0 && currentViewingWishIndex < myWishes.length) {
if (confirm('恭喜!确认愿望已经达成了吗?')) {
AndroidBridge.deleteWish(myWishes[currentViewingWishIndex].id, true);
}
}
});
// 心愿卡片点击
document.querySelectorAll('.wish-note').forEach(card => {
card.addEventListener('click', () => {
const index = parseInt(card.dataset.index);
if (index < myWishes.length && myWishes[index]) {
showViewWishModal(index);
} else {
showWishModal();
}
});
});
// 输入字数统计
document.getElementById('wishInput').addEventListener('input', function() {
document.getElementById('charCount').textContent = this.value.length;
});
// 点击遮罩关闭
document.querySelectorAll('.modal-mask').forEach(mask => {
mask.addEventListener('click', function() {
const modal = this.closest('.modal');
if (modal && modal.id !== 'successModal') modal.classList.remove('show');
});
});
}
function showModal(id) { document.getElementById(id).classList.add('show'); }
function hideModal(id) {
document.getElementById(id).classList.remove('show');
if (id === 'wishModal') {
document.getElementById('wishInput').value = '';
document.getElementById('charCount').textContent = '0';
}
}
function showWishModal() {
if (!AndroidBridge.checkLogin()) return;
showModal('wishModal');
}
function showViewWishModal(index) {
currentViewingWishIndex = index;
const wish = myWishes[index];
document.getElementById('wishContent').textContent = wish.content;
document.getElementById('wishLikes').innerHTML = '♥ ' + (wish.likeCount || 0);
document.getElementById('wishComments').innerHTML = '💬 ' + (wish.commentCount || 0);
showModal('viewModal');
}
function loadFestival() {
const festival = AndroidBridge.getFestival();
if (festival) {
currentFestival = festival;
document.getElementById('bannerText').textContent = festival.name + '许愿树';
}
}
function loadMyWishes() {
myWishes = AndroidBridge.getMyWishes() || [];
renderWishCards();
updateProgress();
}
function renderWishCards() {
const cards = document.querySelectorAll('.wish-note');
cards.forEach((card, index) => {
const contentEl = card.querySelector('.note-content');
const countEl = card.querySelector('.count');
if (index < myWishes.length && myWishes[index]) {
const wish = myWishes[index];
contentEl.textContent = wish.content.length > 10 ? wish.content.substring(0, 10) + '...' : wish.content;
countEl.textContent = wish.likeCount || 0;
card.classList.remove('empty');
} else {
contentEl.textContent = '';
countEl.textContent = '0';
card.classList.add('empty');
}
});
}
function updateProgress() {
const count = myWishes.length;
const max = 100;
const percent = Math.min((count / max) * 100, 100);
document.getElementById('wishCount').textContent = count + ' / ' + max;
document.getElementById('progressFill').style.width = percent + '%';
}
function startTimer() {
setInterval(() => {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const diff = tomorrow - now;
const h = Math.floor(diff / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
const s = Math.floor((diff % 60000) / 1000);
document.getElementById('bannerTimer').textContent =
String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}, 1000);
}
// Android回调
function updateWishes(json) {
try { myWishes = JSON.parse(json); renderWishCards(); updateProgress(); } catch (e) {}
}
function updateFestival(json) {
try {
currentFestival = JSON.parse(json);
document.getElementById('bannerText').textContent = currentFestival.name + '许愿树';
} catch (e) {}
}
function onPublishSuccess() {
hideModal('wishModal');
showSuccessModal('🎉', '许愿成功!');
}
function onDeleteSuccess(isComplete) {
hideModal('viewModal');
if (isComplete) showSuccessModal('🎊', '愿望达成!');
}
function showSuccessModal(icon, text) {
document.getElementById('successIcon').textContent = icon;
document.getElementById('successText').textContent = text;
showModal('successModal');
setTimeout(() => hideModal('successModal'), 2000);
}

View File

@ -0,0 +1,205 @@
// 缘池 - 赛博梦幻版
const AndroidBridge = {
isAndroid: () => typeof Android !== 'undefined',
getUserInfo: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getUserInfo()); } catch (e) { return null; }
}
return null;
},
getNearbyUsers: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getNearbyUsers()); } catch (e) { return []; }
}
return [];
},
getMatchableCount: function() {
if (this.isAndroid()) {
try { return Android.getMatchableCount(); } catch (e) { return '--'; }
}
return '--';
},
getCategories: function() {
if (this.isAndroid()) {
try { return JSON.parse(Android.getCategories()); } catch (e) { return []; }
}
return [];
},
onUserClick: function(userId, userName) {
if (this.isAndroid()) Android.onUserClick(userId, userName);
else console.log('点击用户:', userId, userName);
},
onCategoryClick: function(categoryId, categoryName, jumpPage) {
if (this.isAndroid()) Android.onCategoryClick(categoryId, categoryName, jumpPage);
else console.log('点击板块:', categoryId, categoryName);
},
onRefresh: function() {
if (this.isAndroid()) Android.onRefresh();
else console.log('刷新');
},
onVoiceMatch: function() {
if (this.isAndroid()) Android.onVoiceMatch();
else alert('语音匹配功能待接入~');
},
onHeartSignal: function() {
if (this.isAndroid()) Android.onHeartSignal();
else alert('心动信号功能待接入~');
},
showToast: function(msg) {
if (this.isAndroid()) Android.showToast(msg);
else console.log('Toast:', msg);
}
};
document.addEventListener('DOMContentLoaded', initPage);
function initPage() {
setupEventListeners();
setTimeout(() => {
loadUserInfo();
loadMatchableCount();
loadNearbyUsers();
loadCategories();
}, 100);
}
function setupEventListeners() {
// 刷新按钮
document.getElementById('refreshBtn').addEventListener('click', function() {
this.querySelector('.core-icon').style.animation = 'none';
this.offsetHeight;
this.querySelector('.core-icon').style.animation = 'spin 0.5s ease';
AndroidBridge.onRefresh();
loadMatchableCount();
loadNearbyUsers();
});
// 语音匹配
document.getElementById('voiceMatchBtn').addEventListener('click', () => AndroidBridge.onVoiceMatch());
// 心动信号
document.getElementById('heartSignalBtn').addEventListener('click', () => AndroidBridge.onHeartSignal());
// 用户头像点击
document.querySelectorAll('.orbit-user').forEach(user => {
user.addEventListener('click', function() {
const userId = this.dataset.userId || '';
const userName = this.querySelector('.user-name').textContent || '用户';
if (userId) AndroidBridge.onUserClick(userId, userName);
});
});
// 功能卡片点击
document.querySelectorAll('.feature-card').forEach(card => {
card.addEventListener('click', function() {
const id = parseInt(this.dataset.id) || 0;
const name = this.querySelector('.card-title').textContent || '';
const jumpPage = this.dataset.jumpPage || '';
AndroidBridge.onCategoryClick(id, name, jumpPage);
});
});
}
// 添加旋转动画
const style = document.createElement('style');
style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
document.head.appendChild(style);
function loadUserInfo() {
const info = AndroidBridge.getUserInfo();
if (info) {
if (info.name) document.getElementById('userName').textContent = info.name;
if (info.info) document.getElementById('userInfo').textContent = info.info;
}
}
function loadMatchableCount() {
const count = AndroidBridge.getMatchableCount();
document.getElementById('matchCount').textContent = count;
}
function loadNearbyUsers() {
const users = AndroidBridge.getNearbyUsers();
renderOrbitUsers(users);
}
function renderOrbitUsers(users) {
const userEls = document.querySelectorAll('.orbit-user');
const defaultUsers = [
{ id: '1', name: '小树', location: '杭州' },
{ id: '2', name: 'Lina', location: '深圳' },
{ id: '3', name: '小七', location: '武汉' },
{ id: '4', name: '小北', location: '西安' },
{ id: '5', name: '暖暖', location: '成都' },
{ id: '6', name: '阿宁', location: '南宁' }
];
const data = (users && users.length > 0) ? users : defaultUsers;
userEls.forEach((el, index) => {
const user = data[index] || {};
el.dataset.userId = user.id || '';
el.querySelector('.user-name').textContent = user.name || '用户';
el.querySelector('.user-location').textContent = user.location || '';
const img = el.querySelector('.avatar-img');
if (user.avatar) {
img.src = user.avatar;
img.onerror = () => { img.src = ''; };
} else {
img.src = '';
}
});
}
function loadCategories() {
const categories = AndroidBridge.getCategories();
if (categories && categories.length > 0) {
renderCategories(categories);
}
}
function renderCategories(categories) {
const grid = document.getElementById('categoryGrid');
const defaultIcons = ['💕', '🎮', '🎤', '🎨', '🔫', '🎲'];
const defaultGlows = ['pink', 'cyan', 'purple', 'pink', 'cyan', 'purple'];
const cards = grid.querySelectorAll('.feature-card');
cards.forEach((card, index) => {
const cat = categories[index];
if (cat) {
card.dataset.id = cat.id || index + 1;
card.dataset.jumpPage = cat.jumpPage || '';
card.querySelector('.card-title').textContent = cat.name || '未知';
}
});
}
// Android回调
function updateNearbyUsers(json) {
try { renderOrbitUsers(JSON.parse(json)); } catch (e) {}
}
function updateMatchableCount(count) {
document.getElementById('matchCount').textContent = count;
}
function updateCategories(json) {
try { renderCategories(JSON.parse(json)); } catch (e) {}
}
function updateUserInfo(name, info) {
if (name) document.getElementById('userName').textContent = name;
if (info) document.getElementById('userInfo').textContent = info;
}

View File

@ -0,0 +1,482 @@
/* ========== 基础重置 ========== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
:root {
--bg-dark: #0a0a14;
--bg-mid: #12122b;
--neon-pink: #FF2E63;
--neon-cyan: #08D9D6;
--neon-purple: #a855f7;
--gold-start: #f6d365;
--gold-end: #fda085;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
}
html, body {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
overflow-x: hidden;
}
/* ========== 主容器 ========== */
.profile-app {
position: relative;
width: 100%;
max-width: 430px;
min-height: 100vh;
margin: 0 auto;
background: linear-gradient(180deg, var(--bg-mid) 0%, var(--bg-dark) 100%);
overflow: hidden;
}
.profile-inner {
position: relative;
z-index: 10;
padding: 14px 16px;
padding-bottom: 100px;
}
/* ========== 背景层 ========== */
.bg-layer {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.avatar-blur {
position: absolute;
top: -50px;
left: 50%;
transform: translateX(-50%);
width: 300px;
height: 300px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple));
border-radius: 50%;
filter: blur(100px);
opacity: 0.4;
}
.gradient-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 0%, var(--bg-dark) 60%);
}
.grid-lines {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: linear-gradient(to bottom, black 20%, transparent 80%);
}
/* ========== 玻璃拟态 ========== */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
}
/* ========== 顶部栏 ========== */
.top-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.top-actions {
display: flex;
gap: 10px;
}
.icon-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
}
.icon-btn:active {
transform: scale(0.92);
background: rgba(255, 255, 255, 0.15);
}
/* ========== 用户信息区 ========== */
.user-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.avatar-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.avatar-ring {
position: relative;
width: 90px;
height: 90px;
border-radius: 50%;
padding: 4px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-cyan));
}
.ring-glow {
position: absolute;
inset: -4px;
border-radius: 50%;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-cyan));
filter: blur(10px);
opacity: 0.5;
animation: ring-pulse 2s ease-in-out infinite;
}
@keyframes ring-pulse {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple));
}
.edit-btn {
padding: 6px 14px;
border: none;
font-size: 12px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s;
}
.edit-btn:active {
transform: scale(0.95);
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 26px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(90deg, #fff, var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.user-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.tag {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.tag.level {
background: linear-gradient(90deg, var(--neon-purple), var(--neon-pink));
color: #fff;
}
.tag.fans {
background: linear-gradient(90deg, var(--gold-start), var(--gold-end));
color: #5a3d00;
}
.tag.vip {
background: linear-gradient(90deg, var(--neon-cyan), var(--neon-purple));
color: #fff;
}
.user-id {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.gender-icon {
color: var(--neon-cyan);
}
.copy-btn {
background: none;
border: none;
font-size: 14px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.3s;
}
.copy-btn:active {
opacity: 1;
}
/* ========== 钱包卡片 ========== */
.wallet-card {
position: relative;
padding: 20px;
margin-bottom: 16px;
overflow: hidden;
background: linear-gradient(135deg, rgba(246, 211, 101, 0.15), rgba(253, 160, 133, 0.1));
border: 1px solid rgba(246, 211, 101, 0.3);
}
.wallet-glow {
position: absolute;
top: -50%;
right: -30%;
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(246, 211, 101, 0.4) 0%, transparent 70%);
animation: wallet-pulse 3s ease-in-out infinite;
}
@keyframes wallet-pulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.2); }
}
.wallet-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: wallet-shine 4s ease-in-out infinite;
}
@keyframes wallet-shine {
0%, 100% { left: -100%; }
50% { left: 100%; }
}
.wallet-content {
position: relative;
display: flex;
align-items: center;
gap: 14px;
}
.wallet-icon {
font-size: 36px;
}
.wallet-info {
flex: 1;
display: flex;
flex-direction: column;
}
.wallet-label {
font-size: 12px;
color: var(--text-secondary);
}
.wallet-value {
font-size: 24px;
font-weight: 700;
background: linear-gradient(90deg, var(--gold-start), var(--gold-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.wallet-actions {
display: flex;
gap: 10px;
}
.wallet-btn {
padding: 8px 18px;
border: none;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
background: linear-gradient(90deg, var(--gold-start), var(--gold-end));
color: #5a3d00;
}
.wallet-btn.outline {
background: transparent;
border: 1px solid var(--gold-start);
color: var(--gold-start);
}
.wallet-btn:active {
transform: scale(0.95);
}
/* ========== 统计数据 ========== */
.stats-section {
display: flex;
align-items: center;
padding: 18px 10px;
margin-bottom: 16px;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.3s;
}
.stat-item:active {
transform: scale(0.95);
}
.stat-value {
font-size: 20px;
font-weight: 700;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
.stat-divider {
width: 1px;
height: 30px;
background: var(--glass-border);
}
/* ========== 功能菜单 ========== */
.menu-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.menu-item {
display: flex;
align-items: center;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.menu-item:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.08);
}
.menu-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
margin-right: 14px;
}
.menu-icon.pink {
background: rgba(255, 46, 99, 0.15);
}
.menu-icon.cyan {
background: rgba(8, 217, 214, 0.15);
}
.menu-icon.purple {
background: rgba(168, 85, 247, 0.15);
}
.menu-icon.gray {
background: rgba(255, 255, 255, 0.1);
}
.menu-text {
flex: 1;
display: flex;
flex-direction: column;
}
.menu-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.menu-desc {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.menu-arrow {
font-size: 20px;
color: var(--text-secondary);
}
/* ========== 响应式 ========== */
@media (max-height: 700px) {
.avatar-ring {
width: 75px;
height: 75px;
}
.user-name {
font-size: 22px;
}
.wallet-card {
padding: 16px;
}
.wallet-value {
font-size: 20px;
}
}

View File

@ -0,0 +1,859 @@
/* ========== 基础重置 ========== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
:root {
--bg-dark: #0a0a14;
--bg-mid: #12122b;
--neon-pink: #FF2E63;
--neon-cyan: #08D9D6;
--neon-purple: #a855f7;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.15);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
}
html, body {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
overflow: hidden;
}
/* ========== 主容器 ========== */
.wish-app {
position: relative;
width: 100%;
max-width: 430px;
min-height: 100vh;
margin: 0 auto;
background: linear-gradient(135deg, var(--bg-mid) 0%, var(--bg-dark) 100%);
overflow: hidden;
}
.wish-inner {
position: relative;
z-index: 10;
padding: 14px 16px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ========== 背景层 ========== */
.bg-layer {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
}
.orb-1 {
width: 300px;
height: 300px;
top: -100px;
right: -100px;
background: var(--neon-pink);
animation: float-orb 8s ease-in-out infinite;
}
.orb-2 {
width: 250px;
height: 250px;
bottom: 100px;
left: -80px;
background: var(--neon-cyan);
animation: float-orb 10s ease-in-out infinite reverse;
}
@keyframes float-orb {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(30px, 20px) scale(1.1); }
}
.grid-lines {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: linear-gradient(to bottom, transparent, black 20%, black 80%, transparent);
}
/* ========== 玻璃拟态 ========== */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
}
/* ========== 顶部栏 ========== */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.logo-area {
display: flex;
align-items: center;
gap: 8px;
}
.logo-glow {
font-size: 20px;
color: var(--neon-cyan);
text-shadow: 0 0 10px var(--neon-cyan);
animation: pulse-glow 2s ease-in-out infinite;
}
.logo-text {
font-size: 20px;
font-weight: 700;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.action-btns {
display: flex;
gap: 10px;
}
.icon-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
.icon-btn:active {
transform: scale(0.92);
background: var(--glass-highlight);
}
.notify-badge {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: var(--neon-pink);
border-radius: 50%;
box-shadow: 0 0 8px var(--neon-pink);
}
@keyframes pulse-glow {
0%, 100% { opacity: 1; text-shadow: 0 0 10px var(--neon-cyan); }
50% { opacity: 0.7; text-shadow: 0 0 20px var(--neon-cyan), 0 0 30px var(--neon-cyan); }
}
/* ========== 活动横幅 ========== */
.event-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
margin-bottom: 16px;
position: relative;
overflow: hidden;
}
.banner-glow {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 46, 99, 0.1), transparent);
animation: scan-glow 3s linear infinite;
}
@keyframes scan-glow {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.banner-icon {
font-size: 32px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.banner-info {
flex: 1;
}
.banner-title {
font-size: 15px;
font-weight: 600;
color: var(--neon-pink);
margin-bottom: 2px;
}
.banner-desc {
font-size: 11px;
color: var(--text-secondary);
}
.banner-timer {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 14px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple));
border-radius: 12px;
}
.timer-label {
font-size: 9px;
opacity: 0.8;
}
.timer-value {
font-size: 14px;
font-weight: 700;
font-family: monospace;
}
/* ========== 许愿树区域 ========== */
.tree-area {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 320px;
}
/* 发光树影 */
.tree-silhouette {
position: absolute;
width: 280px;
height: 320px;
display: flex;
flex-direction: column;
align-items: center;
}
.tree-glow {
position: absolute;
width: 200px;
height: 200px;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
background: radial-gradient(circle, rgba(8, 217, 214, 0.2) 0%, transparent 70%);
border-radius: 50%;
animation: tree-breathe 4s ease-in-out infinite;
}
@keyframes tree-breathe {
0%, 100% { transform: translate(-50%, -60%) scale(1); opacity: 0.6; }
50% { transform: translate(-50%, -60%) scale(1.15); opacity: 1; }
}
.tree-crown {
position: relative;
width: 100%;
height: 220px;
}
.crown-layer {
position: absolute;
left: 50%;
transform: translateX(-50%);
border-left: 60px solid transparent;
border-right: 60px solid transparent;
opacity: 0.15;
}
.layer-1 {
bottom: 0;
border-bottom: 100px solid var(--neon-cyan);
width: 0;
filter: blur(2px);
}
.layer-2 {
bottom: 70px;
border-bottom: 90px solid var(--neon-cyan);
border-left-width: 50px;
border-right-width: 50px;
filter: blur(2px);
}
.layer-3 {
bottom: 130px;
border-bottom: 80px solid var(--neon-cyan);
border-left-width: 40px;
border-right-width: 40px;
filter: blur(2px);
}
.tree-trunk {
width: 30px;
height: 60px;
background: linear-gradient(180deg, rgba(139, 69, 19, 0.3), rgba(139, 69, 19, 0.1));
border-radius: 0 0 8px 8px;
margin-top: -10px;
}
/* 树上光点 */
.tree-lights {
position: absolute;
inset: 0;
}
.light-dot {
position: absolute;
left: var(--x);
top: var(--y);
width: 6px;
height: 6px;
background: var(--neon-cyan);
border-radius: 50%;
box-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan);
animation: twinkle 2s ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes twinkle {
0%, 100% { opacity: 0.4; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
/* ========== 心愿信笺 ========== */
.wish-notes {
position: absolute;
inset: 0;
pointer-events: none;
}
.wish-note {
position: absolute;
left: var(--x);
top: var(--y);
width: 65px;
height: 80px;
padding: 8px 6px 6px;
display: flex;
flex-direction: column;
pointer-events: auto;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.wish-note.floating {
animation: note-float 4s ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes note-float {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-8px) rotate(2deg); }
}
.wish-note:active {
transform: scale(1.1) !important;
}
.wish-note.empty {
opacity: 0.3;
border-style: dashed;
}
.note-pin {
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 12px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple));
border-radius: 50%;
box-shadow: 0 0 8px var(--neon-pink);
}
.note-content {
flex: 1;
font-size: 9px;
line-height: 1.3;
color: var(--text-primary);
overflow: hidden;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
}
.note-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 10px;
color: var(--neon-pink);
margin-top: 4px;
}
.heart {
animation: heartbeat 1.5s ease-in-out infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* ========== 中心许愿按钮 ========== */
.wish-core {
position: absolute;
left: 50%;
top: 55%;
transform: translate(-50%, -50%);
width: 85px;
height: 85px;
cursor: pointer;
z-index: 20;
}
.core-pulse {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--neon-cyan);
animation: pulse-ring 2s ease-out infinite;
}
.core-pulse.delay {
animation-delay: 1s;
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.8; }
100% { transform: scale(2); opacity: 0; }
}
.core-btn {
position: absolute;
inset: 8px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple), var(--neon-cyan));
background-size: 200% 200%;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow:
0 0 30px rgba(255, 46, 99, 0.5),
0 0 60px rgba(8, 217, 214, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.2);
animation: gradient-rotate 4s linear infinite, core-glow 2s ease-in-out infinite;
transition: transform 0.2s;
}
.wish-core:active .core-btn {
transform: scale(0.92);
}
@keyframes gradient-rotate {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes core-glow {
0%, 100% { box-shadow: 0 0 30px rgba(255, 46, 99, 0.5), 0 0 60px rgba(8, 217, 214, 0.3); }
50% { box-shadow: 0 0 50px rgba(255, 46, 99, 0.7), 0 0 100px rgba(8, 217, 214, 0.5); }
}
.core-icon {
font-size: 26px;
font-weight: 300;
color: #fff;
line-height: 1;
}
.core-text {
font-size: 11px;
font-weight: 600;
color: #fff;
margin-top: 2px;
}
/* ========== 底部区域 ========== */
.bottom-area {
padding: 12px 0;
margin-top: auto;
}
.progress-section {
padding: 14px 16px;
margin-bottom: 14px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-label {
font-size: 13px;
color: var(--text-secondary);
}
.progress-value {
font-size: 14px;
font-weight: 600;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.progress-track {
position: relative;
height: 10px;
background: rgba(0, 0, 0, 0.4);
border-radius: 5px;
overflow: hidden;
}
.progress-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple), var(--neon-cyan));
border-radius: 5px;
transition: width 0.5s ease;
}
.progress-glow {
position: absolute;
top: 0;
left: -50%;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: progress-shine 2s linear infinite;
}
@keyframes progress-shine {
0% { left: -50%; }
100% { left: 150%; }
}
/* 霓虹按钮 */
.neon-btn {
position: relative;
width: 100%;
height: 52px;
border: none;
border-radius: 26px;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple));
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
}
.neon-btn:active {
transform: scale(0.97);
}
.btn-glow {
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: translateX(-100%);
animation: btn-shine 3s ease-in-out infinite;
}
@keyframes btn-shine {
0%, 100% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
}
.btn-text {
position: relative;
z-index: 2;
font-size: 16px;
font-weight: 700;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.main-btn {
box-shadow: 0 4px 20px rgba(255, 46, 99, 0.4), 0 0 40px rgba(255, 46, 99, 0.2);
}
/* ========== 弹窗 ========== */
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 100;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-box {
position: relative;
width: 88%;
max-width: 340px;
animation: modal-pop 0.3s ease;
}
@keyframes modal-pop {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-head {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 20px;
border-bottom: 1px solid var(--glass-border);
}
.modal-icon {
font-size: 22px;
color: var(--neon-cyan);
text-shadow: 0 0 10px var(--neon-cyan);
}
.modal-title {
flex: 1;
font-size: 17px;
font-weight: 600;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.close-btn {
width: 30px;
height: 30px;
border: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
color: var(--text-secondary);
font-size: 20px;
cursor: pointer;
transition: all 0.2s;
}
.close-btn:active {
transform: scale(0.9);
background: rgba(255, 255, 255, 0.2);
}
.modal-body {
padding: 20px;
}
.input-area {
width: 100%;
height: 100px;
padding: 14px;
border-radius: 12px;
color: var(--text-primary);
font-size: 14px;
resize: none;
outline: none;
transition: border-color 0.3s;
}
.input-area::placeholder {
color: var(--text-secondary);
}
.input-area:focus {
border-color: var(--neon-cyan);
box-shadow: 0 0 0 2px rgba(8, 217, 214, 0.2);
}
.char-info {
text-align: right;
font-size: 12px;
color: var(--text-secondary);
margin-top: 8px;
}
.wish-display {
padding: 16px;
border-radius: 12px;
font-size: 15px;
line-height: 1.6;
min-height: 80px;
}
.wish-stats {
display: flex;
gap: 20px;
margin-top: 14px;
}
.stat {
font-size: 14px;
color: var(--text-secondary);
}
.modal-foot {
display: flex;
gap: 12px;
padding: 16px 20px 20px;
}
.btn-secondary, .btn-primary, .btn-danger, .btn-success {
flex: 1;
height: 44px;
border: none;
border-radius: 22px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary {
color: var(--text-primary);
}
.btn-secondary:active, .btn-danger:active, .btn-success:active {
transform: scale(0.96);
}
.btn-primary {
background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple));
color: #fff;
box-shadow: 0 4px 15px rgba(255, 46, 99, 0.3);
}
.btn-danger {
background: rgba(255, 82, 82, 0.2);
color: #ff5252;
border: 1px solid rgba(255, 82, 82, 0.3);
}
.btn-success {
background: linear-gradient(90deg, #10b981, #059669);
color: #fff;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
/* 成功弹窗 */
.success-box {
position: relative;
text-align: center;
padding: 50px 40px;
}
.success-particles {
position: absolute;
inset: 0;
background: radial-gradient(circle at 30% 30%, var(--neon-pink) 2px, transparent 2px),
radial-gradient(circle at 70% 20%, var(--neon-cyan) 2px, transparent 2px),
radial-gradient(circle at 50% 60%, var(--neon-purple) 2px, transparent 2px);
animation: particles-float 2s ease-out forwards;
opacity: 0;
}
@keyframes particles-float {
0% { opacity: 1; transform: scale(0.5); }
100% { opacity: 0; transform: scale(2); }
}
.success-icon {
font-size: 64px;
margin-bottom: 16px;
animation: success-pop 0.5s ease;
}
@keyframes success-pop {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.success-text {
font-size: 20px;
font-weight: 700;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ========== 响应式 ========== */
@media (max-height: 680px) {
.tree-area { min-height: 280px; }
.tree-silhouette { width: 240px; height: 280px; }
.wish-core { width: 75px; height: 75px; }
.wish-note { width: 55px; height: 70px; }
}
@media (max-height: 580px) {
.event-banner { padding: 10px 14px; margin-bottom: 10px; }
.tree-area { min-height: 240px; }
.tree-silhouette { width: 200px; height: 240px; }
}

View File

@ -0,0 +1,633 @@
/* ========== 基础重置 ========== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
:root {
--bg-dark: #0a0a14;
--bg-mid: #12122b;
--neon-pink: #FF2E63;
--neon-cyan: #08D9D6;
--neon-purple: #a855f7;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.15);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
/* 轨道半径 - 根据屏幕动态调整 */
--orbit-radius: min(38vw, 140px);
--avatar-size: min(15vw, 56px);
--center-size: min(22vw, 90px);
}
html, body {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
overflow-x: hidden;
overflow-y: auto;
}
/* ========== 主容器 ========== */
.pond-app {
position: relative;
width: 100%;
max-width: 430px;
min-height: 100vh;
min-height: 100dvh;
margin: 0 auto;
background: linear-gradient(135deg, var(--bg-mid) 0%, var(--bg-dark) 100%);
overflow-x: hidden;
}
.pond-inner {
position: relative;
z-index: 10;
padding: 12px 14px;
padding-bottom: 70px;
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
}
/* ========== 背景层 ========== */
.bg-layer {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.3;
}
.orb-1 {
width: 200px;
height: 200px;
top: -60px;
left: -60px;
background: var(--neon-cyan);
animation: float-orb 10s ease-in-out infinite;
}
.orb-2 {
width: 180px;
height: 180px;
bottom: 100px;
right: -60px;
background: var(--neon-pink);
animation: float-orb 12s ease-in-out infinite reverse;
}
@keyframes float-orb {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(15px, 20px) scale(1.05); }
}
.grid-lines {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%);
}
/* ========== 玻璃拟态 ========== */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 14px;
}
/* ========== 顶部栏 ========== */
.top-bar {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
flex-shrink: 0;
}
.logo-area {
display: flex;
align-items: center;
gap: 6px;
}
.logo-glow {
font-size: 20px;
color: var(--neon-cyan);
text-shadow: 0 0 12px var(--neon-cyan);
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% { text-shadow: 0 0 8px var(--neon-cyan); }
50% { text-shadow: 0 0 20px var(--neon-cyan), 0 0 30px var(--neon-cyan); }
}
.logo-text {
font-family: "Snell Roundhand", cursive;
font-size: 20px;
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ========== 信息栏 ========== */
.info-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
margin-bottom: 12px;
flex-shrink: 0;
}
.info-left, .info-right {
display: flex;
flex-direction: column;
gap: 2px;
}
.info-label, .info-desc {
font-size: 10px;
color: var(--text-secondary);
}
.info-value, .info-name {
font-size: 15px;
font-weight: 700;
}
.info-unit {
font-size: 11px;
color: var(--neon-pink);
margin-left: 2px;
}
.neon-text {
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.info-right {
text-align: right;
}
/* ========== 匹配圆环区域 - 摩天轮布局 ========== */
.match-area {
position: relative;
width: 100%;
/* 固定高度,确保圆环区域大小一致 */
height: calc(var(--orbit-radius) * 2 + var(--avatar-size) + 50px);
max-height: 340px;
margin: 0 auto 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 中心脉冲圆环 */
.center-ring {
position: absolute;
width: var(--center-size);
height: var(--center-size);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.ring-pulse {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--neon-cyan);
animation: ring-expand 2.5s ease-out infinite;
}
.ring-pulse.delay-1 { animation-delay: 0.8s; }
.ring-pulse.delay-2 { animation-delay: 1.6s; }
@keyframes ring-expand {
0% { transform: scale(1); opacity: 0.7; }
100% { transform: scale(2.2); opacity: 0; }
}
.ring-core {
position: absolute;
inset: 8px;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple), var(--neon-cyan));
background-size: 200% 200%;
border-radius: 50%;
cursor: pointer;
animation: gradient-spin 4s linear infinite;
box-shadow:
0 0 25px rgba(8, 217, 214, 0.4),
0 0 50px rgba(255, 46, 99, 0.3),
inset 0 0 15px rgba(255, 255, 255, 0.2);
transition: transform 0.2s;
}
.ring-core:active {
transform: scale(0.92);
}
@keyframes gradient-spin {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.core-inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.core-icon {
font-size: min(8vw, 28px);
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* 轨道环 - 真正的圆形轨道 */
.orbit-ring {
position: absolute;
width: calc(var(--orbit-radius) * 2);
height: calc(var(--orbit-radius) * 2);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px dashed rgba(255, 255, 255, 0.15);
border-radius: 50%;
pointer-events: none;
}
/* 外层装饰轨道 */
.orbit-ring::before {
content: '';
position: absolute;
inset: -15px;
border: 1px solid rgba(168, 85, 247, 0.1);
border-radius: 50%;
}
.orbit-ring::after {
content: '';
position: absolute;
inset: 15px;
border: 1px solid rgba(8, 217, 214, 0.1);
border-radius: 50%;
}
/* 用户头像容器 - 摩天轮定位 */
.orbit-users {
position: absolute;
width: calc(var(--orbit-radius) * 2);
height: calc(var(--orbit-radius) * 2);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
/* 整体缓慢旋转 */
animation: orbit-spin 60s linear infinite;
}
@keyframes orbit-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
/* 单个用户 - 精确定位在轨道上 */
.orbit-user {
position: absolute;
width: var(--avatar-size);
height: auto;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.3s ease;
/* 反向旋转保持头像正向 */
animation: counter-spin 60s linear infinite;
}
@keyframes counter-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(-360deg); }
}
/* 6个用户的精确位置 - 圆周均匀分布 */
.orbit-user[data-pos="1"] {
left: 50%;
top: 0;
transform: translate(-50%, -50%);
}
.orbit-user[data-pos="2"] {
left: calc(50% + 86.6%/2); /* cos(30°) ≈ 0.866 */
top: 25%;
transform: translate(-50%, -50%);
}
.orbit-user[data-pos="3"] {
left: calc(50% + 86.6%/2);
top: 75%;
transform: translate(-50%, -50%);
}
.orbit-user[data-pos="4"] {
left: 50%;
top: 100%;
transform: translate(-50%, -50%);
}
.orbit-user[data-pos="5"] {
left: calc(50% - 86.6%/2);
top: 75%;
transform: translate(-50%, -50%);
}
.orbit-user[data-pos="6"] {
left: calc(50% - 86.6%/2);
top: 25%;
transform: translate(-50%, -50%);
}
.orbit-user:active {
transform: translate(-50%, -50%) scale(1.1) !important;
}
.user-avatar {
width: var(--avatar-size);
height: var(--avatar-size);
border-radius: 50%;
padding: 2px;
position: relative;
overflow: visible;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-cyan));
}
.avatar-glow {
position: absolute;
inset: -3px;
border-radius: 50%;
background: linear-gradient(135deg, var(--neon-pink), var(--neon-cyan));
opacity: 0.4;
z-index: -1;
filter: blur(6px);
}
.user-name {
font-size: 10px;
font-weight: 600;
color: var(--text-primary);
margin-top: 4px;
text-align: center;
max-width: calc(var(--avatar-size) + 10px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
.user-location {
font-size: 8px;
color: var(--text-secondary);
text-align: center;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
/* ========== 快捷操作按钮 ========== */
.quick-actions {
display: flex;
gap: 10px;
margin-bottom: 12px;
flex-shrink: 0;
}
.action-btn {
flex: 1;
height: 44px;
border: none;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.action-btn:active {
transform: scale(0.96);
}
.action-btn.pink {
background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple));
box-shadow: 0 4px 16px rgba(255, 46, 99, 0.35);
}
.action-btn.cyan {
background: linear-gradient(90deg, var(--neon-purple), var(--neon-cyan));
box-shadow: 0 4px 16px rgba(8, 217, 214, 0.35);
}
.btn-icon {
font-size: 16px;
}
.btn-label {
font-size: 13px;
font-weight: 600;
color: #fff;
}
/* ========== 功能入口 2x3 ========== */
.feature-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
flex-shrink: 0;
}
.feature-card {
position: relative;
padding: 14px 6px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
}
.feature-card:active {
transform: scale(0.95);
}
.card-glow {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
filter: blur(25px);
opacity: 0.12;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: opacity 0.3s;
}
.card-glow.pink { background: var(--neon-pink); }
.card-glow.cyan { background: var(--neon-cyan); }
.card-glow.purple { background: var(--neon-purple); }
.feature-card:hover .card-glow {
opacity: 0.25;
}
.card-icon {
font-size: 24px;
position: relative;
z-index: 1;
}
.card-title {
font-size: 10px;
color: var(--text-primary);
text-align: center;
position: relative;
z-index: 1;
}
/* ========== 响应式 - 小屏幕优化 ========== */
@media (max-height: 680px) {
:root {
--orbit-radius: min(32vw, 115px);
--avatar-size: min(13vw, 48px);
--center-size: min(18vw, 70px);
}
.match-area {
max-height: 280px;
margin-bottom: 10px;
}
.info-bar {
padding: 8px 12px;
margin-bottom: 10px;
}
.quick-actions {
margin-bottom: 10px;
}
.action-btn {
height: 40px;
}
.feature-card {
padding: 10px 4px;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-size: 9px;
}
}
@media (max-height: 600px) {
:root {
--orbit-radius: min(28vw, 100px);
--avatar-size: min(11vw, 42px);
--center-size: min(16vw, 60px);
}
.match-area {
max-height: 240px;
margin-bottom: 8px;
}
.top-bar {
margin-bottom: 6px;
}
.logo-glow, .logo-text {
font-size: 18px;
}
.pond-inner {
padding: 10px 12px;
padding-bottom: 65px;
}
.action-btn {
height: 38px;
}
.btn-label {
font-size: 12px;
}
.feature-grid {
gap: 8px;
}
}
/* 超小屏幕 */
@media (max-height: 550px) {
:root {
--orbit-radius: min(25vw, 85px);
--avatar-size: min(10vw, 38px);
--center-size: min(14vw, 52px);
}
.match-area {
max-height: 200px;
}
.user-name {
font-size: 9px;
}
.user-location {
display: none;
}
}

View File

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>许愿树</title>
<link rel="stylesheet" href="./styles/wishtree.css" />
</head>
<body>
<div class="wish-app">
<!-- 背景层 -->
<div class="bg-layer">
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="grid-lines"></div>
</div>
<div class="wish-inner">
<!-- 顶部栏 -->
<header class="top-bar">
<div class="logo-area">
<span class="logo-glow"></span>
<span class="logo-text">WishTree</span>
</div>
<div class="action-btns">
<button class="icon-btn glass" id="searchBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
</button>
<button class="icon-btn glass" id="notifyBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
<span class="notify-badge"></span>
</button>
</div>
</header>
<!-- 活动横幅 -->
<section class="event-banner glass">
<div class="banner-glow"></div>
<div class="banner-icon">🎄</div>
<div class="banner-info">
<div class="banner-title" id="bannerText">元旦许愿树</div>
<div class="banner-desc">许下心愿,愿望成真</div>
</div>
<div class="banner-timer">
<span class="timer-label">剩余</span>
<span class="timer-value" id="bannerTimer">00:00:00</span>
</div>
</section>
<!-- 许愿树主区域 -->
<section class="tree-area">
<!-- 发光树影 -->
<div class="tree-silhouette">
<div class="tree-glow"></div>
<div class="tree-trunk"></div>
<div class="tree-crown">
<div class="crown-layer layer-1"></div>
<div class="crown-layer layer-2"></div>
<div class="crown-layer layer-3"></div>
</div>
<!-- 树上的光点 -->
<div class="tree-lights">
<span class="light-dot" style="--x: 20%; --y: 25%; --delay: 0s;"></span>
<span class="light-dot" style="--x: 75%; --y: 20%; --delay: 0.5s;"></span>
<span class="light-dot" style="--x: 35%; --y: 45%; --delay: 1s;"></span>
<span class="light-dot" style="--x: 65%; --y: 40%; --delay: 1.5s;"></span>
<span class="light-dot" style="--x: 50%; --y: 15%; --delay: 2s;"></span>
<span class="light-dot" style="--x: 25%; --y: 60%; --delay: 0.3s;"></span>
<span class="light-dot" style="--x: 70%; --y: 55%; --delay: 0.8s;"></span>
</div>
</div>
<!-- 心愿信笺卡片 -->
<div class="wish-notes" id="wishCards">
<div class="wish-note glass floating" data-index="0" style="--x: 12%; --y: 15%; --delay: 0s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="1" style="--x: 72%; --y: 12%; --delay: 0.4s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="2" style="--x: 5%; --y: 40%; --delay: 0.8s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="3" style="--x: 78%; --y: 38%; --delay: 1.2s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="4" style="--x: 18%; --y: 62%; --delay: 0.2s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="5" style="--x: 68%; --y: 60%; --delay: 0.6s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
<div class="wish-note glass floating" data-index="6" style="--x: 42%; --y: 8%; --delay: 1s;">
<div class="note-pin"></div>
<div class="note-content"></div>
<div class="note-footer"><span class="heart"></span><span class="count">0</span></div>
</div>
</div>
<!-- 中心许愿按钮 -->
<div class="wish-core" id="addWishCard">
<div class="core-pulse"></div>
<div class="core-pulse delay"></div>
<div class="core-btn">
<span class="core-icon">+</span>
<span class="core-text">许愿</span>
</div>
</div>
</section>
<!-- 底部进度区 -->
<section class="bottom-area">
<div class="progress-section glass">
<div class="progress-header">
<span class="progress-label">祈愿能量</span>
<span class="progress-value" id="wishCount">0 / 100</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="progressFill"></div>
<div class="progress-glow"></div>
</div>
</div>
<button class="main-btn neon-btn" id="makeWishBtn">
<span class="btn-glow"></span>
<span class="btn-text">✦ 前往祈愿</span>
</button>
</section>
</div>
</div>
<!-- 许愿弹窗 -->
<div class="modal" id="wishModal">
<div class="modal-mask"></div>
<div class="modal-box glass">
<div class="modal-head">
<span class="modal-icon"></span>
<span class="modal-title">许下心愿</span>
<button class="close-btn" id="modalClose">×</button>
</div>
<div class="modal-body">
<textarea class="input-area glass" id="wishInput" placeholder="在星空下写下你的心愿..." maxlength="50"></textarea>
<div class="char-info"><span id="charCount">0</span>/50</div>
</div>
<div class="modal-foot">
<button class="btn-secondary glass" id="btnCancel">取消</button>
<button class="btn-primary neon-btn" id="btnConfirm">✦ 许愿</button>
</div>
</div>
</div>
<!-- 查看心愿弹窗 -->
<div class="modal" id="viewModal">
<div class="modal-mask"></div>
<div class="modal-box glass">
<div class="modal-head">
<span class="modal-icon">💫</span>
<span class="modal-title">我的心愿</span>
<button class="close-btn" id="viewModalClose">×</button>
</div>
<div class="modal-body">
<div class="wish-display glass" id="wishContent"></div>
<div class="wish-stats">
<span class="stat" id="wishLikes">♥ 0</span>
<span class="stat" id="wishComments">💬 0</span>
</div>
</div>
<div class="modal-foot">
<button class="btn-danger" id="btnDelete">删除</button>
<button class="btn-success" id="btnComplete">🎉 达成</button>
</div>
</div>
</div>
<!-- 成功弹窗 -->
<div class="modal" id="successModal">
<div class="modal-mask"></div>
<div class="success-box">
<div class="success-particles"></div>
<div class="success-icon" id="successIcon">🎉</div>
<div class="success-text" id="successText">许愿成功!</div>
</div>
</div>
<script src="./scripts/wishtree.js"></script>
</body>
</html>

View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>缘池</title>
<link rel="stylesheet" href="./styles/yuanchi.css" />
</head>
<body>
<div class="pond-app">
<!-- 背景层 -->
<div class="bg-layer">
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="grid-lines"></div>
</div>
<div class="pond-inner">
<!-- 顶部栏 -->
<header class="top-bar">
<div class="logo-area">
<span class="logo-glow"></span>
<span class="logo-text">FishPond</span>
</div>
</header>
<!-- 信息栏 -->
<section class="info-bar glass">
<div class="info-left">
<span class="info-label">剩余匹配</span>
<span class="info-value neon-text" id="matchCount">--</span>
<span class="info-unit"></span>
</div>
<div class="info-right">
<span class="info-desc" id="userInfo">真诚 | 南宁 | 在线</span>
<span class="info-name neon-text" id="userName">加载中...</span>
</div>
</section>
<!-- 匹配圆环区域 -->
<section class="match-area">
<!-- 中心脉冲圆环 -->
<div class="center-ring">
<div class="ring-pulse"></div>
<div class="ring-pulse delay-1"></div>
<div class="ring-pulse delay-2"></div>
<div class="ring-core" id="refreshBtn">
<div class="core-inner">
<span class="core-icon"></span>
</div>
</div>
</div>
<!-- 轨道环 -->
<div class="orbit-ring"></div>
<!-- 用户头像 - 摩天轮布局 -->
<div class="orbit-users" id="orbitGroup">
<div class="orbit-user" data-pos="1">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
<div class="orbit-user" data-pos="2">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
<div class="orbit-user" data-pos="3">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
<div class="orbit-user" data-pos="4">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
<div class="orbit-user" data-pos="5">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
<div class="orbit-user" data-pos="6">
<div class="user-avatar glass">
<img src="" alt="" class="avatar-img">
<div class="avatar-glow"></div>
</div>
<div class="user-name"></div>
<div class="user-location"></div>
</div>
</div>
</section>
<!-- 快捷操作按钮 -->
<section class="quick-actions">
<button class="action-btn neon-btn pink" id="voiceMatchBtn">
<span class="btn-icon">🎧</span>
<span class="btn-label">语音匹配</span>
</button>
<button class="action-btn neon-btn cyan" id="heartSignalBtn">
<span class="btn-icon">💓</span>
<span class="btn-label">心动信号</span>
</button>
</section>
<!-- 功能入口 2x3 -->
<section class="feature-grid" id="categoryGrid">
<div class="feature-card glass" data-id="1">
<div class="card-glow pink"></div>
<div class="card-icon">💕</div>
<div class="card-title">在线处对象</div>
</div>
<div class="feature-card glass" data-id="2">
<div class="card-glow cyan"></div>
<div class="card-icon">🎮</div>
<div class="card-title">找人玩游戏</div>
</div>
<div class="feature-card glass" data-id="3">
<div class="card-glow purple"></div>
<div class="card-icon">🎤</div>
<div class="card-title">一起KTV</div>
</div>
<div class="feature-card glass" data-id="4">
<div class="card-glow pink"></div>
<div class="card-icon">🎨</div>
<div class="card-title">你画我猜</div>
</div>
<div class="feature-card glass" data-id="5">
<div class="card-glow cyan"></div>
<div class="card-icon">🔫</div>
<div class="card-title">和平精英</div>
</div>
<div class="feature-card glass" data-id="6">
<div class="card-glow purple"></div>
<div class="card-icon">🎲</div>
<div class="card-title">桌游派对</div>
</div>
</section>
</div>
</div>
<script src="./scripts/yuanchi.js"></script>
</body>
</html>

View File

@ -56,7 +56,7 @@ public class DrawGuessActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -125,12 +125,12 @@ public class DynamicCommunityActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -57,7 +57,7 @@ public class FindGameActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -139,7 +139,7 @@ public class FishPondActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -0,0 +1,508 @@
package com.example.livestreaming;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 缘池页面 - WebView版本
* 使用H5页面展示通过JavaScript Bridge与原生交互
*/
public class FishPondWebViewActivity extends AppCompatActivity {
private static final String TAG = "FishPondWebView";
private WebView webView;
private ProgressBar loadingProgress;
private ApiService apiService;
// 缓存数据
private String cachedUserName = "";
private String cachedUserInfo = "";
private int cachedMatchableCount = 0;
private List<CommunityResponse.NearbyUser> cachedNearbyUsers = new ArrayList<>();
private List<CommunityResponse.Category> cachedCategories = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fish_pond_webview);
apiService = ApiClient.getService(this);
initViews();
setupWebView();
setupBottomNav();
// 预加载数据
loadCurrentUserInfo();
loadMatchableUserCount();
loadNearbyUsers();
loadCategories();
// 加载H5页面
webView.loadUrl("file:///android_asset/web/yuanchi.html");
}
private void initViews() {
webView = findViewById(R.id.webView);
loadingProgress = findViewById(R.id.loadingProgress);
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
// 添加JavaScript接口
webView.addJavascriptInterface(new FishPondJSInterface(), "Android");
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
loadingProgress.setVisibility(View.GONE);
// 页面加载完成后推送缓存的数据到H5
pushDataToH5();
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress < 100) {
loadingProgress.setVisibility(View.VISIBLE);
} else {
loadingProgress.setVisibility(View.GONE);
}
}
});
}
private void setupBottomNav() {
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
bottomNav.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeWebViewActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
}
/**
* 加载当前用户信息
*/
private void loadCurrentUserInfo() {
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
cachedUserName = prefs.getString("profile_name", "爱你");
String location = prefs.getString("profile_location", "广西");
String gender = prefs.getString("profile_gender", "");
String genderText = "真诚";
String locationText = location;
if (!TextUtils.isEmpty(location) && location.contains("-")) {
locationText = location.replace("-", "·");
} else if (TextUtils.isEmpty(location)) {
locationText = "南宁";
}
cachedUserInfo = genderText + " | " + locationText + " | 在线";
}
/**
* 加载可匹配用户数量
*/
private void loadMatchableUserCount() {
apiService.getMatchableUserCount().enqueue(new Callback<ApiResponse<Integer>>() {
@Override
public void onResponse(Call<ApiResponse<Integer>> call, Response<ApiResponse<Integer>> response) {
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
cachedMatchableCount = response.body().getData();
pushMatchableCountToH5();
}
}
@Override
public void onFailure(Call<ApiResponse<Integer>> call, Throwable t) {
Log.e(TAG, "获取用户总数失败: " + t.getMessage());
}
});
}
/**
* 加载附近用户
*/
private void loadNearbyUsers() {
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
double latitude = prefs.getFloat("latitude", 22.82f);
double longitude = prefs.getFloat("longitude", 108.32f);
apiService.getNearbyUsers(latitude, longitude, 5000, 6)
.enqueue(new Callback<ApiResponse<CommunityResponse.NearbyUserList>>() {
@Override
public void onResponse(Call<ApiResponse<CommunityResponse.NearbyUserList>> call,
Response<ApiResponse<CommunityResponse.NearbyUserList>> response) {
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
List<CommunityResponse.NearbyUser> users = response.body().getData().getUsers();
if (users != null && !users.isEmpty()) {
cachedNearbyUsers = users;
pushNearbyUsersToH5();
}
}
}
@Override
public void onFailure(Call<ApiResponse<CommunityResponse.NearbyUserList>> call, Throwable t) {
Log.e(TAG, "获取附近用户失败: " + t.getMessage());
}
});
}
/**
* 加载板块列表
*/
private void loadCategories() {
apiService.getCommunityCategories().enqueue(new Callback<ApiResponse<List<CommunityResponse.Category>>>() {
@Override
public void onResponse(Call<ApiResponse<List<CommunityResponse.Category>>> call,
Response<ApiResponse<List<CommunityResponse.Category>>> response) {
if (response.isSuccessful() && response.body() != null
&& response.body().isOk() && response.body().getData() != null) {
cachedCategories = response.body().getData();
pushCategoriesToH5();
}
}
@Override
public void onFailure(Call<ApiResponse<List<CommunityResponse.Category>>> call, Throwable t) {
Log.e(TAG, "加载板块失败: " + t.getMessage());
}
});
}
/**
* 推送所有数据到H5
*/
private void pushDataToH5() {
pushUserInfoToH5();
pushMatchableCountToH5();
pushNearbyUsersToH5();
pushCategoriesToH5();
}
private void pushUserInfoToH5() {
runOnUiThread(() -> {
String js = String.format("updateUserInfo('%s', '%s')",
escapeJs(cachedUserName), escapeJs(cachedUserInfo));
webView.evaluateJavascript(js, null);
});
}
private void pushMatchableCountToH5() {
runOnUiThread(() -> {
String js = String.format("updateMatchableCount('%d')", cachedMatchableCount);
webView.evaluateJavascript(js, null);
});
}
private void pushNearbyUsersToH5() {
runOnUiThread(() -> {
try {
JSONArray usersArray = new JSONArray();
String baseUrl = ApiClient.getCurrentBaseUrl(this);
for (CommunityResponse.NearbyUser user : cachedNearbyUsers) {
JSONObject userObj = new JSONObject();
userObj.put("id", String.valueOf(user.getUserId()));
userObj.put("name", user.getNickname() != null ? user.getNickname() : "用户");
userObj.put("location", user.getLocation() != null ? user.getLocation() : "附近");
String avatarUrl = user.getAvatar();
if (avatarUrl != null && !avatarUrl.isEmpty()) {
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
avatarUrl = baseUrl + avatarUrl;
}
}
userObj.put("avatar", avatarUrl != null ? avatarUrl : "");
usersArray.put(userObj);
}
String js = String.format("updateNearbyUsers('%s')", escapeJs(usersArray.toString()));
webView.evaluateJavascript(js, null);
} catch (JSONException e) {
Log.e(TAG, "构建用户JSON失败: " + e.getMessage());
}
});
}
private void pushCategoriesToH5() {
runOnUiThread(() -> {
try {
JSONArray categoriesArray = new JSONArray();
for (CommunityResponse.Category cat : cachedCategories) {
if (cat.status == 1 || cat.status == 0) {
JSONObject catObj = new JSONObject();
catObj.put("id", cat.getId());
catObj.put("name", cat.getName() != null ? cat.getName() : "");
catObj.put("icon", cat.getIcon() != null ? cat.getIcon() : "");
catObj.put("jumpPage", cat.getJumpPage() != null ? cat.getJumpPage() : "");
categoriesArray.put(catObj);
}
}
String js = String.format("updateCategories('%s')", escapeJs(categoriesArray.toString()));
webView.evaluateJavascript(js, null);
} catch (JSONException e) {
Log.e(TAG, "构建板块JSON失败: " + e.getMessage());
}
});
}
private String escapeJs(String str) {
if (str == null) return "";
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
/**
* JavaScript接口类 - 供H5调用
*/
private class FishPondJSInterface {
@JavascriptInterface
public String getUserInfo() {
try {
JSONObject obj = new JSONObject();
obj.put("name", cachedUserName);
obj.put("info", cachedUserInfo);
return obj.toString();
} catch (JSONException e) {
return "{}";
}
}
@JavascriptInterface
public String getMatchableCount() {
return String.valueOf(cachedMatchableCount);
}
@JavascriptInterface
public String getNearbyUsers() {
try {
JSONArray usersArray = new JSONArray();
String baseUrl = ApiClient.getCurrentBaseUrl(FishPondWebViewActivity.this);
for (CommunityResponse.NearbyUser user : cachedNearbyUsers) {
JSONObject userObj = new JSONObject();
userObj.put("id", String.valueOf(user.getUserId()));
userObj.put("name", user.getNickname() != null ? user.getNickname() : "用户");
userObj.put("location", user.getLocation() != null ? user.getLocation() : "附近");
String avatarUrl = user.getAvatar();
if (avatarUrl != null && !avatarUrl.isEmpty()) {
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
avatarUrl = baseUrl + avatarUrl;
}
}
userObj.put("avatar", avatarUrl != null ? avatarUrl : "");
usersArray.put(userObj);
}
return usersArray.toString();
} catch (JSONException e) {
return "[]";
}
}
@JavascriptInterface
public String getCategories() {
try {
JSONArray categoriesArray = new JSONArray();
for (CommunityResponse.Category cat : cachedCategories) {
if (cat.status == 1 || cat.status == 0) {
JSONObject catObj = new JSONObject();
catObj.put("id", cat.getId());
catObj.put("name", cat.getName() != null ? cat.getName() : "");
catObj.put("icon", cat.getIcon() != null ? cat.getIcon() : "");
catObj.put("jumpPage", cat.getJumpPage() != null ? cat.getJumpPage() : "");
categoriesArray.put(catObj);
}
}
return categoriesArray.toString();
} catch (JSONException e) {
return "[]";
}
}
@JavascriptInterface
public void onUserClick(String oderId, String userName) {
Log.d(TAG, "点击用户: " + oderId + ", " + userName);
runOnUiThread(() -> {
// 跳转到用户详情页
try {
int userId = 0;
try {
userId = Integer.parseInt(oderId);
} catch (NumberFormatException e) {
Log.e(TAG, "用户ID解析失败: " + oderId);
}
if (userId > 0) {
UserProfileActivity.start(FishPondWebViewActivity.this, userId);
} else {
Toast.makeText(FishPondWebViewActivity.this, "用户ID无效", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Toast.makeText(FishPondWebViewActivity.this, "查看用户: " + userName, Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onCategoryClick(int categoryId, String categoryName, String jumpPage) {
Log.d(TAG, "点击板块: " + categoryId + ", " + categoryName + ", " + jumpPage);
runOnUiThread(() -> {
// 跳转到板块详情页
try {
Intent intent = new Intent(FishPondWebViewActivity.this, DynamicCommunityActivity.class);
intent.putExtra("category_id", categoryId);
intent.putExtra("category_name", categoryName);
startActivity(intent);
} catch (Exception e) {
Toast.makeText(FishPondWebViewActivity.this, "板块 \"" + categoryName + "\" 暂未开放", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onRefresh() {
Log.d(TAG, "刷新");
runOnUiThread(() -> {
loadMatchableUserCount();
loadNearbyUsers();
});
}
@JavascriptInterface
public void onVoiceMatch() {
runOnUiThread(() -> {
Toast.makeText(FishPondWebViewActivity.this, "语音匹配功能待接入~", Toast.LENGTH_SHORT).show();
});
}
@JavascriptInterface
public void onHeartSignal() {
runOnUiThread(() -> {
Toast.makeText(FishPondWebViewActivity.this, "心动信号功能待接入~", Toast.LENGTH_SHORT).show();
});
}
@JavascriptInterface
public void showToast(String message) {
runOnUiThread(() -> {
Toast.makeText(FishPondWebViewActivity.this, message, Toast.LENGTH_SHORT).show();
});
}
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
// 刷新底部导航状态
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_friends);
UnreadMessageManager.updateBadge(bottomNav);
}
}
@Override
protected void onDestroy() {
if (webView != null) {
webView.destroy();
}
super.onDestroy();
}
}

View File

@ -102,12 +102,12 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -56,7 +56,7 @@ public class KTVTogetherActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -238,7 +238,7 @@ public class MainActivity extends AppCompatActivity {
if (!AuthHelper.requireLogin(this, "缘池功能需要登录")) {
return;
}
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return;
}
@ -501,12 +501,12 @@ public class MainActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}
@ -1749,16 +1749,16 @@ public class MainActivity extends AppCompatActivity {
allBackendCategories.addAll(categories);
runOnUiThread(() -> {
// "我的频道"加载用户保存的频道列表
List<ChannelManagerAdapter.ChannelItem> myChannelList = loadMyChannels();
// 清空现有标签
binding.categoryTabs.removeAllTabs();
// 只显示"我的频道"里的内容
for (ChannelManagerAdapter.ChannelItem channel : myChannelList) {
if (channel != null && channel.getName() != null) {
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName()));
// 添加"推荐"作为第一个标签
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐"));
// 直接使用后端返回的分类
for (com.example.livestreaming.net.CategoryResponse cat : categories) {
if (cat != null && cat.getName() != null) {
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(cat.getName()));
}
}
@ -1766,7 +1766,7 @@ public class MainActivity extends AppCompatActivity {
String lastCategory = CategoryFilterManager.getLastCategory(this);
restoreCategoryTabSelection();
Log.d(TAG, "updateCategoryTabs() 分类标签更新完成,共 " + binding.categoryTabs.getTabCount() + " 个标签(只显示我的频道");
Log.d(TAG, "updateCategoryTabs() 分类标签更新完成,共 " + binding.categoryTabs.getTabCount() + " 个标签(直接使用后端分类");
});
}
@ -1777,23 +1777,33 @@ public class MainActivity extends AppCompatActivity {
if (binding == null || binding.categoryTabs == null) return;
runOnUiThread(() -> {
// "我的频道"加载用户保存的频道列表
List<ChannelManagerAdapter.ChannelItem> myChannelList = loadMyChannels();
// 清空现有标签
binding.categoryTabs.removeAllTabs();
// 只显示"我的频道"里的内容
for (ChannelManagerAdapter.ChannelItem channel : myChannelList) {
if (channel != null && channel.getName() != null) {
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName()));
// 使用后端分类如果已加载
if (!allBackendCategories.isEmpty()) {
// 添加"推荐"作为第一个标签
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐"));
// 添加后端分类
for (com.example.livestreaming.net.CategoryResponse cat : allBackendCategories) {
if (cat != null && cat.getName() != null) {
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(cat.getName()));
}
}
} else {
// 后端分类未加载使用硬编码默认值
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐"));
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("娱乐"));
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("游戏"));
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("音乐"));
binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("户外"));
}
// 恢复上次选中的分类
restoreCategoryTabSelection();
Log.d(TAG, "useDefaultCategories() 使用我的频道,共 " + binding.categoryTabs.getTabCount() + " 个标签");
Log.d(TAG, "useDefaultCategories() 使用默认分类,共 " + binding.categoryTabs.getTabCount() + " 个标签");
});
}

View File

@ -103,12 +103,12 @@ public class MessagesActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -56,7 +56,7 @@ public class OnlineDatingActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -56,7 +56,7 @@ public class PeaceEliteActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -113,12 +113,12 @@ public class ProfileActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -0,0 +1,358 @@
package com.example.livestreaming;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import org.json.JSONException;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 个人中心 - WebView版本
*/
public class ProfileWebViewActivity extends AppCompatActivity {
private static final String TAG = "ProfileWebView";
private WebView webView;
private ProgressBar loadingProgress;
private ApiService apiService;
// 缓存数据
private String cachedName = "";
private String cachedId = "";
private String cachedAvatar = "";
private String cachedGender = "male";
private int cachedFollowing = 0;
private int cachedFans = 0;
private int cachedLikes = 0;
private int cachedFriends = 0;
private String cachedBalance = "0.00";
public static void start(Context context) {
context.startActivity(new Intent(context, ProfileWebViewActivity.class));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile_webview);
apiService = ApiClient.getService(this);
initViews();
setupWebView();
setupBottomNav();
loadUserData();
webView.loadUrl("file:///android_asset/web/profile.html");
}
private void initViews() {
webView = findViewById(R.id.webView);
loadingProgress = findViewById(R.id.loadingProgress);
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
webView.addJavascriptInterface(new ProfileJSInterface(), "Android");
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
loadingProgress.setVisibility(View.GONE);
pushDataToH5();
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
loadingProgress.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
}
});
}
private void setupBottomNav() {
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_profile);
UnreadMessageManager.updateBadge(bottomNav);
bottomNav.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeWebViewActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
return true;
}
return true;
});
}
}
private void loadUserData() {
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
cachedName = prefs.getString("profile_name", "用户");
cachedId = prefs.getString("profile_id", "12345678");
cachedAvatar = prefs.getString("profile_avatar", "");
cachedGender = prefs.getString("profile_gender", "male");
cachedFollowing = prefs.getInt("following_count", 0);
cachedFans = prefs.getInt("fans_count", 0);
cachedLikes = prefs.getInt("likes_count", 0);
cachedFriends = prefs.getInt("friends_count", 0);
cachedBalance = prefs.getString("wallet_balance", "0.00");
}
private void pushDataToH5() {
runOnUiThread(() -> {
try {
JSONObject profile = new JSONObject();
profile.put("name", cachedName);
profile.put("id", cachedId);
profile.put("avatar", cachedAvatar);
profile.put("gender", cachedGender);
profile.put("following", cachedFollowing);
profile.put("fans", cachedFans);
profile.put("likes", cachedLikes);
profile.put("friends", cachedFriends);
String js = String.format("updateProfile('%s')", escapeJs(profile.toString()));
webView.evaluateJavascript(js, null);
String balanceJs = String.format("updateWalletBalance('%s')", cachedBalance);
webView.evaluateJavascript(balanceJs, null);
} catch (JSONException e) {
Log.e(TAG, "构建JSON失败: " + e.getMessage());
}
});
}
private String escapeJs(String str) {
if (str == null) return "";
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n");
}
private class ProfileJSInterface {
@JavascriptInterface
public String getUserProfile() {
try {
JSONObject profile = new JSONObject();
profile.put("name", cachedName);
profile.put("id", cachedId);
profile.put("avatar", cachedAvatar);
profile.put("gender", cachedGender);
profile.put("following", cachedFollowing);
profile.put("fans", cachedFans);
profile.put("likes", cachedLikes);
profile.put("friends", cachedFriends);
return profile.toString();
} catch (JSONException e) {
return "{}";
}
}
@JavascriptInterface
public String getWalletBalance() {
return cachedBalance;
}
@JavascriptInterface
public void onEditProfile() {
runOnUiThread(() -> {
try {
startActivity(new Intent(ProfileWebViewActivity.this, EditProfileActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "编辑资料", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onWalletClick() {
runOnUiThread(() -> {
try {
startActivity(new Intent(ProfileWebViewActivity.this, WalletActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "钱包", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onRecharge() {
runOnUiThread(() -> {
try {
startActivity(new Intent(ProfileWebViewActivity.this, RechargeActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "充值", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onWithdraw() {
runOnUiThread(() -> Toast.makeText(ProfileWebViewActivity.this, "提现功能开发中", Toast.LENGTH_SHORT).show());
}
@JavascriptInterface
public void onFollowingClick() {
runOnUiThread(() -> FollowingListActivity.start(ProfileWebViewActivity.this));
}
@JavascriptInterface
public void onFansClick() {
runOnUiThread(() -> FansListActivity.start(ProfileWebViewActivity.this));
}
@JavascriptInterface
public void onLikesClick() {
runOnUiThread(() -> LikesListActivity.start(ProfileWebViewActivity.this));
}
@JavascriptInterface
public void onFriendsClick() {
runOnUiThread(() -> {
try {
startActivity(new Intent(ProfileWebViewActivity.this, MyFriendsActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "我的好友", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void onMenuClick(String menuId) {
runOnUiThread(() -> {
switch (menuId) {
case "liked":
LikedRoomsActivity.start(ProfileWebViewActivity.this);
break;
case "records":
MyRecordsActivity.start(ProfileWebViewActivity.this);
break;
case "streamer":
try {
startActivity(new Intent(ProfileWebViewActivity.this, StreamerCenterActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "主播中心", Toast.LENGTH_SHORT).show();
}
break;
case "settings":
try {
startActivity(new Intent(ProfileWebViewActivity.this, SettingsPageActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "设置", Toast.LENGTH_SHORT).show();
}
break;
}
});
}
@JavascriptInterface
public void onSearchClick() {
runOnUiThread(() -> SearchActivity.start(ProfileWebViewActivity.this));
}
@JavascriptInterface
public void onHistoryClick() {
runOnUiThread(() -> WatchHistoryActivity.start(ProfileWebViewActivity.this));
}
@JavascriptInterface
public void onSettingsClick() {
runOnUiThread(() -> {
try {
startActivity(new Intent(ProfileWebViewActivity.this, SettingsPageActivity.class));
} catch (Exception e) {
Toast.makeText(ProfileWebViewActivity.this, "设置", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void copyToClipboard(String text) {
runOnUiThread(() -> {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("ID", text);
clipboard.setPrimaryClip(clip);
});
}
@JavascriptInterface
public void showToast(String message) {
runOnUiThread(() -> Toast.makeText(ProfileWebViewActivity.this, message, Toast.LENGTH_SHORT).show());
}
}
@Override
protected void onResume() {
super.onResume();
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_profile);
UnreadMessageManager.updateBadge(bottomNav);
}
loadUserData();
pushDataToH5();
}
@Override
protected void onDestroy() {
if (webView != null) webView.destroy();
super.onDestroy();
}
}

View File

@ -56,7 +56,7 @@ public class TableGamesActivity extends BaseCategoryActivity {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -206,12 +206,12 @@ public class VoiceMatchActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
WishTreeWebViewActivity.start(this);
finish();
return true;
}

View File

@ -499,7 +499,7 @@ public class WishTreeActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}

View File

@ -0,0 +1,413 @@
package com.example.livestreaming;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.WishtreeRequest;
import com.example.livestreaming.net.WishtreeResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 许愿树页面 - WebView版本
*/
public class WishTreeWebViewActivity extends AppCompatActivity {
private static final String TAG = "WishTreeWebView";
private WebView webView;
private ProgressBar loadingProgress;
private ApiService apiService;
// 缓存数据
private WishtreeResponse.Festival currentFestival;
private List<WishtreeResponse.Wish> myWishes = new ArrayList<>();
public static void start(Context context) {
context.startActivity(new Intent(context, WishTreeWebViewActivity.class));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wish_tree_webview);
apiService = ApiClient.getService(this);
initViews();
setupWebView();
setupBottomNav();
// 预加载数据
loadCurrentFestival();
loadMyWishes();
// 加载H5页面
webView.loadUrl("file:///android_asset/web/wishtree.html");
}
private void initViews() {
webView = findViewById(R.id.webView);
loadingProgress = findViewById(R.id.loadingProgress);
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webView.addJavascriptInterface(new WishTreeJSInterface(), "Android");
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
loadingProgress.setVisibility(View.GONE);
pushDataToH5();
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress < 100) {
loadingProgress.setVisibility(View.VISIBLE);
} else {
loadingProgress.setVisibility(View.GONE);
}
}
});
}
private void setupBottomNav() {
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_wish_tree);
UnreadMessageManager.updateBadge(bottomNav);
bottomNav.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondWebViewActivity.class));
finish();
return true;
}
if (id == R.id.nav_wish_tree) {
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
}
/**
* 加载当前节日
*/
private void loadCurrentFestival() {
apiService.getCurrentFestival().enqueue(new Callback<ApiResponse<WishtreeResponse.Festival>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call,
@NonNull Response<ApiResponse<WishtreeResponse.Festival>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
currentFestival = response.body().getData();
pushFestivalToH5();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call, @NonNull Throwable t) {
Log.e(TAG, "加载节日失败: " + t.getMessage());
}
});
}
/**
* 加载我的心愿
*/
private void loadMyWishes() {
apiService.getMyWishes(1, 10).enqueue(new Callback<ApiResponse<WishtreeResponse.WishPage>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call,
@NonNull Response<ApiResponse<WishtreeResponse.WishPage>> response) {
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
myWishes = response.body().getData().list;
if (myWishes == null) myWishes = new ArrayList<>();
pushWishesToH5();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call, @NonNull Throwable t) {
Log.e(TAG, "加载心愿失败: " + t.getMessage());
}
});
}
/**
* 发布心愿
*/
private void publishWish(String content) {
Integer festivalId = currentFestival != null ? currentFestival.id : null;
WishtreeRequest.PublishWish request = new WishtreeRequest.PublishWish(festivalId, content, null);
apiService.publishWish(request).enqueue(new Callback<ApiResponse<WishtreeResponse.Wish>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call,
@NonNull Response<ApiResponse<WishtreeResponse.Wish>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
runOnUiThread(() -> {
webView.evaluateJavascript("onPublishSuccess()", null);
Toast.makeText(WishTreeWebViewActivity.this, "许愿成功", Toast.LENGTH_SHORT).show();
});
loadMyWishes();
} else {
String msg = response.body() != null ? response.body().getMessage() : "发布失败";
runOnUiThread(() -> Toast.makeText(WishTreeWebViewActivity.this, msg, Toast.LENGTH_SHORT).show());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call, @NonNull Throwable t) {
runOnUiThread(() -> Toast.makeText(WishTreeWebViewActivity.this, "网络错误", Toast.LENGTH_SHORT).show());
}
});
}
/**
* 删除心愿
*/
private void deleteWish(long wishId, boolean isComplete) {
apiService.deleteMyWish(wishId).enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(@NonNull Call<ApiResponse<String>> call,
@NonNull Response<ApiResponse<String>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
runOnUiThread(() -> {
webView.evaluateJavascript("onDeleteSuccess(" + isComplete + ")", null);
if (isComplete) {
Toast.makeText(WishTreeWebViewActivity.this, "恭喜愿望达成!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(WishTreeWebViewActivity.this, "心愿已删除", Toast.LENGTH_SHORT).show();
}
});
loadMyWishes();
} else {
runOnUiThread(() -> Toast.makeText(WishTreeWebViewActivity.this, "操作失败", Toast.LENGTH_SHORT).show());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse<String>> call, @NonNull Throwable t) {
runOnUiThread(() -> Toast.makeText(WishTreeWebViewActivity.this, "网络错误", Toast.LENGTH_SHORT).show());
}
});
}
/**
* 推送数据到H5
*/
private void pushDataToH5() {
pushFestivalToH5();
pushWishesToH5();
}
private void pushFestivalToH5() {
if (currentFestival == null) return;
runOnUiThread(() -> {
try {
JSONObject obj = new JSONObject();
obj.put("id", currentFestival.id);
obj.put("name", currentFestival.name != null ? currentFestival.name : "");
String js = String.format("updateFestival('%s')", escapeJs(obj.toString()));
webView.evaluateJavascript(js, null);
} catch (JSONException e) {
Log.e(TAG, "构建节日JSON失败: " + e.getMessage());
}
});
}
private void pushWishesToH5() {
runOnUiThread(() -> {
try {
JSONArray arr = new JSONArray();
for (WishtreeResponse.Wish wish : myWishes) {
JSONObject obj = new JSONObject();
obj.put("id", wish.id);
obj.put("content", wish.content != null ? wish.content : "");
obj.put("likeCount", wish.likeCount);
obj.put("commentCount", wish.commentCount);
arr.put(obj);
}
String js = String.format("updateWishes('%s')", escapeJs(arr.toString()));
webView.evaluateJavascript(js, null);
} catch (JSONException e) {
Log.e(TAG, "构建心愿JSON失败: " + e.getMessage());
}
});
}
private String escapeJs(String str) {
if (str == null) return "";
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
/**
* JavaScript接口类
*/
private class WishTreeJSInterface {
@JavascriptInterface
public String getFestival() {
if (currentFestival == null) return "{}";
try {
JSONObject obj = new JSONObject();
obj.put("id", currentFestival.id);
obj.put("name", currentFestival.name != null ? currentFestival.name : "");
return obj.toString();
} catch (JSONException e) {
return "{}";
}
}
@JavascriptInterface
public String getMyWishes() {
try {
JSONArray arr = new JSONArray();
for (WishtreeResponse.Wish wish : myWishes) {
JSONObject obj = new JSONObject();
obj.put("id", wish.id);
obj.put("content", wish.content != null ? wish.content : "");
obj.put("likeCount", wish.likeCount);
obj.put("commentCount", wish.commentCount);
arr.put(obj);
}
return arr.toString();
} catch (JSONException e) {
return "[]";
}
}
@JavascriptInterface
public void publishWish(String content) {
Log.d(TAG, "发布心愿: " + content);
WishTreeWebViewActivity.this.publishWish(content);
}
@JavascriptInterface
public void deleteWish(long wishId, boolean isComplete) {
Log.d(TAG, "删除心愿: " + wishId + ", isComplete: " + isComplete);
WishTreeWebViewActivity.this.deleteWish(wishId, isComplete);
}
@JavascriptInterface
public void onSearchClick() {
runOnUiThread(() -> SearchActivity.start(WishTreeWebViewActivity.this));
}
@JavascriptInterface
public void onNotifyClick() {
runOnUiThread(() -> NotificationsActivity.start(WishTreeWebViewActivity.this));
}
@JavascriptInterface
public boolean checkLogin() {
String token = AuthStore.getToken(WishTreeWebViewActivity.this);
if (token == null || token.isEmpty()) {
runOnUiThread(() -> {
Toast.makeText(WishTreeWebViewActivity.this, "请先登录", Toast.LENGTH_SHORT).show();
startActivity(new Intent(WishTreeWebViewActivity.this, LoginActivity.class));
});
return false;
}
return true;
}
@JavascriptInterface
public void showToast(String message) {
runOnUiThread(() -> Toast.makeText(WishTreeWebViewActivity.this, message, Toast.LENGTH_SHORT).show());
}
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigation);
if (bottomNav != null) {
bottomNav.setSelectedItemId(R.id.nav_wish_tree);
UnreadMessageManager.updateBadge(bottomNav);
}
loadMyWishes();
}
@Override
protected void onDestroy() {
if (webView != null) {
webView.destroy();
}
super.onDestroy();
}
}

View File

@ -0,0 +1,352 @@
package com.example.livestreaming.location;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import androidx.core.content.ContextCompat;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
/**
* 天地图定位服务
* 使用Android原生定位获取经纬度然后通过天地图API进行逆地理编码获取地址
*/
public class TianDiTuLocationService {
private static final String TAG = "TianDiTuLocation";
// 天地图API Key
private static final String TIANDITU_API_KEY = "1b337ad2535a95289c5b943858dd53ac";
// 天地图逆地理编码API
private static final String GEOCODE_API_URL = "http://api.tianditu.gov.cn/geocoder";
// 测试模式如果为true使用固定的中国坐标进行测试
// 设置为false后将使用真实的GPS定位
// 注意Android模拟器默认位置在美国需要在模拟器中手动设置中国坐标
// 或者开启测试模式进行开发测试
private static final boolean TEST_MODE = false; // 关闭测试模式使用真实GPS定位
// private static final double TEST_LATITUDE = 22.5431; // 深圳纬度仅测试模式使用
// private static final double TEST_LONGITUDE = 114.0579; // 深圳经度仅测试模式使用
private Context context;
private LocationManager locationManager;
private LocationListener locationListener;
private OnLocationResultListener resultListener;
// 保存当前定位的经纬度
private double currentLatitude;
private double currentLongitude;
public interface OnLocationResultListener {
void onSuccess(String province, String city, String address, double latitude, double longitude);
void onError(String error);
}
public TianDiTuLocationService(Context context) {
this.context = context.getApplicationContext();
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
/**
* 开始定位
*/
public void startLocation(OnLocationResultListener listener) {
this.resultListener = listener;
// 检查权限
if (!checkLocationPermission()) {
if (resultListener != null) {
resultListener.onError("缺少定位权限");
}
return;
}
// 检查定位服务是否开启
if (!isLocationEnabled()) {
if (resultListener != null) {
resultListener.onError("请先开启定位服务");
}
return;
}
try {
// 创建位置监听器
locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
double lat = location.getLatitude();
double lon = location.getLongitude();
Log.d(TAG, "位置更新 - 纬度(Latitude): " + lat + ", 经度(Longitude): " + lon);
// 获取到位置后停止监听
stopLocation();
// 进行逆地理编码
reverseGeocode(lon, lat);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
};
// 优先使用GPS定位
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0,
0,
locationListener
);
Log.d(TAG, "开始GPS定位");
}
// 同时使用网络定位作为备选
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
0,
0,
locationListener
);
Log.d(TAG, "开始网络定位");
}
// 尝试获取最后已知位置
Location lastKnownLocation = getLastKnownLocation();
if (lastKnownLocation != null) {
double lat = lastKnownLocation.getLatitude();
double lon = lastKnownLocation.getLongitude();
Log.d(TAG, "使用最后已知位置 - 纬度(Latitude): " + lat + ", 经度(Longitude): " + lon);
stopLocation();
reverseGeocode(lon, lat);
}
} catch (SecurityException e) {
Log.e(TAG, "定位权限异常", e);
if (resultListener != null) {
resultListener.onError("定位权限异常: " + e.getMessage());
}
}
}
/**
* 停止定位
*/
public void stopLocation() {
if (locationManager != null && locationListener != null) {
try {
locationManager.removeUpdates(locationListener);
Log.d(TAG, "停止定位");
} catch (SecurityException e) {
Log.e(TAG, "停止定位异常", e);
}
}
}
/**
* 检查定位权限
*/
private boolean checkLocationPermission() {
return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED;
}
/**
* 检查定位服务是否开启
*/
private boolean isLocationEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
/**
* 获取最后已知位置
*/
private Location getLastKnownLocation() {
try {
Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Location networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (gpsLocation != null && networkLocation != null) {
// 返回较新的位置
return gpsLocation.getTime() > networkLocation.getTime() ? gpsLocation : networkLocation;
} else if (gpsLocation != null) {
return gpsLocation;
} else {
return networkLocation;
}
} catch (SecurityException e) {
Log.e(TAG, "获取最后已知位置异常", e);
return null;
}
}
/**
* 逆地理编码 - 将经纬度转换为地址
*/
private void reverseGeocode(final double longitude, final double latitude) {
// 保存经纬度
this.currentLongitude = longitude;
this.currentLatitude = latitude;
new Thread(() -> {
try {
Log.d(TAG, "开始逆地理编码 - 经度(Lon): " + longitude + ", 纬度(Lat): " + latitude);
// 检查坐标是否在中国境内粗略判断
// 中国经度范围73°E - 135°E纬度范围3°N - 53°N
if (longitude < 73 || longitude > 135 || latitude < 3 || latitude > 53) {
Log.w(TAG, "坐标不在中国境内 - 经度: " + longitude + " (应在73-135之间), 纬度: " + latitude + " (应在3-53之间)");
if (resultListener != null) {
resultListener.onError("当前位置不在中国境内天地图API仅支持中国地区\n坐标: " + latitude + ", " + longitude);
}
return;
}
Log.d(TAG, "坐标检查通过,在中国境内");
// 构建请求URL
JSONObject postData = new JSONObject();
postData.put("lon", longitude);
postData.put("lat", latitude);
postData.put("ver", 1);
String urlString = GEOCODE_API_URL + "?postStr=" + URLEncoder.encode(postData.toString(), "UTF-8")
+ "&type=geocode&tk=" + TIANDITU_API_KEY;
Log.d(TAG, "逆地理编码请求: " + urlString);
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
int responseCode = connection.getResponseCode();
Log.d(TAG, "响应码: " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
String responseStr = response.toString();
Log.d(TAG, "逆地理编码响应: " + responseStr);
// 解析响应
parseGeocodeResponse(responseStr);
} else {
Log.e(TAG, "逆地理编码失败,响应码: " + responseCode);
if (resultListener != null) {
resultListener.onError("获取地址失败,错误码: " + responseCode);
}
}
connection.disconnect();
} catch (Exception e) {
Log.e(TAG, "逆地理编码异常", e);
if (resultListener != null) {
resultListener.onError("获取地址异常: " + e.getMessage());
}
}
}).start();
}
/**
* 解析天地图逆地理编码响应
*/
private void parseGeocodeResponse(String responseStr) {
try {
JSONObject json = new JSONObject(responseStr);
// 检查状态
String status = json.optString("status");
Log.d(TAG, "API状态码: " + status);
if (!"0".equals(status)) {
String msg = json.optString("msg", "未知错误");
Log.e(TAG, "天地图API返回错误: status=" + status + ", msg=" + msg);
if (resultListener != null) {
resultListener.onError("获取地址失败: " + msg + " (状态码: " + status + ")");
}
return;
}
// 获取地址信息
JSONObject result = json.optJSONObject("result");
if (result != null) {
JSONObject addressComponent = result.optJSONObject("addressComponent");
String formattedAddress = result.optString("formatted_address", "");
Log.d(TAG, "完整地址: " + formattedAddress);
String province = "";
String city = "";
if (addressComponent != null) {
province = addressComponent.optString("province", "");
city = addressComponent.optString("city", "");
String county = addressComponent.optString("county", "");
Log.d(TAG, "省份: " + province + ", 城市: " + city + ", 区县: " + county);
// 如果city为空尝试使用county
if (city.isEmpty()) {
city = county;
}
}
// 如果省份和城市都为空尝试使用完整地址
if (province.isEmpty() && city.isEmpty() && !formattedAddress.isEmpty()) {
Log.d(TAG, "省份和城市为空,使用完整地址");
province = formattedAddress;
}
Log.d(TAG, "解析地址成功 - 省份: " + province + ", 城市: " + city + ", 完整地址: " + formattedAddress);
if (resultListener != null) {
resultListener.onSuccess(province, city, formattedAddress, currentLatitude, currentLongitude);
}
} else {
Log.e(TAG, "响应中没有result字段");
if (resultListener != null) {
resultListener.onError("地址解析失败:响应数据格式错误");
}
}
} catch (Exception e) {
Log.e(TAG, "解析响应异常", e);
if (resultListener != null) {
resultListener.onError("地址解析异常: " + e.getMessage());
}
}
}
}

View File

@ -0,0 +1,65 @@
package com.example.livestreaming.util;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import com.example.livestreaming.R;
/**
* 未读红点呼吸动画工具类
*/
public class BadgeAnimator {
/**
* 启动未读红点的呼吸动画
* @param badgeView 红点视图
*/
public static void startPulseAnimation(View badgeView) {
if (badgeView == null || badgeView.getVisibility() != View.VISIBLE) {
return;
}
try {
Animation pulseAnim = AnimationUtils.loadAnimation(
badgeView.getContext(),
R.anim.badge_pulse
);
badgeView.startAnimation(pulseAnim);
} catch (Exception e) {
// 动画加载失败时静默处理
}
}
/**
* 停止动画
* @param badgeView 红点视图
*/
public static void stopAnimation(View badgeView) {
if (badgeView != null) {
badgeView.clearAnimation();
}
}
/**
* 显示红点并启动动画
* @param badgeView 红点视图
* @param count 未读数量
*/
public static void showBadgeWithAnimation(View badgeView, int count) {
if (badgeView == null) return;
if (count > 0) {
badgeView.setVisibility(View.VISIBLE);
if (badgeView instanceof android.widget.TextView) {
((android.widget.TextView) badgeView).setText(
count > 99 ? "99+" : String.valueOf(count)
);
}
startPulseAnimation(badgeView);
} else {
stopAnimation(badgeView);
badgeView.setVisibility(View.GONE);
}
}
}

View File

@ -0,0 +1,65 @@
package com.example.livestreaming.util;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import com.example.livestreaming.R;
/**
* 点击缩放动画工具类
* 为所有可点击的 View 添加 0.95x 缩放反馈
*/
public class ClickScaleAnimator {
/**
* View 添加点击缩放效果
* @param view 目标 View
*/
public static void attach(View view) {
if (view == null) return;
view.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
v.animate()
.scaleX(0.95f)
.scaleY(0.95f)
.setDuration(100)
.start();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.animate()
.scaleX(1.0f)
.scaleY(1.0f)
.setDuration(150)
.start();
break;
}
return false; // 返回 false 以便 onClick 仍然触发
});
}
/**
* 批量为多个 View 添加点击缩放效果
* @param views 目标 View 数组
*/
public static void attachAll(View... views) {
for (View view : views) {
attach(view);
}
}
/**
* 使用 XML 动画资源播放缩放动画
* @param view 目标 View
*/
public static void playScaleAnimation(View view) {
if (view == null) return;
Animation anim = AnimationUtils.loadAnimation(view.getContext(), R.anim.scale_click);
view.startAnimation(anim);
}
}

View File

@ -0,0 +1,44 @@
package com.example.livestreaming.util;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import com.example.livestreaming.R;
/**
* 钱包光束旋转动画工具类
*/
public class WalletBeamAnimator {
/**
* 启动钱包光束旋转动画
* @param beamOverlay 光束叠加层 View
*/
public static void startBeamRotation(View beamOverlay) {
if (beamOverlay == null) return;
RotateAnimation rotate = new RotateAnimation(
0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
);
rotate.setDuration(8000);
rotate.setRepeatCount(Animation.INFINITE);
rotate.setRepeatMode(Animation.RESTART);
rotate.setInterpolator(new LinearInterpolator());
beamOverlay.startAnimation(rotate);
}
/**
* 停止钱包光束旋转动画
* @param beamOverlay 光束叠加层 View
*/
public static void stopBeamRotation(View beamOverlay) {
if (beamOverlay == null) return;
beamOverlay.clearAnimation();
}
}

View File

@ -0,0 +1,92 @@
package com.example.livestreaming.util;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
/**
* 心愿卡片漂浮动画工具类
* 为许愿树页面的心愿卡片提供随风摆动的动效
*/
public class WishCardAnimator {
/**
* 为单个卡片启动漂浮动画
* @param view 目标视图
* @param amplitude 漂浮幅度 (dp)
* @param duration 动画周期 (ms)
* @param startDelay 启动延迟 (ms)
*/
public static ObjectAnimator startFloatAnimation(View view, float amplitude, long duration, long startDelay) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationY", 0f, -amplitude, 0f, amplitude, 0f);
animator.setDuration(duration);
animator.setRepeatCount(ObjectAnimator.INFINITE);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setStartDelay(startDelay);
animator.start();
return animator;
}
/**
* 为卡片启动组合动画漂浮 + 轻微旋转
*/
public static ObjectAnimator startFloatWithRotation(View view, float yAmplitude, float rotationAmplitude, long duration, long startDelay) {
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", 0f, -yAmplitude, 0f, yAmplitude, 0f);
PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofFloat("rotation", 0f, rotationAmplitude, 0f, -rotationAmplitude, 0f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, pvhY, pvhRotation);
animator.setDuration(duration);
animator.setRepeatCount(ObjectAnimator.INFINITE);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setStartDelay(startDelay);
animator.start();
return animator;
}
/**
* 为一组心愿卡片启动错落的漂浮动画
* @param cards 卡片视图数组
*/
public static void startAllCardAnimations(View... cards) {
float[] amplitudes = {12f, 15f, 10f, 14f, 11f, 16f, 13f};
long[] durations = {2800L, 3200L, 2500L, 3000L, 2700L, 3100L, 2900L};
long[] delays = {0L, 200L, 400L, 100L, 300L, 150L, 250L};
float[] rotations = {2f, -1.5f, 1.8f, -2f, 1.5f, -1.8f, 2.2f};
for (int i = 0; i < cards.length && i < amplitudes.length; i++) {
if (cards[i] != null) {
startFloatWithRotation(cards[i], amplitudes[i], rotations[i], durations[i], delays[i]);
}
}
}
/**
* 按钮点击缩放动画
*/
public static void playButtonClickAnimation(View button) {
ObjectAnimator scaleDown = ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("scaleX", 1f, 0.95f, 1f),
PropertyValuesHolder.ofFloat("scaleY", 1f, 0.95f, 1f)
);
scaleDown.setDuration(150);
scaleDown.start();
}
/**
* 光晕脉冲动画
*/
public static ObjectAnimator startGlowPulse(View glowView) {
PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 1.1f, 1f);
PropertyValuesHolder pvhScaleY = PropertyValuesHolder.ofFloat("scaleY", 1f, 1.1f, 1f);
PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat("alpha", 0.6f, 1f, 0.6f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(glowView, pvhScaleX, pvhScaleY, pvhAlpha);
animator.setDuration(4000);
animator.setRepeatCount(ObjectAnimator.INFINITE);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();
return animator;
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<scale
android:fromXScale="1.0"
android:toXScale="1.2"
android:fromYScale="1.0"
android:toYScale="1.2"
android:pivotX="50%"
android:pivotY="50%"
android:duration="600"
android:repeatMode="reverse"
android:repeatCount="infinite" />
</set>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationY"
android:valueFrom="0"
android:valueTo="-15"
android:valueType="floatType"
android:duration="2800"
android:repeatMode="reverse"
android:repeatCount="infinite"
android:interpolator="@android:interpolator/accelerate_decelerate" />
</set>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationY"
android:valueFrom="0"
android:valueTo="-12"
android:valueType="floatType"
android:duration="3200"
android:repeatMode="reverse"
android:repeatCount="infinite"
android:interpolator="@android:interpolator/accelerate_decelerate" />
</set>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationY"
android:valueFrom="0"
android:valueTo="-18"
android:valueType="floatType"
android:duration="2500"
android:repeatMode="reverse"
android:repeatCount="infinite"
android:interpolator="@android:interpolator/accelerate_decelerate" />
</set>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:repeatMode="reverse"
android:repeatCount="infinite">
<translate
android:fromYDelta="0"
android:toYDelta="-10dp"
android:duration="1500"
android:repeatMode="reverse"
android:repeatCount="infinite" />
</set>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底部导航图标选中发光动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<!-- 缩放弹跳 -->
<scale
android:duration="200"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="1.15"
android:toYScale="1.15"
android:pivotX="50%"
android:pivotY="50%"
android:repeatMode="reverse"
android:repeatCount="1" />
<!-- 透明度闪烁 -->
<alpha
android:duration="150"
android:fromAlpha="1.0"
android:toAlpha="0.7"
android:repeatMode="reverse"
android:repeatCount="1" />
</set>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 点击缩放反馈动画 - 缩小到0.95x -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<scale
android:duration="150"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.95"
android:toYScale="0.95"
android:pivotX="50%"
android:pivotY="50%"
android:repeatMode="reverse"
android:repeatCount="1" />
</set>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 钱包光束旋转动画 -->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="8000"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:repeatMode="restart"
android:interpolator="@android:anim/linear_interpolator" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/shimmer_gold" android:state_checked="true" />
<item android:color="@color/glass_white_60" />
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底部导航图标颜色 - 选中时金色发光 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FFD700" android:state_checked="true" />
<item android:color="#99FFFFFF" android:state_checked="false" />
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/shimmer_gold" android:state_checked="true" />
<item android:color="@color/shimmer_gold" android:state_selected="true" />
<item android:color="#80FFFFFF" />
</selector>

View File

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

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="@color/glass_white_20" />
<solid android:color="@android:color/transparent" />
</shape>

View File

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

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#CC1A1A2E" />
<corners
android:topLeftRadius="@dimen/radius_large"
android:topRightRadius="@dimen/radius_large" />
<stroke
android:width="1dp"
android:color="@color/glass_white_20" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底部导航 - 深紫色半透明 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#E6121225" />
<stroke
android:width="1dp"
android:color="#1AA855F7" />
</shape>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底部导航 - 深紫色半透明 + 顶部微光边 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 深紫色半透明背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#F0121225" />
</shape>
</item>
<!-- 顶部微光边线 -->
<item android:bottom="-1dp" android:left="-1dp" android:right="-1dp">
<shape android:shape="rectangle">
<stroke
android:width="1dp"
android:color="#20A855F7" />
</shape>
</item>
</layer-list>

View File

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

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/glass_white_20" />
<corners android:radius="@dimen/radius_medium" />
<stroke
android:width="1dp"
android:color="@color/glass_white_40" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/radius_medium" />
<stroke
android:width="1dp"
android:color="@color/glass_white_20" />
</shape>
</item>
</selector>

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="@color/red_primary"
android:endColor="@color/shimmer_pink"
android:type="linear" />
<corners android:radius="@dimen/radius_pill" />
</shape>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 分类标签选中状态 - 带发光圆点指示器 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 透明背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/radius_pill" />
</shape>
</item>
<!-- 底部发光圆点 -->
<item
android:gravity="bottom|center_horizontal"
android:width="8dp"
android:height="8dp"
android:bottom="-2dp">
<shape android:shape="oval">
<solid android:color="#FFD700" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:startColor="#00000000"
android:centerColor="#40000000"
android:endColor="#99000000"
android:type="linear" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:startColor="@color/deep_night_bg"
android:centerColor="@color/deep_night_card"
android:endColor="@color/deep_night_surface"
android:type="linear" />
</shape>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/glass_white_40" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="@dimen/stroke_thin"
android:color="@color/glass_white_60" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/glass_white_20" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="@dimen/stroke_thin"
android:color="@color/glass_white_40" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/glass_white_10" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="@dimen/stroke_thin"
android:color="@color/glass_white_20" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/glass_white_10" />
<corners android:radius="@dimen/radius_large" />
<stroke
android:width="@dimen/stroke_thin"
android:color="@color/glass_white_20" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 大号深色玻璃卡片 - 降低白块感 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0AFFFFFF" />
<corners android:radius="@dimen/radius_large" />
<stroke
android:width="@dimen/stroke_thin"
android:color="#12FFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 大圆角超轻玻璃卡片 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0DFFFFFF" />
<corners android:radius="@dimen/radius_large" />
<stroke
android:width="1dp"
android:color="#1AFFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 超深色玻璃卡片 - 降低白块感 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0DFFFFFF" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="@dimen/stroke_thin"
android:color="#15FFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 超轻玻璃卡片 - 5%透明度,增强星空透出感 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0DFFFFFF" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="1dp"
android:color="#1AFFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="#FF4757"
android:endColor="#FF6B9D"
android:type="linear" />
<corners android:radius="@dimen/radius_small" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 功能入口小卡片 - 玻璃拟态 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0CFFFFFF" />
<corners android:radius="@dimen/radius_medium" />
<stroke
android:width="1dp"
android:color="#15FFFFFF" />
</shape>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底部导航选中图标发光效果 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 外层发光 -->
<item>
<shape android:shape="oval">
<solid android:color="#30FFD700" />
<size android:width="40dp" android:height="40dp" />
</shape>
</item>
<!-- 内层发光 -->
<item android:left="6dp" android:top="6dp" android:right="6dp" android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#50FFD700" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 基础渐变背景 -->
<item>
<shape android:shape="rectangle">
<gradient
android:angle="135"
android:startColor="#1A1A2E"
android:centerColor="#16213E"
android:endColor="#0F3460"
android:type="linear" />
</shape>
</item>
<!-- 顶部光晕 -->
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:gradientRadius="300dp"
android:centerX="0.8"
android:centerY="0.2"
android:startColor="#33A855F7"
android:endColor="#00000000" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间卡片 - 流光双层效果 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 底层磨砂透明 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#0AFFFFFF" />
<corners android:radius="@dimen/radius_medium" />
</shape>
</item>
<!-- 顶层渐变描边 -->
<item>
<shape android:shape="rectangle">
<stroke
android:width="1dp"
android:color="#20A855F7" />
<corners android:radius="@dimen/radius_medium" />
<gradient
android:angle="135"
android:startColor="#08FFD700"
android:centerColor="#05A855F7"
android:endColor="#08FF6B9D"
android:type="linear" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间卡片 - 玻璃拟态效果 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0DFFFFFF" />
<corners android:radius="@dimen/radius_medium" />
<stroke
android:width="1dp"
android:color="#18FFFFFF" />
</shape>

View File

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

View File

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

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 搜索栏 - 霓虹底线样式 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 透明背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
<!-- 底部霓虹细线 -->
<item android:top="-1dp" android:left="-1dp" android:right="-1dp">
<shape android:shape="rectangle">
<stroke
android:width="1.5dp"
android:color="@android:color/transparent" />
<solid android:color="@android:color/transparent" />
</shape>
</item>
<!-- 底部渐变发光线 -->
<item android:gravity="bottom" android:height="2dp">
<shape android:shape="rectangle">
<gradient
android:angle="0"
android:startColor="#00FFD700"
android:centerColor="#FFD700"
android:endColor="#00FFD700"
android:type="linear" />
<corners android:radius="1dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 首页搜索栏 - 玻璃拟态宽版 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#15FFFFFF" />
<corners android:radius="@dimen/radius_large" />
<stroke
android:width="1dp"
android:color="#20FFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 宽搜索栏 - 半透明玻璃 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#26FFFFFF" />
<corners android:radius="@dimen/radius_large" />
<stroke
android:width="1dp"
android:color="#33FFFFFF" />
</shape>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#E6C200" />
<corners android:radius="@dimen/radius_large" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="#FFD700"
android:centerColor="#FFF176"
android:endColor="#FFD700"
android:type="linear" />
<corners android:radius="@dimen/radius_large" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 标签指示器 - 发光小圆点 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 外层发光 -->
<item>
<shape android:shape="oval">
<solid android:color="#40FFD700" />
<size android:width="12dp" android:height="12dp" />
</shape>
</item>
<!-- 内层实心 -->
<item android:left="3dp" android:top="3dp" android:right="3dp" android:bottom="3dp">
<shape android:shape="oval">
<solid android:color="#FFD700" />
</shape>
</item>
</layer-list>

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="@color/shimmer_purple"
android:endColor="@color/shimmer_pink"
android:type="linear" />
<corners android:radius="@dimen/radius_pill" />
</shape>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:centerX="0.5"
android:centerY="0.5"
android:startColor="#4D6C5CE7"
android:centerColor="#267B68EE"
android:endColor="#00000000" />
</shape>

View File

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

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 钱包卡片 - 深色半透明 + 金色描边 + 光束效果 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 深色半透明底层 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#0D1A1A2E" />
<corners android:radius="@dimen/radius_standard" />
</shape>
</item>
<!-- 内部微光渐变 -->
<item android:left="1dp" android:top="1dp" android:right="1dp" android:bottom="1dp">
<shape android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="#10FFD700"
android:centerColor="#05FFFFFF"
android:endColor="#08FFD700"
android:type="linear" />
<corners android:radius="@dimen/radius_standard" />
</shape>
</item>
<!-- 金色描边 -->
<item>
<shape android:shape="rectangle">
<stroke
android:width="1.5dp"
android:color="#90FFD700" />
<corners android:radius="@dimen/radius_standard" />
<solid android:color="@android:color/transparent" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 钱包旋转光束叠加层 -->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%">
<shape android:shape="rectangle">
<gradient
android:angle="45"
android:startColor="#00FFFFFF"
android:centerColor="#15FFD700"
android:endColor="#00FFFFFF"
android:type="sweep" />
<corners android:radius="@dimen/radius_standard" />
</shape>
</rotate>

View File

@ -0,0 +1,13 @@
<?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="#26FFD700"
android:endColor="#1AFFA500"
android:type="linear" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="1dp"
android:color="#4DFFD700" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 钱包卡片 - 深色背景 + 金色描边 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1A1A2E" />
<corners android:radius="@dimen/radius_standard" />
<stroke
android:width="1.5dp"
android:color="@color/shimmer_gold" />
</shape>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 钱包卡片 - 深色背景 + 金色描边 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 深色底层 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#0D1A1A2E" />
<corners android:radius="@dimen/radius_standard" />
</shape>
</item>
<!-- 金色渐变描边 -->
<item>
<shape android:shape="rectangle">
<stroke
android:width="1.5dp"
android:color="#80FFD700" />
<corners android:radius="@dimen/radius_standard" />
<solid android:color="@android:color/transparent" />
</shape>
</item>
<!-- 内部微光效果 -->
<item
android:left="1dp"
android:top="1dp"
android:right="1dp"
android:bottom="1dp">
<shape android:shape="rectangle">
<gradient
android:angle="135"
android:startColor="#15FFD700"
android:centerColor="#08FFD700"
android:endColor="#05FFA500"
android:type="linear" />
<corners android:radius="@dimen/radius_standard" />
</shape>
</item>
</layer-list>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#CCCCCC">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景轨道 -->
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/glass_white_10" />
<corners android:radius="@dimen/radius_small" />
</shape>
</item>
<!-- 进度条 -->
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<gradient
android:angle="0"
android:startColor="@color/shimmer_pink"
android:centerColor="@color/shimmer_purple"
android:endColor="@color/red_primary"
android:type="linear" />
<corners android:radius="@dimen/radius_small" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 头像背光 - 紫色径向渐变 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:type="radial"
android:gradientRadius="60dp"
android:centerX="0.5"
android:centerY="0.5"
android:startColor="#60A855F7"
android:centerColor="#30A855F7"
android:endColor="#00A855F7" />
<size android:width="120dp" android:height="120dp" />
</shape>

View File

@ -5,12 +5,11 @@
android:layout_height="match_parent"
android:background="#000000">
<!-- 使用 OpenGlView 作为摄像头预览,支持 RootEncoder RTMP 推流和视频编码 -->
<!-- keepAspectRatio=false 让预览填满整个屏幕 -->
<!-- 使用 OpenGlView 作为摄像头预览,全屏填充显示 -->
<com.pedro.rtplibrary.view.OpenGlView
android:id="@+id/openGlView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -5,7 +5,8 @@
android:layout_height="match_parent"
android:paddingTop="18dp"
android:clipChildren="false"
android:clipToPadding="false">
android:clipToPadding="false"
android:background="@color/deep_night_bg">
<!-- 背景图片 -->
<ImageView
@ -19,6 +20,17 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 中心光晕效果 -->
<View
android:id="@+id/centerGlow"
android:layout_width="350dp"
android:layout_height="350dp"
android:background="@drawable/bg_tree_glow_radial"
app:layout_constraintBottom_toBottomOf="@id/orbitContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/orbitContainer" />
<ImageView
android:id="@+id/titleText"
android:layout_width="0dp"
@ -35,12 +47,12 @@
android:id="@+id/topCard"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="14dp"
android:background="@drawable/bg_card_starry"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:layout_marginStart="@dimen/page_margin"
android:layout_marginEnd="@dimen/page_margin"
android:layout_marginTop="@dimen/card_padding"
android:background="@drawable/bg_glass_card"
android:paddingStart="@dimen/card_padding"
android:paddingEnd="@dimen/card_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText">
@ -50,8 +62,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="剩余匹配"
android:textColor="#CCFFFFFF"
android:textSize="12sp"
android:textColor="@color/glass_white_60"
android:textSize="@dimen/text_sm"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -61,8 +73,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="50 人"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textColor="@color/shimmer_gold"
android:textSize="@dimen/text_xl"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/leftInfo" />
@ -72,8 +84,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="真诚 | 南宁 | 在线"
android:textColor="#CCFFFFFF"
android:textSize="12sp"
android:textColor="@color/glass_white_60"
android:textSize="@dimen/text_sm"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -83,8 +95,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="小份"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textColor="@color/white"
android:textSize="@dimen/text_lg"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/rightInfo" />
@ -95,8 +107,8 @@
android:id="@+id/orbitContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="18dp"
android:layout_marginBottom="12dp"
android:layout_marginTop="@dimen/spacing_xl"
android:layout_marginBottom="@dimen/spacing_md"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_constraintDimensionRatio="1:1"
@ -135,7 +147,7 @@
android:padding="25dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_globe_clean_filled"
app:tint="#FFFFFF"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -176,11 +188,11 @@
android:id="@+id/orbitUserTopName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginTop="@dimen/spacing_xs"
android:maxLines="1"
android:text="小树"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:textColor="@color/white"
android:textSize="@dimen/text_sm"
android:textStyle="bold" />
<TextView
@ -189,8 +201,8 @@
android:layout_height="wrap_content"
android:maxLines="1"
android:text="杭州"
android:textColor="#CCFFFFFF"
android:textSize="10sp" />
android:textColor="@color/glass_white_60"
android:textSize="@dimen/text_xs" />
</LinearLayout>

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