chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 关于我们页 U42
|
||||
*
|
||||
* 展示平台信息、版本号、法律链接。
|
||||
*/
|
||||
Page({
|
||||
data: {
|
||||
platformName: getApp().globalData.platformName,
|
||||
slogan: getApp().globalData.slogan,
|
||||
version: 'v1.0.0-RC.1',
|
||||
buildDate: '2026-09-01',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.setNavigationBarTitle({ title: '关于我们' });
|
||||
},
|
||||
|
||||
onViewPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy-settings/privacy-settings' });
|
||||
},
|
||||
|
||||
onCallService() {
|
||||
wx.makePhoneCall({ phoneNumber: '0511-80000000' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationBarTitleText":"about","usingComponents":{}}
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- 关于我们页 -->
|
||||
<view class="page">
|
||||
<view class="logo-section">
|
||||
<view class="logo-icon">💕</view>
|
||||
<view class="platform-name">{{platformName}}</view>
|
||||
<view class="slogan">{{slogan}}</view>
|
||||
<view class="version">版本 {{version}} · 构建于 {{buildDate}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">平台简介</view>
|
||||
<view class="intro-text">
|
||||
丹阳公益婚恋服务平台由丹阳市文明办、团委、妇联等多部门联合推动,面向丹阳市成年单身人士提供实名制、公益性的婚恋交友服务。
|
||||
</view>
|
||||
<view class="intro-text">
|
||||
平台采用实名认证 + 人工审核机制,确保用户真实身份;所有服务免费,不收取任何费用。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">联系我们</view>
|
||||
<view class="list-group">
|
||||
<view class="list-row" bindtap="onCallService">
|
||||
<view class="list-icon">📞</view>
|
||||
<view class="list-info">
|
||||
<text class="list-label">客服电话</text>
|
||||
<text class="list-desc">0511-80000000(工作日 9:00-17:00)</text>
|
||||
</view>
|
||||
<text class="list-arrow">›</text>
|
||||
</view>
|
||||
<view class="list-row" bindtap="onViewPrivacy">
|
||||
<view class="list-icon">🔒</view>
|
||||
<view class="list-info">
|
||||
<text class="list-label">隐私政策</text>
|
||||
<text class="list-desc">了解我们如何保护您的个人信息</text>
|
||||
</view>
|
||||
<text class="list-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<text>© 2026 丹阳市文明办 · 共青团丹阳市委 · 丹阳市妇联</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,112 @@
|
||||
/* 关于我们样式 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
background: linear-gradient(135deg, #E96B6B 0%, #ff9a9a 100%);
|
||||
padding: 80rpx 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.platform-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 24rpx 32rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 0 32rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.list-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 24rpx;
|
||||
width: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-label {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
color: #2d2d2d;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.list-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.list-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #ccc;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 48rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #ccc;
|
||||
padding: 0 32rpx;
|
||||
line-height: 1.8;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 注销账号页
|
||||
* 在线便捷渠道,不设置障碍
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
canWithdraw: false,
|
||||
blockers: [],
|
||||
reason: '',
|
||||
submitting: false,
|
||||
submitted: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.checkCanWithdraw();
|
||||
},
|
||||
|
||||
onReasonInput(e) {
|
||||
this.setData({ reason: e.detail.value });
|
||||
},
|
||||
|
||||
async checkCanWithdraw() {
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const res = await api.get('/account/can-withdraw', {}, { Authorization: `Bearer ${token}` });
|
||||
this.setData({
|
||||
canWithdraw: res.canWithdraw,
|
||||
blockers: Object.values(res.blockers || {})
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
async submitWithdraw() {
|
||||
if (!this.data.canWithdraw) return;
|
||||
this.setData({ submitting: true });
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
await api.post('/account/close', { reason: this.data.reason.trim() }, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({ submitted: true });
|
||||
wx.setStorageSync('accessToken', '');
|
||||
wx.setStorageSync('userId', '');
|
||||
} catch (err) {
|
||||
wx.showToast({ title: err.message || '注销失败', icon: 'none' });
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
if (this.data.submitted) {
|
||||
wx.reLaunch({ url: '/pages/login/login' });
|
||||
} else {
|
||||
wx.navigateBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "注销账号",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!-- 注销账号页 -->
|
||||
<view class="page" wx:if="{{!submitted}}">
|
||||
<view class="warning-banner">
|
||||
<text class="warning-icon">⚠</text>
|
||||
<text class="warning-title">注销账号</text>
|
||||
</view>
|
||||
|
||||
<view class="info-section">
|
||||
<text class="section-title">注销后将:</text>
|
||||
<view class="info-list">
|
||||
<view class="info-item">• 停止所有推荐</view>
|
||||
<view class="info-item">• 资料停止向他人展示</view>
|
||||
<view class="info-item">• 聊天停止</view>
|
||||
<view class="info-item">• 红娘服务结束</view>
|
||||
<view class="info-item">• 未开始的活动报名将取消</view>
|
||||
<view class="info-item">• 必要信息脱敏保留(符合法律要求)</view>
|
||||
<view class="info-item">• 所有数据不可恢复</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!canWithdraw}}" class="blocker-section">
|
||||
<text class="blocker-title">无法注销:存在进行中的业务</text>
|
||||
<view class="blocker-item" wx:for="{{blockers}}" wx:key="index">{{item}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">注销原因(选填)</text>
|
||||
<view class="reason-options">
|
||||
<view class="reason-chip {{reason==='太少了'?'active':''}}" data-val="太少了" bindtap="setReason">太少了</view>
|
||||
<view class="reason-chip {{reason==='找不到合适的'?'active':''}}" data-val="找不到合适的" bindtap="setReason">找不到合适的</view>
|
||||
<view class="reason-chip {{reason==='暂时不需要'?'active':''}}" data-val="暂时不需要" bindtap="setReason">暂时不需要</view>
|
||||
<view class="reason-chip {{reason==='其他原因'?'active':''}}" data-val="其他原因" bindtap="setReason">其他原因</view>
|
||||
</view>
|
||||
<textarea class="other-reason" placeholder="或其他原因(选填)..." value="{{reason}}" bindinput="onReasonInput" maxlength="200" />
|
||||
</view>
|
||||
|
||||
<button class="btn-confirm" bindtap="submitWithdraw" loading="{{submitting}}" disabled="{{!canWithdraw}}">
|
||||
确认注销账号
|
||||
</button>
|
||||
<text class="hint">平台不会要求您打电话或邮寄身份证,全程在线完成</text>
|
||||
</view>
|
||||
|
||||
<!-- 注销成功 -->
|
||||
<view class="page submitted" wx:if="{{submitted}}">
|
||||
<view class="success-icon">✓</view>
|
||||
<text class="success-title">账号已注销</text>
|
||||
<text class="success-hint">感谢您的使用,期待您未来再次相遇。</text>
|
||||
<button class="btn-back" bindtap="goBack">返回登录页</button>
|
||||
</view>
|
||||
@@ -0,0 +1,141 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
background: #FFF8F7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
background: #FFF3F2;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.warning-icon { font-size: 56rpx; display: block; }
|
||||
.warning-title { font-size: 32rpx; font-weight: bold; color: #E96B6B; display: block; margin-top: 12rpx; }
|
||||
|
||||
.info-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.blocker-section {
|
||||
background: #FFF8E1;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.blocker-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #E59A32;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.blocker-item {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.reason-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.reason-chip {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.reason-chip.active {
|
||||
background: #FFF3F2;
|
||||
border-color: #E96B6B;
|
||||
color: #E96B6B;
|
||||
}
|
||||
|
||||
.other-reason {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
font-size: 26rpx;
|
||||
background: #F9F9F9;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.btn-confirm[disabled] { background: #CCC; }
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 16rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 成功状态 */
|
||||
.submitted {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.success-icon { font-size: 80rpx; color: #E96B6B; margin-bottom: 24rpx; }
|
||||
.success-title { font-size: 36rpx; font-weight: bold; color: #1A1A1A; margin-bottom: 16rpx; }
|
||||
.success-hint { font-size: 28rpx; color: #666; text-align: center; line-height: 1.6; margin-bottom: 48rpx; }
|
||||
.btn-back {
|
||||
width: 60%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 我的婚恋状态页 U41
|
||||
* 展示当前状态,提供退出服务/正在了解/成功牵手入口
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
relationshipStatus: 'searching', // searching, paused, getting_to_know, matched_successfully
|
||||
recommendationEnabled: true,
|
||||
matchCount: 0,
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
async loadStatus() {
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const res = await api.get('/account/status', {}, { Authorization: `Bearer ${token}` });
|
||||
this.setData({
|
||||
relationshipStatus: res.relationshipStatus || 'searching',
|
||||
recommendationEnabled: res.recommendationEnabled !== false,
|
||||
matchCount: res.activeMatchCount || 0
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
async withdrawService() {
|
||||
wx.showModal({
|
||||
title: '退出婚恋服务',
|
||||
content: '退出后您的资料将不再展示,系统不再为您推荐对象,但账号保留。是否继续?',
|
||||
success: async (ret) => {
|
||||
if (ret.confirm) {
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
await api.post('/account/withdraw', {}, { Authorization: `Bearer ${token}` });
|
||||
this.setData({ recommendationEnabled: false, relationshipStatus: 'paused' });
|
||||
wx.showToast({ title: '已退出婚恋服务', icon: 'success' });
|
||||
} catch (err) {
|
||||
wx.showToast({ title: err.message || '操作失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async resumeService() {
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
await api.post('/account/resume', {}, { Authorization: `Bearer ${token}` });
|
||||
this.setData({ recommendationEnabled: true, relationshipStatus: 'searching' });
|
||||
wx.showToast({ title: '已重新开启', icon: 'success' });
|
||||
} catch (err) {
|
||||
wx.showToast({ title: err.message || '操作失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
goSuccessConfirm() {
|
||||
wx.navigateTo({ url: '/pages/success-confirm/success-confirm' });
|
||||
},
|
||||
|
||||
goPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy-settings/privacy-settings' });
|
||||
},
|
||||
|
||||
goLogout() {
|
||||
wx.showModal({
|
||||
title: '注销账号',
|
||||
content: '注销后所有数据将被删除且不可恢复,是否继续?',
|
||||
success: (ret) => {
|
||||
if (ret.confirm) {
|
||||
wx.navigateTo({ url: '/pages/account-logout/account-logout' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的婚恋状态",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- 我的婚恋状态页 U41 -->
|
||||
<view class="page">
|
||||
<view class="status-card" wx:if="{{recommendationEnabled}}">
|
||||
<view class="status-icon">💚</view>
|
||||
<text class="status-title">正在寻找</text>
|
||||
<text class="status-hint">您的资料正在推荐给潜在对象,共 {{matchCount}} 个活跃匹配</text>
|
||||
<button class="btn-withdraw" bindtap="withdrawService">退出婚恋服务</button>
|
||||
</view>
|
||||
|
||||
<view class="status-card paused" wx:if="{{!recommendationEnabled}}">
|
||||
<view class="status-icon">⏸</view>
|
||||
<text class="status-title">婚恋服务已暂停</text>
|
||||
<text class="status-hint">您的资料已隐藏,系统不再为您推荐对象。您可随时重新开启。</text>
|
||||
<button class="btn-resume" bindtap="resumeService">重新开启婚恋服务</button>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" bindtap="goSuccessConfirm">
|
||||
<text class="menu-icon">💍</text>
|
||||
<text class="menu-text">成功牵手确认</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="goPrivacy">
|
||||
<text class="menu-icon">🔒</text>
|
||||
<text class="menu-text">隐私设置</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item danger" bindtap="goLogout">
|
||||
<text class="menu-icon">🗑</text>
|
||||
<text class="menu-text">注销账号</text>
|
||||
<text class="menu-hint">注销后数据不可恢复</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,59 @@
|
||||
.page { padding: 32rpx; background: #FFF8F7; min-height: 100vh; }
|
||||
|
||||
.status-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.status-card.paused {
|
||||
background: #F9F9F9;
|
||||
}
|
||||
|
||||
.status-icon { font-size: 72rpx; display: block; margin-bottom: 16rpx; }
|
||||
.status-title { font-size: 32rpx; font-weight: bold; color: #1A1A1A; display: block; }
|
||||
.status-hint { font-size: 24rpx; color: #999; display: block; margin-top: 12rpx; line-height: 1.5; }
|
||||
|
||||
.btn-withdraw, .btn-resume {
|
||||
width: 80%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
margin-top: 32rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-withdraw {
|
||||
background: #FFF3F2;
|
||||
color: #E96B6B;
|
||||
}
|
||||
|
||||
.btn-resume {
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 28rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.menu-item.danger .menu-text { color: #E96B6B; }
|
||||
|
||||
.menu-icon { font-size: 36rpx; margin-right: 20rpx; }
|
||||
.menu-text { flex: 1; font-size: 28rpx; color: #1A1A1A; }
|
||||
.menu-hint { font-size: 22rpx; color: #999; margin-right: 12rpx; }
|
||||
.menu-arrow { font-size: 32rpx; color: #CCC; }
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 账号受限页 U48
|
||||
* 展示当前限制状态、原因,以及申诉入口
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
hasRestriction: false,
|
||||
accountStatus: 'normal',
|
||||
actions: [],
|
||||
canAppeal: false,
|
||||
appealEnabled: false,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
async loadStatus() {
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const res = await api.get('/appeals/account-status', {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({
|
||||
hasRestriction: res.hasRestriction,
|
||||
accountStatus: res.accountStatus,
|
||||
actions: res.actions || [],
|
||||
canAppeal: res.canAppeal,
|
||||
appealEnabled: res.appealEnabled,
|
||||
loading: false
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
goAppeal(e) {
|
||||
const action = e.currentTarget.dataset.action;
|
||||
wx.navigateTo({
|
||||
url: `/pages/appeal/appeal?type=${action.appealType}&actionId=${action.id}`
|
||||
});
|
||||
},
|
||||
|
||||
goHelp() {
|
||||
wx.navigateTo({ url: '/pages/help-center/help-center' });
|
||||
},
|
||||
|
||||
statusText(s) {
|
||||
const map = { banned: '已封禁', suspended: '临时暂停', limited: '功能限制' };
|
||||
return map[s] || s;
|
||||
},
|
||||
|
||||
actionTypeText(t) {
|
||||
const map = { ban: '永久封禁', feature_limit: '功能限制', temporary_suspend: '临时暂停', warning: '警告' };
|
||||
return map[t] || t;
|
||||
},
|
||||
|
||||
formatTime(t) {
|
||||
if (!t) return '-';
|
||||
return new Date(t).toLocaleDateString('zh-CN');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "账号状态",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- 账号受限页 U48 -->
|
||||
<view class="page" wx:if="{{!loading}}">
|
||||
<view class="status-banner" wx:if="{{hasRestriction}}">
|
||||
<view class="banner-icon">⚠</view>
|
||||
<text class="banner-title">账号受限</text>
|
||||
<text class="banner-status">{{statusText(accountStatus)}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!hasRestriction}}" class="normal-state">
|
||||
<view class="normal-icon">✓</view>
|
||||
<text class="normal-title">账号状态正常</text>
|
||||
<text class="normal-hint">您的账号目前无限制,可以正常使用所有功能。</text>
|
||||
</view>
|
||||
|
||||
<view class="actions-list" wx:if="{{hasRestriction && actions.length > 0}}">
|
||||
<view class="action-card" wx:for="{{actions}}" wx:key="id">
|
||||
<view class="action-header">
|
||||
<text class="action-type">{{actionTypeText(item.actionType)}}</text>
|
||||
<text class="action-time">{{formatTime(item.effectiveAt)}}</text>
|
||||
</view>
|
||||
<text class="action-reason">{{item.reason}}</text>
|
||||
<text class="action-expiry" wx:if="{{item.expiresAt}}">
|
||||
有效期至:{{formatTime(item.expiresAt)}}
|
||||
</text>
|
||||
<text class="action-expiry" wx:else>永久限制</text>
|
||||
<button
|
||||
wx:if="{{item.canAppeal && canAppeal}}"
|
||||
class="btn-appeal"
|
||||
data-action="{{item}}"
|
||||
bindtap="goAppeal">
|
||||
提交申诉
|
||||
</button>
|
||||
<text wx:if="{{item.canAppeal && !canAppeal && appealEnabled}}" class="appeal-hint">
|
||||
该类型限制暂不支持在线申诉,请联系客服。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="appeal-disabled" wx:if="{{hasRestriction && !appealEnabled}}">
|
||||
<text class="disabled-hint">当前申诉功能暂未开放,如有疑问请联系客服。</text>
|
||||
<button class="btn-contact" bindtap="goHelp">联系客服</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page loading-page" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
@@ -0,0 +1,156 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
background: #FFF8F7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
background: linear-gradient(135deg, #E96B6B 0%, #C44545 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
font-size: 72rpx;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.banner-status {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255,255,255,0.85);
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.normal-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 32rpx;
|
||||
}
|
||||
|
||||
.normal-icon {
|
||||
font-size: 80rpx;
|
||||
color: #67C23A;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.normal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.normal-hint {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.action-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.action-type {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #E96B6B;
|
||||
}
|
||||
|
||||
.action-time {
|
||||
font-size: 22rpx;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.action-reason {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-expiry {
|
||||
font-size: 24rpx;
|
||||
color: #E59A32;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.btn-appeal {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: #FFF3F2;
|
||||
color: #E96B6B;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1rpx solid #E96B6B;
|
||||
}
|
||||
|
||||
.appeal-hint {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.appeal-disabled {
|
||||
text-align: center;
|
||||
padding: 60rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.disabled-hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.btn-contact {
|
||||
width: 60%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading-page {
|
||||
text-align: center;
|
||||
padding: 100rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 页面占位骨架
|
||||
*/
|
||||
Page({
|
||||
data: {},
|
||||
onLoad(options) {},
|
||||
onShow() {}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationBarTitleText":"activity-detail","usingComponents":{}}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="page"><text>页面: activity-detail</text></view>
|
||||
@@ -0,0 +1 @@
|
||||
.page{padding:32rpx;background:#FFF8F7;min-height:100vh;text-align:center;color:#999;padding-top:200rpx;}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 活动反馈页 U32
|
||||
* 活动结束后开放,用户评价 + 表达进一步了解意向
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activityId: '',
|
||||
activityTitle: '',
|
||||
activityEndAt: '',
|
||||
overallExperience: '', // very_satisfied / satisfied / normal / dissatisfied
|
||||
willingNextTime: '', // willing / maybe / not_now
|
||||
metSomeoneInteresting: null, // true / false / null
|
||||
otherParticipants: [],
|
||||
selectedInterests: [],
|
||||
submitting: false,
|
||||
feedbackSubmitted: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.setData({ activityId: options.id });
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
// 加载活动信息
|
||||
const activityRes = await api.get(`/activities/${this.data.activityId}`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({
|
||||
activityTitle: activityRes.title,
|
||||
activityEndAt: activityRes.endAt
|
||||
});
|
||||
|
||||
// 加载同活动已签到用户列表(仅用于选择意向对象)
|
||||
const partsRes = await api.get(`/activities/${this.data.activityId}/participants`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({
|
||||
otherParticipants: (partsRes || []).filter(p => p.userId !== wx.getStorageSync('userId'))
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('加载失败', err);
|
||||
}
|
||||
},
|
||||
|
||||
onOverallChange(e) {
|
||||
this.setData({ overallExperience: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
onWillingChange(e) {
|
||||
this.setData({ willingNextTime: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
onMetSomeoneChange(e) {
|
||||
this.setData({ metSomeoneInteresting: e.currentTarget.dataset.value === 'yes' ? true : false });
|
||||
},
|
||||
|
||||
toggleInterest(userId) {
|
||||
const selected = this.data.selectedInterests;
|
||||
const idx = selected.indexOf(userId);
|
||||
if (idx >= 0) {
|
||||
selected.splice(idx, 1);
|
||||
} else {
|
||||
selected.push(userId);
|
||||
}
|
||||
this.setData({ selectedInterests: selected });
|
||||
},
|
||||
|
||||
async submitFeedback() {
|
||||
const { overallExperience, willingNextTime, metSomeoneInteresting, selectedInterests, activityId } = this.data;
|
||||
|
||||
if (!overallExperience || !willingNextTime) {
|
||||
wx.showToast({ title: '请填写必填项', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ submitting: true });
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const body = {
|
||||
overallExperience,
|
||||
willingNextTime,
|
||||
metSomeoneInteresting,
|
||||
interestTargetUserIds: selectedInterests
|
||||
};
|
||||
const res = await api.post(`/activities/${activityId}/feedback`, body, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({ feedbackSubmitted: true, submitting: false });
|
||||
if ((res.newMutuals || []).length > 0) {
|
||||
wx.showModal({
|
||||
title: '双向匹配成功!',
|
||||
content: `您与${res.newMutuals.length}位朋友互相表达了进一步了解意愿,快去缘分中心看看吧`,
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.switchTab({ url: '/pages/fate-center/fate-center' });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
wx.showToast({ title: err.message || '提交失败', icon: 'none' });
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
skipFeedback() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "活动反馈",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<!-- 活动反馈页 U32 -->
|
||||
<view class="page" wx:if="{{!feedbackSubmitted}}">
|
||||
<view class="header">
|
||||
<text class="title">活动反馈</text>
|
||||
<text class="activity-name">{{activityTitle}}</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">整体体验</text>
|
||||
<view class="radio-group">
|
||||
<view class="radio-item {{overallExperience==='very_satisfied'?'active':''}}" data-value="very_satisfied" bindtap="onOverallChange">
|
||||
<text>😊 非常满意</text>
|
||||
</view>
|
||||
<view class="radio-item {{overallExperience==='satisfied'?'active':''}}" data-value="satisfied" bindtap="onOverallChange">
|
||||
<text>🙂 满意</text>
|
||||
</view>
|
||||
<view class="radio-item {{overallExperience==='normal'?'active':''}}" data-value="normal" bindtap="onOverallChange">
|
||||
<text>😐 一般</text>
|
||||
</view>
|
||||
<view class="radio-item {{overallExperience==='dissatisfied'?'active':''}}" data-value="dissatisfied" bindtap="onOverallChange">
|
||||
<text>😕 不满意</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">是否愿意参加下一次活动</text>
|
||||
<view class="radio-group">
|
||||
<view class="radio-item {{willingNextTime==='willing'?'active':''}}" data-value="willing" bindtap="onWillingChange">
|
||||
<text>💚 愿意</text>
|
||||
</view>
|
||||
<view class="radio-item {{willingNextTime==='maybe'?'active':''}}" data-value="maybe" bindtap="onWillingChange">
|
||||
<text>🤔 看情况</text>
|
||||
</view>
|
||||
<view class="radio-item {{willingNextTime==='not_now'?'active':''}}" data-value="not_now" bindtap="onWillingChange">
|
||||
<text>😶 暂不参加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{activityEndAt}}">
|
||||
<text class="section-title">是否在活动中认识了想进一步了解的人</text>
|
||||
<view class="radio-group">
|
||||
<view class="radio-item {{metSomeoneInteresting===true?'active':''}}" data-value="yes" bindtap="onMetSomeoneChange">
|
||||
<text>是</text>
|
||||
</view>
|
||||
<view class="radio-item {{metSomeoneInteresting===false?'active':''}}" data-value="no" bindtap="onMetSomeoneChange">
|
||||
<text>否</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="participant-list" wx:if="{{metSomeoneInteresting && otherParticipants.length > 0}}">
|
||||
<text class="list-hint">选择您想进一步了解的人(仅活动结束后开放)</text>
|
||||
<view class="participant-item" wx:for="{{otherParticipants}}" wx:key="userId"
|
||||
bindtap="toggleInterest" data-user-id="{{item.userId}}">
|
||||
<image class="avatar" src="{{item.avatarFileKey || '/assets/default-avatar.png'}}" mode="aspectFill"/>
|
||||
<text class="name">{{item.displayName}}</text>
|
||||
<text class="checkmark" wx:if="{{selectedInterests.indexOf(item.userId) >= 0}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-submit" bindtap="submitFeedback" loading="{{submitting}}" disabled="{{!overallExperience || !willingNextTime}}">
|
||||
提交反馈
|
||||
</button>
|
||||
<button class="btn-skip" bindtap="skipFeedback">跳过反馈</button>
|
||||
</view>
|
||||
|
||||
<!-- 已提交状态 -->
|
||||
<view class="page submitted" wx:if="{{feedbackSubmitted}}">
|
||||
<view class="success-icon">✓</view>
|
||||
<text class="success-title">反馈已提交</text>
|
||||
<text class="success-hint">感谢您的参与!</text>
|
||||
<button class="btn-home" bindtap="goHome">返回首页</button>
|
||||
</view>
|
||||
@@ -0,0 +1,170 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
background: #FFF8F7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
padding: 24rpx;
|
||||
background: #F9F9F9;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.radio-item.active {
|
||||
background: #FFF3F2;
|
||||
border-color: #E96B6B;
|
||||
color: #E96B6B;
|
||||
}
|
||||
|
||||
.participant-list {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.participant-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
color: #E96B6B;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn-submit[disabled] {
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
border-radius: 44rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1rpx solid #DDD;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
/* 已提交状态 */
|
||||
.submitted {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 80rpx;
|
||||
color: #E96B6B;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.success-hint {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.btn-home {
|
||||
width: 80%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 活动互选结果页 U36
|
||||
* 展示活动中双方互相表达的意向结果
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activityId: '',
|
||||
activityTitle: '',
|
||||
matches: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
if (options.activityId) {
|
||||
this.setData({ activityId: options.activityId });
|
||||
this.loadMatches();
|
||||
}
|
||||
},
|
||||
|
||||
async loadMatches() {
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const res = await api.get(`/activities/${this.data.activityId}/interests/mutual`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
// 获取活动标题
|
||||
const actRes = await api.get(`/activities/${this.data.activityId}`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({
|
||||
activityTitle: actRes.title,
|
||||
matches: res || [],
|
||||
loading: false
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.loadMatches();
|
||||
},
|
||||
|
||||
goToProfile(userId) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/profile-detail/profile-detail?id=${userId}&activityId=${this.data.activityId}`
|
||||
});
|
||||
},
|
||||
|
||||
startChat(userId) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?userId=${userId}&activityId=${this.data.activityId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "活动缘分",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!-- 活动互选结果页 U36 -->
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">活动缘分</text>
|
||||
<text class="activity-name">{{activityTitle}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!loading && matches.length === 0}}" class="empty-state">
|
||||
<text class="empty-icon">✦</text>
|
||||
<text>暂无双向匹配</text>
|
||||
<text class="empty-hint">活动结束后,与其他参与者互相表达意愿后可在此查看</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!loading && matches.length > 0}}" class="match-list">
|
||||
<view class="match-card" wx:for="{{matches}}" wx:key="targetUserId"
|
||||
bindtap="goToProfile" data-user-id="{{item.targetUserId}}">
|
||||
<image class="match-avatar" src="{{item.fileKey || '/assets/default-avatar.png'}}" mode="aspectFill"/>
|
||||
<view class="match-info">
|
||||
<text class="match-name">{{item.name}}</text>
|
||||
<text class="match-meta">{{item.age}}岁 · {{item.locationName || ''}}</text>
|
||||
</view>
|
||||
<view class="match-actions" catchtap>
|
||||
<button class="btn-chat" bindtap="startChat" data-user-id="{{item.targetUserId}}">聊天</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-refresh" bindtap="onRefresh" wx:if="{{!loading}}">刷新</button>
|
||||
</view>
|
||||
@@ -0,0 +1,107 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
background: #FFF8F7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
text-align: center;
|
||||
padding: 100rpx 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
color: #E96B6B;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 24rpx;
|
||||
color: #BBB;
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.match-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.match-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.match-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50rpx;
|
||||
background: #F0F0F0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.match-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.match-meta {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.btn-chat {
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 24rpx;
|
||||
border: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
margin-top: 32rpx;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
border: 1rpx solid #DDD;
|
||||
border-radius: 44rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// pages/activity-mutual-result/activity-mutual-result.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "互选结果"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||
|
||||
Page({
|
||||
data: { loading: true, results: [] as any[] },
|
||||
onLoad() { this.loadResults(); },
|
||||
onShow() { this.loadResults(); },
|
||||
|
||||
async loadResults() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: `${app.globalData.apiBase}/activities/mutual/my-results`,
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
this.setData({ results: res.data.data || [], loading: false });
|
||||
}
|
||||
} catch (e) { console.error(e); this.setData({ loading: false }); }
|
||||
},
|
||||
|
||||
openChat(e: any) {
|
||||
const partnerId = e.currentTarget.dataset.partnerid;
|
||||
wx.navigateTo({ url: `/pages/chat/chat?partnerId=${partnerId}` });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
<view class="container">
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="loading">加载中...</view>
|
||||
</block>
|
||||
<block wx:elif="{{results.length === 0}}">
|
||||
<view class="empty">
|
||||
<text class="empty-icon">🎯</text>
|
||||
<text class="empty-text">暂无互选结果</text>
|
||||
<text class="empty-hint">参加活动时可以向感兴趣的人表达意向</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="result-card" wx:for="{{results}}" wx:key="partnerId" wx:for-item="item">
|
||||
<view class="card-header">
|
||||
<image class="avatar" src="{{item.partnerAvatar || '/images/default-avatar.png'}}" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<text class="name">{{item.partnerName || '新朋友'}}</text>
|
||||
<text class="status-badge {{item.bothConfirmed ? 'badge-confirmed' : 'badge-waiting'}}">
|
||||
{{item.bothConfirmed ? '双方已确认' : '等待对方确认'}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="activity-name">来自活动:{{item.activityTitle}}</text>
|
||||
</view>
|
||||
<view class="card-actions" wx:if="{{!item.bothConfirmed}}">
|
||||
<button class="btn-waiting" disabled>等待对方确认</button>
|
||||
</view>
|
||||
<view class="card-actions" wx:if="{{item.bothConfirmed}}">
|
||||
<button class="btn-chat" bindtap="openChat" data-partnerid="{{item.partnerId}}">开启聊天</button>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,19 @@
|
||||
.container { padding: 24rpx; min-height: 100vh; background: #f5f5f5; }
|
||||
.loading { text-align: center; padding: 80rpx; color: #999; }
|
||||
.empty { text-align: center; padding: 80rpx 0; }
|
||||
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
|
||||
.empty-text { font-size: 32rpx; color: #666; display: block; }
|
||||
.empty-hint { font-size: 26rpx; color: #999; display: block; margin-top: 12rpx; }
|
||||
.result-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); }
|
||||
.card-header { display: flex; align-items: center; margin-bottom: 16rpx; }
|
||||
.avatar { width: 80rpx; height: 80rpx; border-radius: 50rpx; background: #eee; }
|
||||
.info { flex: 1; margin-left: 20rpx; }
|
||||
.name { font-size: 30rpx; font-weight: bold; color: #333; display: block; }
|
||||
.status-badge { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 12rpx; margin-top: 8rpx; display: inline-block; }
|
||||
.badge-confirmed { background: #e8f5e9; color: #2e7d32; }
|
||||
.badge-waiting { background: #fff3e0; color: #e65100; }
|
||||
.card-body { margin-bottom: 16rpx; }
|
||||
.activity-name { font-size: 24rpx; color: #999; }
|
||||
.card-actions { display: flex; justify-content: flex-end; }
|
||||
.btn-waiting { background: #f5f5f5; color: #999; font-size: 26rpx; padding: 12rpx 32rpx; border-radius: 24rpx; border: none; }
|
||||
.btn-chat { background: linear-gradient(135deg, #FF6B6B, #EE5A24); color: #fff; font-size: 26rpx; padding: 12rpx 32rpx; border-radius: 24rpx; border: none; }
|
||||
@@ -0,0 +1,66 @@
|
||||
// pages/activity-mutual-waiting/activity-mutual-waiting.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "等待确认"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activityId: '', partnerId: '',
|
||||
partnerName: '', partnerAvatar: '',
|
||||
myConfirmed: false, bothConfirmed: false, partnerDeclined: false
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
this.setData({ activityId: options.activityId, partnerId: options.partnerId });
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
async loadStatus() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/status/${this.data.partnerId}`,
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
const d = res.data.data;
|
||||
this.setData({
|
||||
myConfirmed: d.myStatus === 'accepted',
|
||||
bothConfirmed: d.bothConfirmed,
|
||||
partnerDeclined: d.partnerStatus === 'declined'
|
||||
});
|
||||
}
|
||||
// 加载对方信息
|
||||
this.loadPartnerInfo();
|
||||
} catch (e) { console.error(e); }
|
||||
},
|
||||
|
||||
async loadPartnerInfo() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: `${app.globalData.apiBase}/profiles/${this.data.partnerId}`,
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
this.setData({ partnerName: res.data.data?.displayName || '新朋友',
|
||||
partnerAvatar: res.data.data?.avatarFileId ? `${app.globalData.uploadBase}/files/${res.data.data.avatarFileId}` : '' });
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
|
||||
async onConfirm() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/confirm`,
|
||||
method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||||
data: { partnerId: this.data.partnerId }
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
this.setData({ myConfirmed: true, bothConfirmed: res.data.data.bothConfirmed });
|
||||
if (res.data.data.bothConfirmed) {
|
||||
wx.showToast({ title: '双方已确认!', icon: 'success' });
|
||||
setTimeout(() => this.openChat(), 1500);
|
||||
}
|
||||
}
|
||||
} catch (e) { wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
},
|
||||
|
||||
async onDecline() {
|
||||
try {
|
||||
await wx.request({
|
||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/decline`,
|
||||
method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||||
data: { partnerId: this.data.partnerId }
|
||||
});
|
||||
this.setData({ myConfirmed: false, bothConfirmed: false });
|
||||
wx.navigateBack();
|
||||
} catch (e) { wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
},
|
||||
|
||||
openChat() {
|
||||
wx.navigateTo({ url: `/pages/chat/chat?partnerId=${this.data.partnerId}` });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<image class="avatar" src="{{partnerAvatar || '/images/default-avatar.png'}}" mode="aspectFill" />
|
||||
<text class="partner-name">{{partnerName || '新朋友'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="status-section" wx:if="{{bothConfirmed}}">
|
||||
<view class="success-icon">✅</view>
|
||||
<text class="success-title">双方已确认!</text>
|
||||
<text class="success-hint">你们可以开始聊天了</text>
|
||||
<button class="chat-btn" bindtap="openChat">开启聊天</button>
|
||||
</view>
|
||||
|
||||
<view class="status-section" wx:elif="{{myConfirmed}}">
|
||||
<view class="waiting-icon">⏳</view>
|
||||
<text class="waiting-title">已发送邀请</text>
|
||||
<text class="waiting-hint">对方确认后你们可以开始聊天</text>
|
||||
<button class="decline-btn" bindtap="onDecline">暂不联系</button>
|
||||
</view>
|
||||
|
||||
<view class="status-section" wx:else>
|
||||
<view class="pending-icon">💬</view>
|
||||
<text class="pending-title">向Ta表达兴趣</text>
|
||||
<text class="pending-hint">双方确认后才能开启线上沟通</text>
|
||||
<button class="confirm-btn" bindtap="onConfirm">确认开启沟通</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,10 @@
|
||||
.container { padding: 40rpx 24rpx; min-height: 100vh; background: #f5f5f5; display: flex; flex-direction: column; align-items: center; }
|
||||
.header { text-align: center; margin-bottom: 40rpx; }
|
||||
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: #eee; }
|
||||
.partner-name { font-size: 36rpx; font-weight: bold; color: #333; display: block; margin-top: 16rpx; }
|
||||
.status-section { background: #fff; border-radius: 20rpx; padding: 48rpx 40rpx; width: 100%; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
|
||||
.success-icon, .waiting-icon, .pending-icon { font-size: 72rpx; margin-bottom: 20rpx; }
|
||||
.success-title, .waiting-title, .pending-title { font-size: 34rpx; font-weight: bold; color: #333; display: block; margin-bottom: 12rpx; }
|
||||
.success-hint, .waiting-hint, .pending-hint { font-size: 26rpx; color: #888; display: block; margin-bottom: 32rpx; }
|
||||
.chat-btn, .confirm-btn { background: linear-gradient(135deg, #FF6B6B, #EE5A24); color: #fff; border: none; border-radius: 44rpx; font-size: 30rpx; padding: 16rpx 60rpx; }
|
||||
.decline-btn { background: #f5f5f5; color: #999; border: none; border-radius: 44rpx; font-size: 28rpx; padding: 16rpx 60rpx; margin-top: 20rpx; }
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 页面占位骨架
|
||||
*/
|
||||
Page({
|
||||
data: {},
|
||||
onLoad(options) {},
|
||||
onShow() {}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationBarTitleText":"activity-register","usingComponents":{}}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="page"><text>页面: activity-register</text></view>
|
||||
@@ -0,0 +1 @@
|
||||
.page{padding:32rpx;background:#FFF8F7;min-height:100vh;text-align:center;color:#999;padding-top:200rpx;}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 公益活动列表页 U31
|
||||
*
|
||||
* 重构要点(遵循 AGENTS.md 不变量):
|
||||
* - 数据:ActivityStore.fetchActivities / loadMoreActivities(游标分页)
|
||||
* - 列表项:ui-activity-card 组件,"公益免费"为主视觉标识(AGENTS 第 1 条:用户永久免费)
|
||||
* - 跳转:点击卡片 → activity-detail,不在列表页处理报名
|
||||
* - 单向意向保密:本页不接入互选意向提交(仅在 activity-matches 页由用户主动操作,AGENTS 第 6 条)
|
||||
* - 下拉刷新 + onReachBottom 加载更多
|
||||
*/
|
||||
const { activityStore, appStore, authStore } = require('../../stores');
|
||||
const { to } = require('../../constants');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activities: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
firstLoaded: false,
|
||||
/** @type {{title:string, description:string, illustration:string, actions:Array}} */
|
||||
emptyConfig: {
|
||||
title: '暂无进行中的活动',
|
||||
description: '近期暂无报名中的公益活动,请稍后再来',
|
||||
illustration: 'activity',
|
||||
actions: [
|
||||
{ text: '我的活动', event: 'my', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
this._unsub = activityStore.bindToPage(this, {
|
||||
activities: 'activities',
|
||||
activitiesHasMore: 'activitiesHasMore',
|
||||
});
|
||||
this.fetchActivities(true);
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
// 从活动详情返回后,若列表为空则刷新(避免详情页报名后列表不同步)
|
||||
if (this.data.firstLoaded && this.data.activities.length === 0) {
|
||||
this.fetchActivities(true);
|
||||
}
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
|
||||
/** 首次或下拉刷新拉取活动列表 */
|
||||
async fetchActivities(first = false) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
await activityStore.fetchActivities();
|
||||
this.setData({
|
||||
loading: false,
|
||||
firstLoaded: true,
|
||||
hasMore: activityStore.state.activitiesHasMore,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loading: false, firstLoaded: true });
|
||||
appStore.toastError(err, '加载活动失败');
|
||||
}
|
||||
},
|
||||
|
||||
/** 加载更多(onReachBottom 触发) */
|
||||
async loadMore() {
|
||||
if (this.data.loadingMore || !this.data.hasMore) return;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
await activityStore.loadMoreActivities();
|
||||
this.setData({
|
||||
loadingMore: false,
|
||||
hasMore: activityStore.state.activitiesHasMore,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loadingMore: false });
|
||||
appStore.toastError(err, '加载更多失败');
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchActivities(true).then(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore();
|
||||
},
|
||||
|
||||
/** 点击活动卡片 → 跳转详情 */
|
||||
onActivityTap(e) {
|
||||
const activityId = e.detail.activityId;
|
||||
if (!activityId) return;
|
||||
wx.navigateTo({ url: to.activityDetail(activityId) });
|
||||
},
|
||||
|
||||
/** 空状态:去我的活动 */
|
||||
onEmptyAction(e) {
|
||||
const { event } = e.detail;
|
||||
if (event === 'my') {
|
||||
wx.navigateTo({ url: '/pages/my-activities/my-activities' });
|
||||
}
|
||||
},
|
||||
|
||||
/** 顶部"我的活动"入口 */
|
||||
onMyActivities() {
|
||||
wx.navigateTo({ url: '/pages/my-activities/my-activities' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "公益活动",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ui-section-header": "/components/section-header/index",
|
||||
"ui-activity-card": "/components/activity-card/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-empty": "/components/empty/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<!-- 公益活动列表页 U31 -->
|
||||
<view class="act-page">
|
||||
<!-- 顶部区:标题 + 我的活动入口 -->
|
||||
<view class="act-page__topbar">
|
||||
<ui-section-header
|
||||
title="公益活动"
|
||||
subtitle="丹阳本地公益组织主办"
|
||||
action="我的活动"
|
||||
bind:action="onMyActivities"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 首次加载 -->
|
||||
<view wx:if="{{loading && !firstLoaded}}" class="act-loading">
|
||||
<ui-loading mode="skeleton" rows="3" />
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view wx:elif="{{activities.length}}" class="act-list">
|
||||
<ui-activity-card
|
||||
wx:for="{{activities}}"
|
||||
wx:key="activityId"
|
||||
activity="{{item}}"
|
||||
bind:tap="onActivityTap"
|
||||
/>
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loadingMore}}" class="act-list__more">
|
||||
<ui-loading mode="spinner" inline text="加载更多活动…" />
|
||||
</view>
|
||||
<view wx:elif="{{!hasMore}}" class="act-list__end">已到底部</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:else class="act-empty">
|
||||
<ui-empty
|
||||
title="{{emptyConfig.title}}"
|
||||
description="{{emptyConfig.description}}"
|
||||
illustration="{{emptyConfig.illustration}}"
|
||||
actions="{{emptyConfig.actions}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 公益活动列表页 U31
|
||||
*/
|
||||
|
||||
.act-page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
padding-bottom: var(--spacing-section);
|
||||
}
|
||||
|
||||
.act-page__topbar {
|
||||
padding: var(--spacing-3) var(--spacing-page-x) var(--spacing-1);
|
||||
}
|
||||
|
||||
/* ============ 加载骨架 ============ */
|
||||
.act-loading {
|
||||
padding: var(--spacing-3) var(--spacing-page-x);
|
||||
}
|
||||
|
||||
/* ============ 列表 ============ */
|
||||
.act-list {
|
||||
padding: var(--spacing-2) var(--spacing-page-x) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.act-list__more {
|
||||
padding: var(--spacing-3) 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.act-list__end {
|
||||
padding: var(--spacing-3) 0;
|
||||
text-align: center;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
/* ============ 空状态 ============ */
|
||||
.act-empty {
|
||||
padding-top: 160rpx;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 申诉提交页(重构:用 appealService.submitAppeal + ui-nav-bar + ui-textarea)
|
||||
*
|
||||
* 用户针对封禁/暂停处置提交申诉。类型由入口决定(ban_appeal / suspend_appeal)。
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 8 条:敏感数据默认拒绝——本页只接收申诉原因 + 证据截图 + 联系方式,
|
||||
* 不接受身份证号等敏感字段;联系方式为可选。
|
||||
* - 第 4 条:Block 优先级最高——申诉不会自动解封或解除拉黑,由运营人工处置。
|
||||
* 幂等:appealService.submitAppeal 内部以 appealType+relatedActionId+秒级时间生成 Idempotency-Key。
|
||||
*/
|
||||
const { appStore } = require('../../stores');
|
||||
const { appealService } = require('../../services');
|
||||
|
||||
const TYPE_LABELS = {
|
||||
ban_appeal: '封禁申诉',
|
||||
suspend_appeal: '暂停申诉',
|
||||
};
|
||||
|
||||
const MIN_REASON_LEN = 10;
|
||||
const MAX_EVIDENCE = 5;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
appealType: '',
|
||||
actionId: '',
|
||||
typeLabel: '申诉',
|
||||
reason: '',
|
||||
reasonError: '',
|
||||
evidenceFiles: [],
|
||||
contactMethod: '',
|
||||
submitting: false,
|
||||
submitted: false,
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
try {
|
||||
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight || 20,
|
||||
navBarHeight: info.platform === 'android' ? 48 : 44,
|
||||
});
|
||||
} catch (e) { /* 使用默认值 */ }
|
||||
|
||||
const appealType = options.type ? decodeURIComponent(options.type) : '';
|
||||
const actionId = options.actionId ? decodeURIComponent(options.actionId) : '';
|
||||
this.setData({
|
||||
appealType,
|
||||
actionId,
|
||||
typeLabel: TYPE_LABELS[appealType] || '申诉',
|
||||
});
|
||||
},
|
||||
|
||||
onReasonInput(e) {
|
||||
this.setData({ reason: e.detail.value, reasonError: '' });
|
||||
},
|
||||
|
||||
onContactInput(e) {
|
||||
this.setData({ contactMethod: e.detail.value });
|
||||
},
|
||||
|
||||
// ============ 证据上传 ============
|
||||
|
||||
chooseEvidence() {
|
||||
const remain = MAX_EVIDENCE - this.data.evidenceFiles.length;
|
||||
if (remain <= 0) {
|
||||
appStore.toast('最多上传 5 张证据');
|
||||
return;
|
||||
}
|
||||
wx.chooseMedia({
|
||||
count: remain,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const files = res.tempFiles.map((f) => f.tempFilePath);
|
||||
this.setData({ evidenceFiles: [...this.data.evidenceFiles, ...files] });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
previewImage(e) {
|
||||
wx.previewImage({
|
||||
current: e.currentTarget.dataset.url,
|
||||
urls: this.data.evidenceFiles,
|
||||
});
|
||||
},
|
||||
|
||||
removeImage(e) {
|
||||
const idx = e.currentTarget.dataset.index;
|
||||
const files = this.data.evidenceFiles;
|
||||
files.splice(idx, 1);
|
||||
this.setData({ evidenceFiles: files });
|
||||
},
|
||||
|
||||
// ============ 提交 ============
|
||||
|
||||
async submitAppeal() {
|
||||
const { appealType, actionId, reason, contactMethod } = this.data;
|
||||
const trimmedReason = (reason || '').trim();
|
||||
|
||||
if (!appealType) {
|
||||
appStore.toast('缺少申诉类型');
|
||||
return;
|
||||
}
|
||||
if (!trimmedReason) {
|
||||
this.setData({ reasonError: '请填写申诉原因' });
|
||||
return;
|
||||
}
|
||||
if (trimmedReason.length < MIN_REASON_LEN) {
|
||||
this.setData({ reasonError: `申诉原因请至少 ${MIN_REASON_LEN} 个字` });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ submitting: true });
|
||||
try {
|
||||
await appealService.submitAppeal({
|
||||
appealType,
|
||||
relatedActionId: actionId || undefined,
|
||||
reason: trimmedReason,
|
||||
// 证据上传至 OSS 后回填 fileId;当前阶段先以本地路径占位,由后端证据服务接入后补全
|
||||
evidenceFileIds: [],
|
||||
contactMethod: (contactMethod || '').trim() || undefined,
|
||||
});
|
||||
this.setData({ submitted: true, submitting: false });
|
||||
appStore.toastSuccess('申诉已提交');
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "提交申诉",
|
||||
"backgroundColor": "#FFF8F7",
|
||||
"backgroundTextStyle": "dark",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-section-header": "/components/section-header/index",
|
||||
"ui-textarea": "/components/textarea/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!-- 申诉提交页(重构:appealService + ui-nav-bar + ui-textarea) -->
|
||||
<view class="page">
|
||||
<ui-nav-bar
|
||||
title="{{typeLabel}}"
|
||||
show-back="{{true}}"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
/>
|
||||
|
||||
<!-- 提交成功态 -->
|
||||
<view wx:if="{{submitted}}" class="body" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="success-wrap">
|
||||
<view class="success-icon">✓</view>
|
||||
<text class="success-title">申诉已提交</text>
|
||||
<text class="success-hint">工作人员将在 1-3 个工作日内审核您的申诉结果,请耐心等待。</text>
|
||||
<button class="btn-primary" bindtap="goBack">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单态 -->
|
||||
<view wx:else class="body" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="intro-card">
|
||||
<text class="intro-title">{{typeLabel}}</text>
|
||||
<text class="intro-subtitle">请如实描述您的情况,工作人员将认真审核</text>
|
||||
</view>
|
||||
|
||||
<!-- 申诉原因 -->
|
||||
<view class="card">
|
||||
<ui-section-header title="申诉原因" subtitle="必填,至少10字" />
|
||||
<ui-textarea
|
||||
value="{{reason}}"
|
||||
placeholder="请详细说明您认为处置不当的原因,以及您希望恢复账号的理由..."
|
||||
maxlength="500"
|
||||
error="{{reasonError}}"
|
||||
bind:input="onReasonInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 补充证据 -->
|
||||
<view class="card">
|
||||
<ui-section-header title="补充证据" subtitle="选填,最多5张" />
|
||||
<view class="evidence-list">
|
||||
<view
|
||||
class="evidence-item"
|
||||
wx:for="{{evidenceFiles}}"
|
||||
wx:key="index"
|
||||
data-url="{{item}}"
|
||||
bindtap="previewImage"
|
||||
>
|
||||
<image src="{{item}}" mode="aspectFill" />
|
||||
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">✕</view>
|
||||
</view>
|
||||
<view class="evidence-add" bindtap="chooseEvidence" wx:if="{{evidenceFiles.length < 5}}">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加证据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<view class="card">
|
||||
<ui-section-header title="联系方式" subtitle="选填" />
|
||||
<input
|
||||
class="contact-input"
|
||||
placeholder="请输入手机号或邮箱,便于我们联系您"
|
||||
value="{{contactMethod}}"
|
||||
bindinput="onContactInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<button class="btn-submit" bindtap="submitAppeal" loading="{{submitting}}" disabled="{{!reason}}">
|
||||
提交申诉
|
||||
</button>
|
||||
<text class="hint">提交后工作人员将在 1-3 个工作日内审核,请勿重复提交</text>
|
||||
|
||||
<!-- 公益免费标识 -->
|
||||
<view class="free-badge">
|
||||
<text>本平台为公益服务,申诉受理永久免费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,209 @@
|
||||
/* 申诉提交页 — Design Tokens */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #FFF8F7;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 介绍卡片 */
|
||||
.intro-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
border-left: 8rpx solid #E96B6B;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.intro-subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 证据 */
|
||||
.evidence-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.evidence-item {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.evidence-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.evidence-add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #F7F8FA;
|
||||
border: 2rpx dashed #D0D0D0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 48rpx;
|
||||
color: #B0B0B0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 22rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 联系方式输入 */
|
||||
.contact-input {
|
||||
margin-top: 16rpx;
|
||||
background: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
min-height: 40rpx;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.btn-submit::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-submit[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 80%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #B0B0B0;
|
||||
margin-top: 20rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
/* 成功态 */
|
||||
.success-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 48rpx;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
line-height: 120rpx;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: #E8F5E9;
|
||||
color: #1A6B3E;
|
||||
font-size: 64rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.success-hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 公益免费标识 */
|
||||
.free-badge {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.free-badge text {
|
||||
display: inline-block;
|
||||
font-size: 22rpx;
|
||||
color: #1A6B3E;
|
||||
background: #E8F5E9;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Page({ data: {}, onLoad() {}, onShow() {} })
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationBarTitleText":"bind-phone","usingComponents":{}}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="page"><text>bind-phone</text></view>
|
||||
@@ -0,0 +1 @@
|
||||
.page{padding:32rpx;background:#FFF8F7;min-height:100vh;text-align:center;padding-top:200rpx;color:#999;}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 黑名单管理页(重构:用 matchStore + ui-nav-bar + DangerActionDialog)
|
||||
*
|
||||
* 列表展示已拉黑用户(脱敏字段),支持解除拉黑。
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 4 条:Block 优先级最高——解除拉黑后对方可重新与你互动;
|
||||
* 解除需 DangerActionDialog 二次确认。
|
||||
* - 第 8 条:敏感数据默认拒绝——列表仅展示后端返回的脱敏字段(昵称/拉黑时间/原因),
|
||||
* 不展示手机号/身份证等敏感信息。
|
||||
* - 第 9 条:PostgreSQL 是 Source of Truth——列表状态以 matchStore 同步服务端返回为准。
|
||||
*/
|
||||
const { matchStore, appStore } = require('../../stores');
|
||||
const { DANGER_ACTION } = require('../../constants');
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
blockedList: [],
|
||||
blockedHasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
actionLoadingUserId: '',
|
||||
|
||||
// DangerActionDialog
|
||||
showDangerDialog: false,
|
||||
dangerAction: DANGER_ACTION.UNBLOCK,
|
||||
dangerTargetName: '',
|
||||
pendingUnblockUserId: '',
|
||||
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try {
|
||||
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight || 20,
|
||||
navBarHeight: info.platform === 'android' ? 48 : 44,
|
||||
});
|
||||
} catch (e) { /* 使用默认值 */ }
|
||||
|
||||
this._unsub = matchStore.bindToPage(this, {
|
||||
blockedList: 'blockedList',
|
||||
blockedHasMore: 'blockedHasMore',
|
||||
});
|
||||
|
||||
this.fetchList(true);
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchList(true);
|
||||
},
|
||||
|
||||
async fetchList(first) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
await matchStore.fetchBlockedList({ pageSize: PAGE_SIZE });
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchList(true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.loadingMore || !this.data.blockedHasMore) return;
|
||||
this.setData({ loadingMore: true });
|
||||
matchStore.loadMoreBlockedList()
|
||||
.catch((err) => appStore.toastError(err))
|
||||
.finally(() => this.setData({ loadingMore: false }));
|
||||
},
|
||||
|
||||
// ============ 解除拉黑 ============
|
||||
|
||||
onUnblock(e) {
|
||||
const userId = e.currentTarget.dataset.userId;
|
||||
const userName = e.currentTarget.dataset.userName || '对方';
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({
|
||||
showDangerDialog: true,
|
||||
dangerTargetName: userName,
|
||||
pendingUnblockUserId: userId,
|
||||
});
|
||||
},
|
||||
|
||||
async onDangerConfirm() {
|
||||
const userId = this.data.pendingUnblockUserId;
|
||||
this.setData({ showDangerDialog: false, pendingUnblockUserId: '' });
|
||||
if (!userId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
await matchStore.unblock(userId);
|
||||
// 从本地列表移除(Store 的 unblock 不自动移除,需页面处理)
|
||||
matchStore.setState({
|
||||
blockedList: matchStore.state.blockedList.filter((u) => u.userId !== userId),
|
||||
});
|
||||
appStore.toastSuccess('已解除拉黑');
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
onDangerCancel() {
|
||||
this.setData({ showDangerDialog: false, pendingUnblockUserId: '' });
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "黑名单管理",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#FFF8F7",
|
||||
"backgroundTextStyle": "dark",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-empty": "/components/empty/index",
|
||||
"ui-avatar": "/components/avatar/index",
|
||||
"ui-danger-action-dialog": "/components/danger-action-dialog/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- 黑名单管理页(重构:matchStore + ui-nav-bar + DangerActionDialog) -->
|
||||
<view class="page">
|
||||
<ui-nav-bar
|
||||
title="黑名单管理"
|
||||
show-back="{{true}}"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
/>
|
||||
|
||||
<view class="body" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<ui-loading wx:if="{{loading}}" mode="skeleton" rows="4" />
|
||||
|
||||
<block wx:elif="{{blockedList.length > 0}}">
|
||||
<view class="list">
|
||||
<view class="item" wx:for="{{blockedList}}" wx:key="userId">
|
||||
<ui-avatar
|
||||
src="{{item.avatarUrl}}"
|
||||
size="lg"
|
||||
default-text="{{item.userName}}"
|
||||
/>
|
||||
<view class="info">
|
||||
<text class="name">{{item.userName || '用户'}}</text>
|
||||
<text class="meta">拉黑于 {{item.blockedAtText || '未知时间'}}</text>
|
||||
<text class="reason" wx:if="{{item.reason}}">原因:{{item.reason}}</text>
|
||||
</view>
|
||||
<button
|
||||
class="btn-unblock {{actionLoadingUserId === item.userId ? 'disabled' : ''}}"
|
||||
catchtap="onUnblock"
|
||||
data-user-id="{{item.userId}}"
|
||||
data-user-name="{{item.userName}}"
|
||||
disabled="{{actionLoadingUserId === item.userId}}"
|
||||
>解除拉黑</button>
|
||||
</view>
|
||||
</view>
|
||||
<ui-loading wx:if="{{loadingMore}}" mode="skeleton" rows="2" />
|
||||
</block>
|
||||
|
||||
<ui-empty
|
||||
wx:else
|
||||
illustration="default"
|
||||
title="黑名单是空的"
|
||||
description="你还没有拉黑过任何人。拉黑后对方将无法与你互动。"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 解除拉黑二次确认 -->
|
||||
<ui-danger-action-dialog
|
||||
visible="{{showDangerDialog}}"
|
||||
action="{{dangerAction}}"
|
||||
target-name="{{dangerTargetName}}"
|
||||
bind:confirm="onDangerConfirm"
|
||||
bind:cancel="onDangerCancel"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,72 @@
|
||||
/* 黑名单管理页 — Design Tokens */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #FFF8F7;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.reason {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #B0B0B0;
|
||||
}
|
||||
|
||||
.btn-unblock {
|
||||
flex-shrink: 0;
|
||||
height: 64rpx;
|
||||
line-height: 60rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
background: #fff;
|
||||
color: #E96B6B;
|
||||
border: 2rpx solid #E96B6B;
|
||||
}
|
||||
|
||||
.btn-unblock::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-unblock.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 聊天详情页 U24(重构:用 messageStore + message-bubble)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 3 条:Mutual Match 后才能聊天——sendMessage 若后端返回 MATCH_REQUIRED,
|
||||
* UI 给出引导文案;不在客户端做硬阻断,由后端守门。
|
||||
* - 第 4 条:Block/Unmatch 后会话 canSendMessage=false,输入栏禁用并显示"已停用"。
|
||||
* - 第 6 条:单向活动意向不泄露——本页只展示已建立的 Mutual 会话消息。
|
||||
* - 第 7 条:普通工作人员不能查看私人聊天——本页仅服务用户端。
|
||||
* - 第 9 条:PostgreSQL 是 Source of Truth——本地仅缓存最近活跃会话消息,冷启动从服务端拉取。
|
||||
*/
|
||||
const { messageStore, appStore, blockUser } = require('../../stores');
|
||||
const { messageService } = require('../../services');
|
||||
const { to, ROUTE, DANGER_ACTION, BUSINESS_ERROR_CODE } = require('../../constants');
|
||||
|
||||
/** 超过该间隔则显示时间分隔(5 分钟) */
|
||||
const TIME_GAP_MS = 5 * 60 * 1000;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
conversationId: '',
|
||||
otherDisplayName: '',
|
||||
otherUserId: '',
|
||||
otherAvatarUrl: '',
|
||||
canSend: true,
|
||||
messages: [],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
sending: false,
|
||||
inputText: '',
|
||||
scrollIntoView: '',
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
showDangerDialog: false,
|
||||
dangerAction: DANGER_ACTION.BLOCK,
|
||||
connectionState: 'disconnected',
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const conversationId = options.id || '';
|
||||
if (!conversationId) {
|
||||
appStore.toast('会话不存在');
|
||||
wx.navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
// 注入状态栏高度供 ui-nav-bar 使用
|
||||
try {
|
||||
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight || 20,
|
||||
navBarHeight: info.platform === 'android' ? 48 : 44,
|
||||
});
|
||||
} catch (e) { /* 使用默认值 */ }
|
||||
|
||||
this.setData({ conversationId });
|
||||
|
||||
// 绑定 messageStore.connectionState 到页面
|
||||
this._unsubConn = messageStore.bindToPage(this, {
|
||||
connectionState: 'connectionState',
|
||||
});
|
||||
// 订阅消息列表 / 分页 / loading 变化
|
||||
this._unsubList = messageStore.subscribe((state) => this._syncFromStore(state));
|
||||
// 注册实时消息监听:收到对方消息立即标记已读(会话已处于前台)
|
||||
this._unsubRealtime = messageStore.onMessage(conversationId, (msg) => {
|
||||
if (msg.bubbleType === 'other') {
|
||||
messageStore.markConversationRead(conversationId).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// 打开会话:拉取最近消息 + 清未读(AGENTS 第 9 条,DB 为准)
|
||||
messageStore.openConversation(conversationId).catch((err) => appStore.toastError(err));
|
||||
// 拉取会话元数据(displayName / canSend / targetUserId / 头像)
|
||||
this.loadConversationMeta();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this._unsubConn && this._unsubConn();
|
||||
this._unsubList && this._unsubList();
|
||||
this._unsubRealtime && this._unsubRealtime();
|
||||
if (this.data.conversationId) {
|
||||
// 关闭会话:activeConversationId 置空,后续收到对方消息将累计未读
|
||||
messageStore.closeConversation();
|
||||
}
|
||||
},
|
||||
|
||||
/** 拉取会话元数据(canSend 以服务端为准) */
|
||||
async loadConversationMeta() {
|
||||
try {
|
||||
const conv = await messageService.getConversation(this.data.conversationId);
|
||||
if (conv) {
|
||||
this.setData({
|
||||
otherDisplayName: conv.targetDisplayName || conv.otherDisplayName || '用户',
|
||||
otherUserId: conv.targetUserId || '',
|
||||
otherAvatarUrl: conv.targetAvatarUrl || '',
|
||||
canSend: conv.canSendMessage !== false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// 兜底:从 store 会话列表查找
|
||||
const local = messageStore.state.conversations.find(
|
||||
(c) => c.conversationId === this.data.conversationId,
|
||||
);
|
||||
if (local) {
|
||||
this.setData({
|
||||
otherDisplayName: local.targetDisplayName || '用户',
|
||||
otherUserId: local.targetUserId || '',
|
||||
otherAvatarUrl: local.targetAvatarUrl || '',
|
||||
canSend: local.canSendMessage !== false,
|
||||
});
|
||||
} else {
|
||||
appStore.toastError(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 把 store 状态同步到页面 */
|
||||
_syncFromStore(state) {
|
||||
const cid = this.data.conversationId;
|
||||
const rawList = state.messagesByConversation[cid] || [];
|
||||
const hasMore = state.hasMoreByConversation[cid] !== false;
|
||||
const loading = state.conversationsLoading;
|
||||
const messages = this._formatMessageList(rawList);
|
||||
const patch = { messages, hasMore };
|
||||
if (loading !== this.data.loading) patch.loading = loading;
|
||||
this.setData(patch);
|
||||
|
||||
// 加载更早消息时,保持滚动位置(定位到加载前的第一条)
|
||||
if (this._loadingMore && this._preserveMsgId) {
|
||||
const idx = messages.findIndex((m) => m.messageId === this._preserveMsgId);
|
||||
if (idx >= 0) this.setData({ scrollIntoView: `msg-${idx}` });
|
||||
this._loadingMore = false;
|
||||
this._preserveMsgId = null;
|
||||
} else {
|
||||
this._scrollToLast(messages);
|
||||
}
|
||||
},
|
||||
|
||||
/** 派生时间分隔 / 头像 / 是否显示头像 */
|
||||
_formatMessageList(list) {
|
||||
const otherAvatar = this.data.otherAvatarUrl;
|
||||
const result = [];
|
||||
let lastTs = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const m = list[i];
|
||||
const ts = m.sentAt || 0;
|
||||
const showTime = i === 0 || (ts && ts - lastTs > TIME_GAP_MS);
|
||||
result.push({
|
||||
...m,
|
||||
showTime,
|
||||
sentAtText: showTime ? this._formatTime(ts) : '',
|
||||
showAvatar: m.bubbleType === 'other',
|
||||
avatarUrl: m.bubbleType === 'other' ? otherAvatar : '',
|
||||
});
|
||||
lastTs = ts;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_formatTime(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts);
|
||||
const now = new Date();
|
||||
const hm = d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
if (d.toDateString() === now.toDateString()) return hm;
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${hm}`;
|
||||
},
|
||||
|
||||
_scrollToLast(list) {
|
||||
if (!list.length) return;
|
||||
const id = `msg-${list.length - 1}`;
|
||||
if (id !== this.data.scrollIntoView) {
|
||||
this.setData({ scrollIntoView: id });
|
||||
}
|
||||
},
|
||||
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack();
|
||||
} else {
|
||||
wx.switchTab({ url: ROUTE.TAB.MESSAGES });
|
||||
}
|
||||
},
|
||||
|
||||
onInput(e) {
|
||||
this.setData({ inputText: e.detail.value });
|
||||
},
|
||||
|
||||
async onSend() {
|
||||
const { inputText, conversationId, sending, canSend } = this.data;
|
||||
if (!canSend || !inputText.trim() || sending) return;
|
||||
this.setData({ sending: true, inputText: '' });
|
||||
try {
|
||||
await messageStore.sendMessage(conversationId, inputText.trim());
|
||||
} catch (err) {
|
||||
// MATCH_REQUIRED:双方互相喜欢后才能聊天(AGENTS 第 3 条,后端守门)
|
||||
if (err && err.code === BUSINESS_ERROR_CODE.MATCH_REQUIRED) {
|
||||
appStore.toast('双方互相喜欢后才能开始聊天');
|
||||
} else {
|
||||
appStore.toastError(err, '发送失败');
|
||||
}
|
||||
} finally {
|
||||
this.setData({ sending: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 重发失败消息 */
|
||||
onRetrySend(e) {
|
||||
const messageId = e.currentTarget.dataset.msgId;
|
||||
if (!messageId) return;
|
||||
messageStore
|
||||
.retrySend(this.data.conversationId, messageId)
|
||||
.catch((err) => appStore.toastError(err, '重发失败'));
|
||||
},
|
||||
|
||||
/** 滚动到顶部:加载更早消息(保持滚动位置) */
|
||||
async onScrollToUpper() {
|
||||
const { conversationId, hasMore, loadingMore } = this.data;
|
||||
if (!hasMore || loadingMore) return;
|
||||
const first = this.data.messages[0];
|
||||
this._loadingMore = true;
|
||||
this._preserveMsgId = first ? first.messageId : null;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
await messageStore.loadMoreMessages(conversationId);
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
this._loadingMore = false;
|
||||
this._preserveMsgId = null;
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 右上角更多:查看资料 / 举报 / 拉黑 */
|
||||
onMore() {
|
||||
const { otherUserId } = this.data;
|
||||
if (!otherUserId) {
|
||||
appStore.toast('暂无更多信息');
|
||||
return;
|
||||
}
|
||||
wx.showActionSheet({
|
||||
itemList: ['查看 Ta 的资料', '举报 Ta', '拉黑 Ta'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.navigateTo({ url: to.detail(otherUserId) });
|
||||
} else if (res.tapIndex === 1) {
|
||||
wx.navigateTo({ url: to.report(otherUserId) });
|
||||
} else if (res.tapIndex === 2) {
|
||||
this.setData({ showDangerDialog: true, dangerAction: DANGER_ACTION.BLOCK });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 危险动作确认(拉黑) */
|
||||
async onConfirmDanger(e) {
|
||||
const action = e.detail.action;
|
||||
const { otherUserId, conversationId } = this.data;
|
||||
this.setData({ showDangerDialog: false });
|
||||
if (action !== DANGER_ACTION.BLOCK) return;
|
||||
try {
|
||||
appStore.showLoading('处理中…');
|
||||
// 拉黑:触发 MatchStore + MessageStore 同步副作用(AGENTS 第 4 条即时停用)
|
||||
await blockUser(otherUserId, '用户主动拉黑');
|
||||
appStore.hideLoading();
|
||||
this.setData({ canSend: false });
|
||||
appStore.toastSuccess('已拉黑');
|
||||
} catch (err) {
|
||||
appStore.hideLoading();
|
||||
appStore.toastError(err);
|
||||
}
|
||||
},
|
||||
|
||||
onCancelDanger() {
|
||||
this.setData({ showDangerDialog: false });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "聊天",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-message-bubble": "/components/message-bubble/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-button": "/components/button/index",
|
||||
"ui-danger-action-dialog": "/components/danger-action-dialog/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 聊天详情页 U24(TypeScript)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 3 条:Mutual Match 后才能聊天
|
||||
* - 第 4 条:Block/Unmatch 后会话 canSendMessage=false
|
||||
* - 第 6 条:单向活动意向不泄露
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { messageStore, appStore, blockUser } from '../../stores';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { messageService } from '../../services';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { to, ROUTE, DANGER_ACTION, BUSINESS_ERROR_CODE } from '../../constants';
|
||||
import type { Message } from '../../types';
|
||||
|
||||
/** 超过该间隔则显示时间分隔(5 分钟) */
|
||||
const TIME_GAP_MS = 5 * 60 * 1000;
|
||||
|
||||
interface ChatData {
|
||||
conversationId: string;
|
||||
otherDisplayName: string;
|
||||
otherUserId: string;
|
||||
otherAvatarUrl: string;
|
||||
canSend: boolean;
|
||||
messages: Array<Message & { showTime: boolean; sentAtText: string; showAvatar: boolean; avatarUrl: string }>;
|
||||
hasMore: boolean;
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
sending: boolean;
|
||||
inputText: string;
|
||||
scrollIntoView: string;
|
||||
statusBarHeight: number;
|
||||
navBarHeight: number;
|
||||
showDangerDialog: boolean;
|
||||
dangerAction: string;
|
||||
connectionState: string;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
conversationId: '',
|
||||
otherDisplayName: '',
|
||||
otherUserId: '',
|
||||
otherAvatarUrl: '',
|
||||
canSend: true,
|
||||
messages: [],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
sending: false,
|
||||
inputText: '',
|
||||
scrollIntoView: '',
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
showDangerDialog: false,
|
||||
dangerAction: DANGER_ACTION.BLOCK,
|
||||
connectionState: 'disconnected',
|
||||
},
|
||||
|
||||
onLoad(options: Record<string, string | undefined>) {
|
||||
const conversationId = options.id || '';
|
||||
if (!conversationId) {
|
||||
appStore.toast('会话不存在');
|
||||
wx.navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: (info as any).statusBarHeight || 20,
|
||||
navBarHeight: (info as any).platform === 'android' ? 48 : 44,
|
||||
});
|
||||
} catch { /* 默认值 */ }
|
||||
|
||||
this.setData({ conversationId });
|
||||
|
||||
this._unsubConn = messageStore.bindToPage(this, { connectionState: 'connectionState' });
|
||||
this._unsubList = messageStore.subscribe((state: any) => this._syncFromStore(state));
|
||||
this._unsubRealtime = messageStore.onMessage(conversationId, (msg: Message) => {
|
||||
if (msg.bubbleType === 'other') {
|
||||
messageStore.markConversationRead(conversationId).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
messageStore.openConversation(conversationId).catch((err: unknown) => {
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
});
|
||||
this.loadConversationMeta();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this._unsubConn && this._unsubConn();
|
||||
this._unsubList && this._unsubList();
|
||||
this._unsubRealtime && this._unsubRealtime();
|
||||
if (this.data.conversationId) {
|
||||
messageStore.closeConversation();
|
||||
}
|
||||
},
|
||||
|
||||
async loadConversationMeta() {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const conv = await messageService.getConversation(this.data.conversationId);
|
||||
if (conv) {
|
||||
this.setData({
|
||||
otherDisplayName: (conv as any).targetDisplayName || (conv as any).otherDisplayName || '用户',
|
||||
otherUserId: (conv as any).targetUserId || '',
|
||||
otherAvatarUrl: (conv as any).targetAvatarUrl || '',
|
||||
canSend: (conv as any).canSendMessage !== false,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const local = (messageStore.state as any).conversations?.find(
|
||||
(c: any) => c.conversationId === this.data.conversationId,
|
||||
);
|
||||
if (local) {
|
||||
this.setData({
|
||||
otherDisplayName: local.targetDisplayName || '用户',
|
||||
otherUserId: local.targetUserId || '',
|
||||
otherAvatarUrl: local.targetAvatarUrl || '',
|
||||
canSend: local.canSendMessage !== false,
|
||||
});
|
||||
} else {
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_syncFromStore(state: any) {
|
||||
const cid = this.data.conversationId;
|
||||
const rawList: Message[] = state.messagesByConversation?.[cid] || [];
|
||||
const hasMore = state.hasMoreByConversation?.[cid] !== false;
|
||||
const loading = state.conversationsLoading;
|
||||
const messages = this._formatMessageList(rawList);
|
||||
const patch: Partial<ChatData> = { messages, hasMore };
|
||||
if (loading !== this.data.loading) patch.loading = loading;
|
||||
this.setData(patch);
|
||||
|
||||
if (this._loadingMore && this._preserveMsgId) {
|
||||
const idx = messages.findIndex((m) => m.messageId === this._preserveMsgId);
|
||||
if (idx >= 0) this.setData({ scrollIntoView: `msg-${idx}` });
|
||||
this._loadingMore = false;
|
||||
this._preserveMsgId = null;
|
||||
} else {
|
||||
this._scrollToLast(messages);
|
||||
}
|
||||
},
|
||||
|
||||
_formatMessageList(list: Message[]) {
|
||||
const otherAvatar = this.data.otherAvatarUrl;
|
||||
const result: Array<Message & { showTime: boolean; sentAtText: string; showAvatar: boolean; avatarUrl: string }> = [];
|
||||
let lastTs = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const m = list[i];
|
||||
const ts = (m as any).sentAt || 0;
|
||||
const showTime = i === 0 || (ts && ts - lastTs > TIME_GAP_MS);
|
||||
result.push({
|
||||
...m,
|
||||
showTime,
|
||||
sentAtText: showTime ? this._formatTime(ts) : '',
|
||||
showAvatar: m.bubbleType === 'other',
|
||||
avatarUrl: m.bubbleType === 'other' ? otherAvatar : '',
|
||||
});
|
||||
lastTs = ts;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_formatTime(ts: number | string): string {
|
||||
if (!ts) return '';
|
||||
const d = new Date(Number(ts));
|
||||
const now = new Date();
|
||||
const hm = d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
if (d.toDateString() === now.toDateString()) return hm;
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${hm}`;
|
||||
},
|
||||
|
||||
_scrollToLast(list: typeof this.data.messages) {
|
||||
if (!list.length) return;
|
||||
const id = `msg-${list.length - 1}`;
|
||||
if (id !== this.data.scrollIntoView) this.setData({ scrollIntoView: id });
|
||||
},
|
||||
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) wx.navigateBack();
|
||||
else wx.switchTab({ url: ROUTE.TAB.MESSAGES });
|
||||
},
|
||||
|
||||
onInput(e: any) {
|
||||
this.setData({ inputText: (e as any).detail.value });
|
||||
},
|
||||
|
||||
async onSend() {
|
||||
const { inputText, conversationId, sending, canSend } = this.data;
|
||||
if (!canSend || !inputText.trim() || sending) return;
|
||||
this.setData({ sending: true, inputText: '' });
|
||||
try {
|
||||
// @ts-ignore
|
||||
await messageStore.sendMessage(conversationId, inputText.trim());
|
||||
} catch (err: unknown) {
|
||||
const code = (err as any)?.code;
|
||||
if (code === BUSINESS_ERROR_CODE.MATCH_REQUIRED) {
|
||||
appStore.toast('双方互相喜欢后才能开始聊天');
|
||||
} else {
|
||||
appStore.toastError(err instanceof Error ? err.message : '发送失败');
|
||||
}
|
||||
} finally {
|
||||
this.setData({ sending: false });
|
||||
}
|
||||
},
|
||||
|
||||
onRetrySend(e: any) {
|
||||
const messageId = (e.currentTarget.dataset as any).msgId as string | undefined;
|
||||
if (!messageId) return;
|
||||
// @ts-ignore
|
||||
messageStore.retrySend(this.data.conversationId, messageId).catch((err: unknown) => {
|
||||
appStore.toastError(err instanceof Error ? err.message : '重发失败');
|
||||
});
|
||||
},
|
||||
|
||||
async onScrollToUpper() {
|
||||
const { conversationId, hasMore, loadingMore } = this.data;
|
||||
if (!hasMore || loadingMore) return;
|
||||
const first = this.data.messages[0];
|
||||
this._loadingMore = true;
|
||||
this._preserveMsgId = first?.messageId || null;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
// @ts-ignore
|
||||
await messageStore.loadMoreMessages(conversationId);
|
||||
} catch (err: unknown) {
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
this._loadingMore = false;
|
||||
this._preserveMsgId = null;
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
onMore() {
|
||||
const { otherUserId } = this.data;
|
||||
if (!otherUserId) { appStore.toast('暂无更多信息'); return; }
|
||||
wx.showActionSheet({
|
||||
itemList: ['查看 Ta 的资料', '举报 Ta', '拉黑 Ta'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) wx.navigateTo({ url: to.detail(otherUserId) });
|
||||
else if (res.tapIndex === 1) wx.navigateTo({ url: to.report(otherUserId) });
|
||||
else if (res.tapIndex === 2) this.setData({ showDangerDialog: true, dangerAction: DANGER_ACTION.BLOCK });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async onConfirmDanger(e: WechatMiniprogram.CustomEvent) {
|
||||
const action = (e.detail as any)?.action as string | undefined;
|
||||
const { otherUserId } = this.data;
|
||||
this.setData({ showDangerDialog: false });
|
||||
if (action !== DANGER_ACTION.BLOCK) return;
|
||||
try {
|
||||
appStore.showLoading('处理中…');
|
||||
await blockUser(otherUserId, '用户主动拉黑');
|
||||
appStore.hideLoading();
|
||||
this.setData({ canSend: false });
|
||||
appStore.toastSuccess('已拉黑');
|
||||
} catch (err: unknown) {
|
||||
appStore.hideLoading();
|
||||
appStore.toastError(err instanceof Error ? err.message : '操作失败');
|
||||
}
|
||||
},
|
||||
|
||||
onCancelDanger() {
|
||||
this.setData({ showDangerDialog: false });
|
||||
},
|
||||
|
||||
_unsubConn: (() => {}) as () => void,
|
||||
_unsubList: (() => {}) as () => void,
|
||||
_unsubRealtime: (() => {}) as () => void,
|
||||
_loadingMore: false,
|
||||
_preserveMsgId: null as string | null,
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- 聊天详情页 U24(重构:ui-nav-bar + ui-message-bubble + MessageStore) -->
|
||||
<view class="page">
|
||||
<!-- 顶部导航 -->
|
||||
<ui-nav-bar
|
||||
title="{{otherDisplayName || '聊天'}}"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
>
|
||||
<view slot="right" class="more-btn" bindtap="onMore">⋯</view>
|
||||
</ui-nav-bar>
|
||||
|
||||
<!-- 安全提示 -->
|
||||
<view class="safety-tip">
|
||||
<text>请注意保护个人隐私,谨慎涉及转账、借款、投资等资金往来。</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="msg-list"
|
||||
scroll-into-view="{{scrollIntoView}}"
|
||||
scroll-with-animation
|
||||
bindscrolltoupper="onScrollToUpper"
|
||||
upper-threshold="80"
|
||||
>
|
||||
<!-- 加载更早消息 -->
|
||||
<ui-loading
|
||||
wx:if="{{loadingMore}}"
|
||||
mode="spinner"
|
||||
inline
|
||||
text="加载更多消息…"
|
||||
/>
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
<view
|
||||
wx:for="{{messages}}"
|
||||
wx:key="messageId"
|
||||
id="msg-{{index}}"
|
||||
>
|
||||
<ui-message-bubble
|
||||
bubble-type="{{item.bubbleType}}"
|
||||
content="{{item.content}}"
|
||||
sent-at="{{item.sentAt}}"
|
||||
sent-at-text="{{item.sentAtText}}"
|
||||
show-time="{{item.showTime}}"
|
||||
show-avatar="{{item.showAvatar}}"
|
||||
avatar-url="{{item.avatarUrl}}"
|
||||
send-status="{{item.sendStatus || 'sent'}}"
|
||||
data-msg-id="{{item.messageId}}"
|
||||
bind:retry="onRetrySend"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 首次加载 -->
|
||||
<ui-loading
|
||||
wx:if="{{loading && messages.length === 0}}"
|
||||
mode="spinner"
|
||||
text="加载中…"
|
||||
/>
|
||||
|
||||
<!-- 没有更多 -->
|
||||
<view wx:if="{{!hasMore && messages.length > 0}}" class="load-more-hint">
|
||||
<text>没有更多消息了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入栏 -->
|
||||
<view wx:if="{{canSend}}" class="input-bar">
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="输入消息..."
|
||||
value="{{inputText}}"
|
||||
bindinput="onInput"
|
||||
confirm-type="send"
|
||||
bindconfirm="onSend"
|
||||
disabled="{{sending}}"
|
||||
maxlength="500"
|
||||
adjust-position="{{true}}"
|
||||
hold-keyboard="{{true}}"
|
||||
/>
|
||||
<ui-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled="{{sending || !inputText.trim()}}"
|
||||
loading="{{sending}}"
|
||||
bind:tap="onSend"
|
||||
>发送</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话已停用 -->
|
||||
<view wx:elif="{{!canSend}}" class="blocked-tip">
|
||||
<text>该对话已停用,无法继续发送消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 危险动作确认(拉黑) -->
|
||||
<ui-danger-action-dialog
|
||||
visible="{{showDangerDialog}}"
|
||||
action="{{dangerAction}}"
|
||||
target-name="{{otherDisplayName}}"
|
||||
bind:confirm="onConfirmDanger"
|
||||
bind:cancel="onCancelDanger"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 聊天详情页样式(重构后大幅简化,气泡主体样式封装在 ui-message-bubble 内)
|
||||
*/
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 右上角更多按钮 */
|
||||
.more-btn {
|
||||
font-size: 40rpx;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0 var(--spacing-2);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 安全提示 */
|
||||
.safety-tip {
|
||||
background: var(--color-warning-light);
|
||||
padding: var(--spacing-2) var(--spacing-page-x);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-warning);
|
||||
text-align: center;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: var(--spacing-2) 0;
|
||||
}
|
||||
|
||||
.load-more-hint {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* 输入栏 */
|
||||
.input-bar {
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--spacing-2) var(--spacing-page-x);
|
||||
padding-bottom: calc(var(--spacing-2) + env(safe-area-inset-bottom));
|
||||
border-top: var(--border-width) solid var(--color-border);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: var(--color-bg-page);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 0 var(--spacing-3);
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* 会话已停用 */
|
||||
.blocked-tip {
|
||||
background: var(--color-bg-card);
|
||||
text-align: center;
|
||||
padding: var(--spacing-5) var(--spacing-page-x);
|
||||
padding-bottom: calc(var(--spacing-5) + env(safe-area-inset-bottom));
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
border-top: var(--border-width) solid var(--color-border);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 签到码页 U31
|
||||
*/
|
||||
const api = require('../../utils/api');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activityId: '',
|
||||
activityTitle: '',
|
||||
locationName: '',
|
||||
token: '',
|
||||
expiresAt: '',
|
||||
loading: true,
|
||||
error: ''
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.setData({ activityId: options.id });
|
||||
this.loadCheckinToken();
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次显示时刷新Token
|
||||
if (this.data.activityId) {
|
||||
this.loadCheckinToken();
|
||||
}
|
||||
},
|
||||
|
||||
async loadCheckinToken() {
|
||||
this.setData({ loading: true, error: '' });
|
||||
try {
|
||||
const token = wx.getStorageSync('accessToken');
|
||||
const res = await api.post(`/activities/${this.data.activityId}/checkin-token`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
this.setData({
|
||||
token: res.token,
|
||||
activityTitle: res.activityTitle,
|
||||
locationName: res.locationName,
|
||||
expiresAt: res.expiresAt,
|
||||
loading: false
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loading: false, error: err.message || '加载失败' });
|
||||
}
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.loadCheckinToken();
|
||||
},
|
||||
|
||||
copyToken() {
|
||||
wx.setClipboardData({
|
||||
data: this.data.token,
|
||||
success: () => wx.showToast({ title: '签到码已复制', icon: 'success' })
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "活动签到",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!-- 签到码页 U31 -->
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">活动签到</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading-state">
|
||||
<text>生成签到码中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{error}}" class="error-state">
|
||||
<text>{{error}}</text>
|
||||
<button class="btn-refresh" bindtap="onRefresh">重试</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!loading && !error}}" class="checkin-card">
|
||||
<view class="activity-info">
|
||||
<text class="activity-title">{{activityTitle}}</text>
|
||||
<text class="activity-location">📍 {{locationName}}</text>
|
||||
</view>
|
||||
|
||||
<view class="qr-section">
|
||||
<view class="qr-placeholder">
|
||||
<text class="qr-icon">▣</text>
|
||||
</view>
|
||||
<text class="qr-hint">请向工作人员展示此签到码</text>
|
||||
</view>
|
||||
|
||||
<view class="token-section">
|
||||
<text class="token-label">签到码</text>
|
||||
<text class="token-value" selectable="true">{{token}}</text>
|
||||
<button class="btn-copy" bindtap="copyToken">复制签到码</button>
|
||||
</view>
|
||||
|
||||
<view class="expire-info">
|
||||
<text>有效期:{{formatExpiresAt(expiresAt)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-refresh-page" bindtap="onRefresh">刷新签到码</button>
|
||||
</view>
|
||||
@@ -0,0 +1,140 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
background: #FFF8F7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.loading-state, .error-state {
|
||||
text-align: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.checkin-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.activity-location {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-icon {
|
||||
font-size: 120rpx;
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.token-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
background: #F5F5F5;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
word-break: break-all;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: #FFF3F2;
|
||||
color: #E96B6B;
|
||||
border-radius: 36rpx;
|
||||
font-size: 26rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.expire-info {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #E59A32;
|
||||
padding: 16rpx;
|
||||
background: #FFF8E1;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.btn-refresh-page {
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
margin-top: 24rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// pages/counseling-intro/counseling-intro.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "公益婚恋咨询"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
Page({
|
||||
data: {
|
||||
services: [] as Array<{ type: string; name: string; description: string; icon: string }>
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadServices();
|
||||
},
|
||||
|
||||
async loadServices() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: `${app.globalData.apiBase}/counseling/services/intro`,
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
this.setData({ services: res.data.data });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载咨询介绍失败', e);
|
||||
}
|
||||
},
|
||||
|
||||
onSelectService(e: any) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
wx.navigateTo({
|
||||
url: `/pages/counseling-request/counseling-request?type=${type}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">公益婚恋咨询</text>
|
||||
<text class="subtitle">专业红娘为您提供情感支持</text>
|
||||
</view>
|
||||
|
||||
<view class="service-list">
|
||||
<view class="service-item" wx:for="{{services}}" wx:key="type" bindtap="onSelectService" data-type="{{item.type}}">
|
||||
<view class="service-icon">{{item.icon}}</view>
|
||||
<view class="service-info">
|
||||
<text class="service-name">{{item.name}}</text>
|
||||
<text class="service-desc">{{item.description}}</text>
|
||||
</view>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="notice">
|
||||
<text>所有咨询服务均为公益免费,无需支付任何费用。</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,14 @@
|
||||
.container { padding: 24rpx; }
|
||||
.header { text-align: center; margin-bottom: 32rpx; }
|
||||
.title { font-size: 36rpx; font-weight: bold; color: #333; }
|
||||
.subtitle { font-size: 26rpx; color: #888; margin-top: 8rpx; display: block; }
|
||||
.service-list { background: #fff; border-radius: 16rpx; overflow: hidden; }
|
||||
.service-item { display: flex; align-items: center; padding: 28rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.service-item:last-child { border-bottom: none; }
|
||||
.service-icon { font-size: 40rpx; margin-right: 20rpx; }
|
||||
.service-info { flex: 1; }
|
||||
.service-name { font-size: 30rpx; color: #333; display: block; }
|
||||
.service-desc { font-size: 24rpx; color: #999; margin-top: 6rpx; display: block; }
|
||||
.arrow { font-size: 36rpx; color: #ccc; }
|
||||
.notice { margin-top: 24rpx; padding: 20rpx; background: #fff8e1; border-radius: 12rpx; text-align: center; }
|
||||
.notice text { font-size: 24rpx; color: #f57c00; }
|
||||
@@ -0,0 +1,66 @@
|
||||
// pages/counseling-request/counseling-request.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "申请咨询"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||
|
||||
Page({
|
||||
data: {
|
||||
serviceType: '',
|
||||
serviceTypeLabel: '',
|
||||
modes: ['线上视频', '线下面谈', '电话沟通'],
|
||||
pModeIndex: 0,
|
||||
preferredTime: '',
|
||||
summary: '',
|
||||
submitting: false
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
if (options.type) {
|
||||
const typeMap: Record<string, string> = {
|
||||
relationship_guidance: '恋爱困惑指导',
|
||||
emotional_support: '情感支持',
|
||||
premarital_guidance: '婚前辅导',
|
||||
anti_fraud_guidance: '婚恋反诈指导'
|
||||
};
|
||||
this.setData({ serviceType: options.type, serviceTypeLabel: typeMap[options.type] || options.type });
|
||||
}
|
||||
},
|
||||
|
||||
onModeChange(e: any) {
|
||||
this.setData({ pModeIndex: e.detail.value });
|
||||
},
|
||||
|
||||
onTimeInput(e: any) { this.setData({ preferredTime: e.detail.value }); },
|
||||
onSummaryInput(e: any) { this.setData({ summary: e.detail.value }); },
|
||||
|
||||
async onSubmit() {
|
||||
const { serviceType, pModeIndex, preferredTime, summary } = this.data;
|
||||
if (!serviceType) return wx.showToast({ title: '请选择服务类型', icon: 'none' });
|
||||
this.setData({ submitting: true });
|
||||
try {
|
||||
await wx.request({
|
||||
url: `${app.globalData.apiBase}/counseling/services`,
|
||||
method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||||
data: { serviceType, preferredServiceMode: this.data.modes[pModeIndex], preferredTime, userRequestSummary: summary }
|
||||
});
|
||||
wx.showToast({ title: '申请已提交', icon: 'success' });
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/my-counseling/my-counseling' }), 1500);
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '提交失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<view class="container">
|
||||
<view class="form-section">
|
||||
<view class="form-group">
|
||||
<text class="label">服务类型</text>
|
||||
<text class="value">{{serviceTypeLabel}}</text>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="label">沟通方式</text>
|
||||
<picker mode="selector" range="{{modes}}" bindchange="onModeChange">
|
||||
<view class="picker-value">{{modes[pModeIndex] || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="label">期望时间</text>
|
||||
<input class="input" placeholder="如:工作日晚上7点后" value="{{preferredTime}}" bindinput="onTimeInput" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="label">问题描述</text>
|
||||
<textarea class="textarea" placeholder="请简要描述您的需求(选填)" value="{{summary}}" bindinput="onSummaryInput" maxlength="500" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" bindtap="onSubmit" loading="{{submitting}}">提交申请</button>
|
||||
<text class="tip">提交后工作人员将在1-3个工作日内与您联系</text>
|
||||
</view>
|
||||
@@ -0,0 +1,11 @@
|
||||
.container { padding: 24rpx; }
|
||||
.form-section { background: #fff; border-radius: 16rpx; padding: 24rpx; }
|
||||
.form-group { padding: 20rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.form-group:last-child { border-bottom: none; }
|
||||
.label { font-size: 28rpx; color: #666; display: block; margin-bottom: 12rpx; }
|
||||
.value { font-size: 30rpx; color: #333; }
|
||||
.picker-value { font-size: 30rpx; color: #333; }
|
||||
.input { font-size: 30rpx; color: #333; }
|
||||
.textarea { font-size: 30rpx; color: #333; width: 100%; height: 160rpx; }
|
||||
.submit-btn { margin-top: 40rpx; background: linear-gradient(135deg, #FF6B6B, #EE5A24); color: #fff; border: none; border-radius: 44rpx; font-size: 32rpx; }
|
||||
.tip { display: block; text-align: center; font-size: 24rpx; color: #999; margin-top: 20rpx; }
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 找对象页 U19-U20(重构:用 ui-user-card + matchStore.like + recommendationService.discover)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 2 条:Required 不可自动放宽——Discover 搜索仅透传筛选条件到后端,
|
||||
* 由后端按 PartnerPreference 做硬约束过滤,前端不做任何"放宽再查"。
|
||||
* - 第 4 条:Block 即时禁止互动——本页 Like 后由 matchStore 从列表移除。
|
||||
* - 第 6 条:单向意向不泄露——Pass/Like 未 Mutual 时对方不收到通知。
|
||||
* - 第 9 条:PostgreSQL 是 Source of Truth——搜索结果不在 Store 持久化,
|
||||
* 列表状态为页面局部状态(BaseStore 原则 2)。
|
||||
*/
|
||||
const { matchStore, appStore, authStore } = require('../../stores');
|
||||
const { recommendationService } = require('../../services');
|
||||
const { to } = require('../../constants');
|
||||
|
||||
const DEFAULT_AGE_MIN = 18;
|
||||
const DEFAULT_AGE_MAX = 60;
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
users: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
cursor: null,
|
||||
hasMore: true,
|
||||
|
||||
// 搜索栏关键词(编辑中)
|
||||
keywordInput: '',
|
||||
showFilter: false,
|
||||
|
||||
// 临时筛选(编辑中)
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
|
||||
// 已应用筛选
|
||||
applied: { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX },
|
||||
hasActiveFilter: false,
|
||||
|
||||
regions: ['丹阳', '镇江', '南京'],
|
||||
actionLoadingUserId: '',
|
||||
|
||||
emptyActions: [
|
||||
{ text: '清空筛选', event: 'reset', variant: 'primary' },
|
||||
{ text: '去今日推荐', event: 'home', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
/** 拉取搜索结果(首页) */
|
||||
async fetchDiscover(first) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const result = await recommendationService.discover(this._buildParams());
|
||||
const items = result.items || [];
|
||||
this.setData({
|
||||
users: first ? items : [...this.data.users, ...items],
|
||||
cursor: result.nextCursor || null,
|
||||
hasMore: !!result.nextCursor,
|
||||
loading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loading: false });
|
||||
appStore.toastError(err);
|
||||
}
|
||||
},
|
||||
|
||||
/** 加载更多 */
|
||||
async loadMore() {
|
||||
if (this.data.loadingMore || !this.data.hasMore || !this.data.cursor) return;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
const result = await recommendationService.discover({
|
||||
...this._buildParams(),
|
||||
cursor: this.data.cursor,
|
||||
});
|
||||
const items = result.items || [];
|
||||
this.setData({
|
||||
users: [...this.data.users, ...items],
|
||||
cursor: result.nextCursor || null,
|
||||
hasMore: !!result.nextCursor,
|
||||
});
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 构造请求参数(仅透传,后端守 Required 硬约束) */
|
||||
_buildParams() {
|
||||
const { applied } = this.data;
|
||||
const params = { pageSize: PAGE_SIZE };
|
||||
if (applied.keyword) params.keyword = applied.keyword;
|
||||
if (applied.region) params.region = applied.region;
|
||||
if (applied.ageMin !== DEFAULT_AGE_MIN) params.ageMin = applied.ageMin;
|
||||
if (applied.ageMax !== DEFAULT_AGE_MAX) params.ageMax = applied.ageMax;
|
||||
return params;
|
||||
},
|
||||
|
||||
// ============ 搜索栏 ============
|
||||
|
||||
onKeywordInput(e) {
|
||||
this.setData({ keywordInput: e.detail.value });
|
||||
},
|
||||
|
||||
/** 关键词搜索(键盘确认或点搜索图标) */
|
||||
onSearch() {
|
||||
const keyword = (this.data.keywordInput || '').trim();
|
||||
const applied = { ...this.data.applied, keyword };
|
||||
this.setData({
|
||||
applied,
|
||||
hasActiveFilter: this._hasActiveFilter(applied),
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
// ============ 筛选面板 ============
|
||||
|
||||
toggleFilter() {
|
||||
this.setData({ showFilter: !this.data.showFilter });
|
||||
},
|
||||
|
||||
onRegionTap(e) {
|
||||
const val = e.currentTarget.dataset.val;
|
||||
this.setData({ tempRegion: this.data.tempRegion === val ? '' : val });
|
||||
},
|
||||
|
||||
onMinAgeChange(e) {
|
||||
let min = e.detail.value;
|
||||
if (min > this.data.tempAgeMax) min = this.data.tempAgeMax;
|
||||
this.setData({ tempAgeMin: min });
|
||||
},
|
||||
|
||||
onMaxAgeChange(e) {
|
||||
let max = e.detail.value;
|
||||
if (max < this.data.tempAgeMin) max = this.data.tempAgeMin;
|
||||
this.setData({ tempAgeMax: max });
|
||||
},
|
||||
|
||||
onApplyFilter() {
|
||||
const { tempRegion, tempAgeMin, tempAgeMax, keywordInput } = this.data;
|
||||
const applied = {
|
||||
keyword: (keywordInput || '').trim(),
|
||||
region: tempRegion,
|
||||
ageMin: tempAgeMin,
|
||||
ageMax: tempAgeMax,
|
||||
};
|
||||
this.setData({
|
||||
applied,
|
||||
hasActiveFilter: this._hasActiveFilter(applied),
|
||||
showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
onResetFilter() {
|
||||
const applied = { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX };
|
||||
this.setData({
|
||||
keywordInput: '',
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied,
|
||||
hasActiveFilter: false,
|
||||
showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
_hasActiveFilter(applied) {
|
||||
return !!(
|
||||
applied.keyword ||
|
||||
applied.region ||
|
||||
applied.ageMin !== DEFAULT_AGE_MIN ||
|
||||
applied.ageMax !== DEFAULT_AGE_MAX
|
||||
);
|
||||
},
|
||||
|
||||
onEmptyAction(e) {
|
||||
const { event } = e.detail || {};
|
||||
if (event === 'reset') {
|
||||
this.onResetFilter();
|
||||
} else if (event === 'home') {
|
||||
wx.switchTab({ url: '/pages/index/index' });
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 卡片操作 ============
|
||||
|
||||
/** 卡片点击:查看资料 */
|
||||
onViewDetail(e) {
|
||||
const { userId } = e.detail || {};
|
||||
if (!userId) return;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
/** 喜欢(actions slot 按钮,catchtap 阻止冒泡到卡片) */
|
||||
async onLike(e) {
|
||||
const userId = e.currentTarget.dataset.userId;
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
const result = await matchStore.like(userId);
|
||||
// 从列表移除(无论是否 Mutual,已做出判断)
|
||||
this.setData({ users: this.data.users.filter((u) => u.userId !== userId) });
|
||||
if (result && result.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 列表交互 ============
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchDiscover(true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "找对象",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#F7F8FA",
|
||||
"backgroundTextStyle": "dark",
|
||||
"usingComponents": {
|
||||
"ui-user-card": "/components/user-card/index",
|
||||
"ui-button": "/components/button/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-empty": "/components/empty/index",
|
||||
"ui-section-header": "/components/section-header/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 找对象页 U19-U20(TypeScript)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 2 条:Required 不可自动放宽——Discover 搜索仅透传筛选条件到后端
|
||||
* - 第 4 条:Block 即时禁止互动
|
||||
* - 第 6 条:单向意向不泄露
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { matchStore, appStore, authStore } from '../../stores';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { recommendationService } from '../../services';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { to } from '../../constants';
|
||||
import type { RecommendationCandidate } from '../../types';
|
||||
|
||||
interface DiscoverData {
|
||||
users: RecommendationCandidate[];
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
cursor: string | null;
|
||||
hasMore: boolean;
|
||||
keywordInput: string;
|
||||
showFilter: boolean;
|
||||
tempRegion: string;
|
||||
tempAgeMin: number;
|
||||
tempAgeMax: number;
|
||||
applied: { keyword: string; region: string; ageMin: number; ageMax: number };
|
||||
hasActiveFilter: boolean;
|
||||
regions: string[];
|
||||
actionLoadingUserId: string;
|
||||
emptyActions: Array<{ text: string; event: string; variant: string }>;
|
||||
}
|
||||
|
||||
const DEFAULT_AGE_MIN = 18;
|
||||
const DEFAULT_AGE_MAX = 60;
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
users: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
cursor: null,
|
||||
hasMore: true,
|
||||
keywordInput: '',
|
||||
showFilter: false,
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied: { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX },
|
||||
hasActiveFilter: false,
|
||||
regions: ['丹阳', '镇江', '南京'],
|
||||
actionLoadingUserId: '',
|
||||
emptyActions: [
|
||||
{ text: '清空筛选', event: 'reset', variant: 'primary' },
|
||||
{ text: '去今日推荐', event: 'home', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
async fetchDiscover(first: boolean) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
// @ts-ignore - recommendationService 接口待 TypeScript 化
|
||||
const result = await recommendationService.discover(this._buildParams());
|
||||
const items: RecommendationCandidate[] = (result?.items as RecommendationCandidate[]) || [];
|
||||
this.setData({
|
||||
users: first ? items : [...this.data.users, ...items],
|
||||
cursor: result?.nextCursor || null,
|
||||
hasMore: !!result?.nextCursor,
|
||||
loading: false,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
this.setData({ loading: false });
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (this.data.loadingMore || !this.data.hasMore || !this.data.cursor) return;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = await recommendationService.discover({
|
||||
...this._buildParams(),
|
||||
cursor: this.data.cursor,
|
||||
});
|
||||
const items: RecommendationCandidate[] = (result?.items as RecommendationCandidate[]) || [];
|
||||
this.setData({
|
||||
users: [...this.data.users, ...items],
|
||||
cursor: result?.nextCursor || null,
|
||||
hasMore: !!result?.nextCursor,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
_buildParams() {
|
||||
const { applied } = this.data;
|
||||
const params: Record<string, unknown> = { pageSize: PAGE_SIZE };
|
||||
if (applied.keyword) params.keyword = applied.keyword;
|
||||
if (applied.region) params.region = applied.region;
|
||||
if (applied.ageMin !== DEFAULT_AGE_MIN) params.ageMin = applied.ageMin;
|
||||
if (applied.ageMax !== DEFAULT_AGE_MAX) params.ageMax = applied.ageMax;
|
||||
return params;
|
||||
},
|
||||
|
||||
onKeywordInput(e: any) {
|
||||
this.setData({ keywordInput: e.detail.value });
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
const keyword = (this.data.keywordInput || '').trim();
|
||||
const applied = { ...this.data.applied, keyword };
|
||||
this.setData({ applied, hasActiveFilter: this._hasActiveFilter(applied) });
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
toggleFilter() {
|
||||
this.setData({ showFilter: !this.data.showFilter });
|
||||
},
|
||||
|
||||
onRegionTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const val = (e.currentTarget.dataset as any).val as string;
|
||||
this.setData({ tempRegion: this.data.tempRegion === val ? '' : val });
|
||||
},
|
||||
|
||||
onMinAgeChange(e: any) {
|
||||
let min = Number((e as any).detail.value);
|
||||
if (min > this.data.tempAgeMax) min = this.data.tempAgeMax;
|
||||
this.setData({ tempAgeMin: min });
|
||||
},
|
||||
|
||||
onMaxAgeChange(e: any) {
|
||||
let max = Number((e as any).detail.value);
|
||||
if (max < this.data.tempAgeMin) max = this.data.tempAgeMin;
|
||||
this.setData({ tempAgeMax: max });
|
||||
},
|
||||
|
||||
onApplyFilter() {
|
||||
const { tempRegion, tempAgeMin, tempAgeMax, keywordInput } = this.data;
|
||||
const applied = { keyword: (keywordInput || '').trim(), region: tempRegion, ageMin: tempAgeMin, ageMax: tempAgeMax };
|
||||
this.setData({ applied, hasActiveFilter: this._hasActiveFilter(applied), showFilter: false });
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
onResetFilter() {
|
||||
const applied = { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX };
|
||||
this.setData({
|
||||
keywordInput: '', tempRegion: '', tempAgeMin: DEFAULT_AGE_MIN, tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied, hasActiveFilter: false, showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
_hasActiveFilter(applied: { keyword: string; region: string; ageMin: number; ageMax: number }) {
|
||||
return !!(
|
||||
applied.keyword || applied.region ||
|
||||
applied.ageMin !== DEFAULT_AGE_MIN || applied.ageMax !== DEFAULT_AGE_MAX
|
||||
);
|
||||
},
|
||||
|
||||
onEmptyAction(e: WechatMiniprogram.TouchEvent) {
|
||||
const { event } = (e.detail as any) || {};
|
||||
if (event === 'reset') this.onResetFilter();
|
||||
else if (event === 'home') wx.switchTab({ url: '/pages/index/index' });
|
||||
},
|
||||
|
||||
onViewDetail(e: WechatMiniprogram.TouchEvent) {
|
||||
const { userId } = (e.detail as any) || {};
|
||||
if (!userId) return;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
async onLike(e: WechatMiniprogram.TouchEvent) {
|
||||
const userId = (e.currentTarget.dataset as any).userId as string | undefined;
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = await matchStore.like(userId);
|
||||
this.setData({ users: this.data.users.filter((u) => u.userId !== userId) });
|
||||
if (result?.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
appStore.toastError(err instanceof Error ? err.message : '操作失败');
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchDiscover(true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- 找对象页 U19-U20(重构:ui-user-card + matchStore + recommendationService.discover) -->
|
||||
<view class="page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-field"
|
||||
placeholder="搜索符合条件的对象"
|
||||
placeholder-class="search-placeholder"
|
||||
value="{{keywordInput}}"
|
||||
bindinput="onKeywordInput"
|
||||
confirm-type="search"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-btn {{showFilter ? 'active' : ''}} {{hasActiveFilter ? 'has-filter' : ''}}" bindtap="toggleFilter">
|
||||
<text>筛选</text>
|
||||
<view wx:if="{{hasActiveFilter}}" class="filter-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选面板 -->
|
||||
<view wx:if="{{showFilter}}" class="filter-panel">
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">年龄范围</text>
|
||||
<view class="range-row">
|
||||
<text class="range-value">{{tempAgeMin}} 岁</text>
|
||||
<slider
|
||||
class="range-slider"
|
||||
min="{{18}}"
|
||||
max="{{60}}"
|
||||
value="{{tempAgeMin}}"
|
||||
activeColor="{{'#E96B6B'}}"
|
||||
backgroundColor="{{'#F0F0F0'}}"
|
||||
block-size="{{20}}"
|
||||
bindchange="onMinAgeChange"
|
||||
/>
|
||||
<text class="range-value">{{tempAgeMax}} 岁</text>
|
||||
</view>
|
||||
<slider
|
||||
class="range-slider"
|
||||
min="{{18}}"
|
||||
max="{{60}}"
|
||||
value="{{tempAgeMax}}"
|
||||
activeColor="{{'#E96B6B'}}"
|
||||
backgroundColor="{{'#F0F0F0'}}"
|
||||
block-size="{{20}}"
|
||||
bindchange="onMaxAgeChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">地区</text>
|
||||
<view class="tag-group">
|
||||
<view
|
||||
wx:for="{{regions}}"
|
||||
wx:key="*this"
|
||||
class="tag {{tempRegion === item ? 'selected' : ''}}"
|
||||
bindtap="onRegionTap"
|
||||
data-val="{{item}}"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-actions">
|
||||
<ui-button variant="ghost" size="md" block bind:tap="onResetFilter">重置</ui-button>
|
||||
<ui-button variant="primary" size="md" block bind:tap="onApplyFilter">应用筛选</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区块标题 -->
|
||||
<ui-section-header
|
||||
wx:if="{{users.length > 0 || loading}}"
|
||||
title="找对象"
|
||||
subtitle="{{hasActiveFilter ? '已应用筛选' : '共 ' + users.length + ' 位'}}"
|
||||
/>
|
||||
|
||||
<!-- 首次加载(骨架屏) -->
|
||||
<ui-loading
|
||||
wx:if="{{loading && users.length === 0}}"
|
||||
mode="skeleton"
|
||||
rows="5"
|
||||
/>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<block wx:elif="{{users.length > 0}}">
|
||||
<view class="user-list">
|
||||
<ui-user-card
|
||||
wx:for="{{users}}"
|
||||
wx:key="userId"
|
||||
user="{{item}}"
|
||||
bind:tap="onViewDetail"
|
||||
>
|
||||
<view
|
||||
slot="actions"
|
||||
class="card-action"
|
||||
catchtap="onLike"
|
||||
data-user-id="{{item.userId}}"
|
||||
>
|
||||
<ui-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading="{{actionLoadingUserId === item.userId}}"
|
||||
>喜欢</ui-button>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<ui-loading
|
||||
wx:if="{{loadingMore}}"
|
||||
mode="spinner"
|
||||
inline
|
||||
text="加载更多…"
|
||||
/>
|
||||
<view wx:if="{{!hasMore && users.length > 0}}" class="list-end-hint">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ui-empty
|
||||
wx:elif="{{!loading}}"
|
||||
illustration="search"
|
||||
title="暂无符合条件的人选"
|
||||
description="尝试调整筛选条件,或看看今天为你推荐的人。"
|
||||
actions="{{emptyActions}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 找对象页样式(重构后大幅简化,用户卡片主体样式封装在 ui-user-card 内)
|
||||
*/
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* ============ 搜索栏 ============ */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) var(--spacing-page-x);
|
||||
background: var(--color-bg-card);
|
||||
gap: var(--spacing-2);
|
||||
border-bottom: var(--border-width) solid var(--color-divider);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: var(--color-bg-page);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-3);
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.search-field {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
min-width: 120rpx;
|
||||
height: 72rpx;
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-primary);
|
||||
position: relative;
|
||||
padding: 0 var(--spacing-3);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.filter-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
}
|
||||
|
||||
.filter-btn.active .filter-dot {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ============ 筛选面板 ============ */
|
||||
.filter-panel {
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--spacing-4) var(--spacing-page-x);
|
||||
border-bottom: var(--border-width) solid var(--color-divider);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range-value {
|
||||
min-width: 96rpx;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.range-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tag-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.tag.selected {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* ============ 用户列表 ============ */
|
||||
.user-list {
|
||||
background: var(--color-bg-card);
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-end-hint {
|
||||
text-align: center;
|
||||
padding: var(--spacing-3);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 我的缘分页 U22(重构:用 matchStore + ui-user-card + ui-nav-bar)
|
||||
*
|
||||
* 3 Tab:互相喜欢(Mutual)/ 我喜欢的 / 喜欢我的(脱敏)
|
||||
* 游标分页:下拉刷新 + onReachBottom 加载更多
|
||||
* like-back:喜欢我的人 → 表达喜欢 → 若 Mutual 自动跳聊天
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 3 条:Mutual Match 后才能聊天——Mutual Tab 点击进入会话;未 Mutual 不可发起会话。
|
||||
* - 第 6 条:单向意向不泄露——"喜欢我的"仅展示后端返回的脱敏字段(昵称/年龄/地区/是否已 Mutual),
|
||||
* Store 与页面均不做二次身份还原。
|
||||
* - 第 5 条:红娘不可替用户 Like——like-back 仅当前用户本人操作。
|
||||
* - 第 9 条:PostgreSQL 是 Source of Truth——列表状态以 matchStore 同步服务端返回为准。
|
||||
*/
|
||||
const { matchStore, appStore, likeUser } = require('../../stores');
|
||||
const { to, ROUTE } = require('../../constants');
|
||||
|
||||
const TAB_MUTUAL = 'mutual';
|
||||
const TAB_LIKED = 'liked';
|
||||
const TAB_LIKERS = 'likers';
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: TAB_MUTUAL,
|
||||
|
||||
// 来自 matchStore 的列表(bindToPage 同步)
|
||||
mutualList: [],
|
||||
mutualHasMore: true,
|
||||
likedList: [],
|
||||
likedHasMore: true,
|
||||
likersList: [],
|
||||
likersHasMore: true,
|
||||
likersCount: 0,
|
||||
|
||||
// 页面局部状态
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
actionLoadingUserId: '',
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try {
|
||||
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight || 20,
|
||||
navBarHeight: info.platform === 'android' ? 48 : 44,
|
||||
});
|
||||
} catch (e) { /* 使用默认值 */ }
|
||||
|
||||
// 绑定 matchStore 列表字段到页面
|
||||
this._unsub = matchStore.bindToPage(this, {
|
||||
mutualList: 'mutualList',
|
||||
mutualHasMore: 'mutualHasMore',
|
||||
likedList: 'likedList',
|
||||
likedHasMore: 'likedHasMore',
|
||||
likersList: 'likersList',
|
||||
likersHasMore: 'likersHasMore',
|
||||
likersCount: 'likersCount',
|
||||
});
|
||||
|
||||
this._fetchTab(TAB_MUTUAL, true);
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 返回页面时刷新当前 Tab(状态可能已变化)
|
||||
this._fetchTab(this.data.activeTab, true);
|
||||
},
|
||||
|
||||
// ============ Tab 切换 ============
|
||||
|
||||
onTabChange(e) {
|
||||
const tab = e.currentTarget.dataset.tab;
|
||||
if (!tab || tab === this.data.activeTab) return;
|
||||
this.setData({ activeTab: tab });
|
||||
this._fetchTab(tab, true);
|
||||
},
|
||||
|
||||
/** 按当前 Tab 拉取首页 */
|
||||
async _fetchTab(tab, first) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const params = { pageSize: PAGE_SIZE };
|
||||
if (tab === TAB_MUTUAL) {
|
||||
await matchStore.fetchMutualList(params);
|
||||
} else if (tab === TAB_LIKED) {
|
||||
await matchStore.fetchLikedList(params);
|
||||
} else if (tab === TAB_LIKERS) {
|
||||
await matchStore.fetchLikersList(params);
|
||||
}
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 列表交互 ============
|
||||
|
||||
onPullDownRefresh() {
|
||||
this._fetchTab(this.data.activeTab, true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.loadingMore) return;
|
||||
const tab = this.data.activeTab;
|
||||
const hasMore = tab === TAB_MUTUAL ? this.data.mutualHasMore
|
||||
: tab === TAB_LIKED ? this.data.likedHasMore
|
||||
: this.data.likersHasMore;
|
||||
if (!hasMore) return;
|
||||
this.setData({ loadingMore: true });
|
||||
(async () => {
|
||||
try {
|
||||
if (tab === TAB_MUTUAL) await matchStore.loadMoreMutualList();
|
||||
else if (tab === TAB_LIKED) await matchStore.loadMoreLikedList();
|
||||
else await matchStore.loadMoreLikersList();
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
})();
|
||||
},
|
||||
|
||||
// ============ 卡片操作 ============
|
||||
|
||||
/**
|
||||
* 互相喜欢 / 我喜欢的 → 查看资料详情
|
||||
* 互相喜欢还可直接进入会话(已 Mutual,允许聊天)
|
||||
*/
|
||||
onViewDetail(e) {
|
||||
const userId = e.detail && e.detail.userId;
|
||||
if (!userId) return;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
/** 互相喜欢 → 开始聊天(已 Mutual,符合第 3 条) */
|
||||
onStartChat(e) {
|
||||
const conversationId = e.currentTarget.dataset.conversationId;
|
||||
if (!conversationId) return;
|
||||
wx.navigateTo({ url: to.chat(conversationId) });
|
||||
},
|
||||
|
||||
/**
|
||||
* 喜欢我的人 → 喜欢 TA(like-back)
|
||||
* 若达成 Mutual,likeUser 会自动刷新会话列表,此处跳转到聊天
|
||||
*/
|
||||
async onLikeBack(e) {
|
||||
const userId = e.currentTarget.dataset.userId;
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
const result = await likeUser(userId);
|
||||
// 从"喜欢我的"列表移除(已做出判断)
|
||||
matchStore.setState({
|
||||
likersList: matchStore.state.likersList.filter((u) => u.userId !== userId),
|
||||
});
|
||||
if (result && result.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
// 刷新互相喜欢列表
|
||||
try { await matchStore.fetchMutualList({ pageSize: PAGE_SIZE }); } catch (e2) { /* ignore */ }
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
/** 空状态动作 */
|
||||
onEmptyAction() {
|
||||
wx.switchTab({ url: ROUTE.TAB.DISCOVER });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的缘分",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#FFF8F7",
|
||||
"backgroundTextStyle": "dark",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-user-card": "/components/user-card/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-empty": "/components/empty/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<!-- 我的缘分页 U22(重构:ui-nav-bar + ui-user-card + matchStore) -->
|
||||
<view class="page">
|
||||
<ui-nav-bar
|
||||
title="我的缘分"
|
||||
show-back="{{true}}"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tabs" style="top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="tab {{activeTab === 'mutual' ? 'active' : ''}}" data-tab="mutual" bindtap="onTabChange">
|
||||
互相喜欢
|
||||
<text class="tab-count" wx:if="{{mutualList.length > 0}}">{{mutualList.length}}</text>
|
||||
</view>
|
||||
<view class="tab {{activeTab === 'liked' ? 'active' : ''}}" data-tab="liked" bindtap="onTabChange">
|
||||
我喜欢的
|
||||
<text class="tab-count" wx:if="{{likedList.length > 0}}">{{likedList.length}}</text>
|
||||
</view>
|
||||
<view class="tab {{activeTab === 'likers' ? 'active' : ''}}" data-tab="likers" bindtap="onTabChange">
|
||||
喜欢我的
|
||||
<text class="tab-count" wx:if="{{likersCount > 0}}">{{likersCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-wrap">
|
||||
<!-- 首次加载 -->
|
||||
<ui-loading wx:if="{{loading}}" mode="skeleton" rows="4" />
|
||||
|
||||
<block wx:elif="{{activeTab === 'mutual'}}">
|
||||
<block wx:if="{{mutualList.length > 0}}">
|
||||
<view class="list">
|
||||
<view class="item" wx:for="{{mutualList}}" wx:key="conversationId">
|
||||
<ui-user-card user="{{item}}" bind:tap="onViewDetail">
|
||||
<view slot="actions">
|
||||
<button class="btn-primary" catchtap="onStartChat" data-conversation-id="{{item.conversationId}}">开始聊天</button>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
</view>
|
||||
</view>
|
||||
<ui-loading wx:if="{{loadingMore}}" mode="skeleton" rows="2" />
|
||||
</block>
|
||||
<ui-empty
|
||||
wx:else
|
||||
illustration="heart"
|
||||
title="还没有互相喜欢的人"
|
||||
description="去「找对象」遇见合适的人,互相喜欢就能聊天啦"
|
||||
actions="{{[{text:'去「找对象', event:'discover', variant:'primary'}]}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{activeTab === 'liked'}}">
|
||||
<block wx:if="{{likedList.length > 0}}">
|
||||
<view class="list">
|
||||
<view class="item" wx:for="{{likedList}}" wx:key="userId">
|
||||
<ui-user-card user="{{item}}">
|
||||
<view slot="actions">
|
||||
<view class="status-waiting">已表达喜欢,等待回应</view>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
</view>
|
||||
</view>
|
||||
<ui-loading wx:if="{{loadingMore}}" mode="skeleton" rows="2" />
|
||||
</block>
|
||||
<ui-empty
|
||||
wx:else
|
||||
illustration="heart"
|
||||
title="还没有喜欢过任何人"
|
||||
description="去「找对象」看看,遇到心仪的人就表达喜欢吧"
|
||||
actions="{{[{text:'去「找对象', event:'discover', variant:'primary'}]}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{activeTab === 'likers'}}">
|
||||
<block wx:if="{{likersList.length > 0}}">
|
||||
<view class="list">
|
||||
<view class="item" wx:for="{{likersList}}" wx:key="userId">
|
||||
<ui-user-card user="{{item}}">
|
||||
<view slot="actions">
|
||||
<button
|
||||
class="btn-like-back {{actionLoadingUserId === item.userId ? 'disabled' : ''}}"
|
||||
catchtap="onLikeBack"
|
||||
data-user-id="{{item.userId}}"
|
||||
disabled="{{actionLoadingUserId === item.userId}}"
|
||||
>喜欢TA</button>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
</view>
|
||||
</view>
|
||||
<ui-loading wx:if="{{loadingMore}}" mode="skeleton" rows="2" />
|
||||
</block>
|
||||
<ui-empty
|
||||
wx:else
|
||||
illustration="heart"
|
||||
title="还没有人喜欢你"
|
||||
description="完善资料、上传真实生活照,更容易被合适的人看到"
|
||||
actions="{{[{text:'去「找对象', event:'discover', variant:'primary'}]}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,119 @@
|
||||
/* 我的缘分页 U22 — Design Tokens */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #FFF8F7;
|
||||
}
|
||||
|
||||
/* Tab 切换栏(粘性吸附在自定义导航栏下方) */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 28rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #E96B6B;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 4rpx;
|
||||
background: #E96B6B;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
display: inline-block;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-left: 8rpx;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 列表容器 */
|
||||
.list-wrap {
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.btn-primary {
|
||||
flex-shrink: 0;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-like-back {
|
||||
flex-shrink: 0;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
background: #FFF3F2;
|
||||
color: #E96B6B;
|
||||
border: 2rpx solid #E96B6B;
|
||||
}
|
||||
|
||||
.btn-like-back::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-like-back.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 等待回应状态 */
|
||||
.status-waiting {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
color: #E96B6B;
|
||||
padding: 8rpx 16rpx;
|
||||
background: #FFF3F2;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 帮助中心页 U41
|
||||
*
|
||||
* 静态 FAQ,无后端依赖。
|
||||
*/
|
||||
const { ROUTE } = require('../../constants');
|
||||
|
||||
const FAQ_DATA = [
|
||||
{
|
||||
category: '账号与登录',
|
||||
items: [
|
||||
{ q: '如何注册账号?', a: '点击「微信一键登录」,系统将自动创建账号并完成实名认证。' },
|
||||
{ q: '忘记登录密码怎么办?', a: '本平台使用微信授权登录,无需设置密码。如遇到问题请重新登录。' },
|
||||
{ q: '如何绑定手机号?', a: '登录后在「我的」页面完成实名认证即可绑定手机号。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '推荐与匹配',
|
||||
items: [
|
||||
{ q: '推荐列表是怎么生成的?', a: '系统根据您的择偶条件和已有用户资料,按照匹配算法每日生成推荐列表。' },
|
||||
{ q: '为什么收不到推荐?', a: '请检查您的资料完整度是否高于60%,以及择偶条件是否合理(不宜过窄)。' },
|
||||
{ q: '如何暂停推荐?', a: '在「设置」中切换关系状态为「已暂停寻找」即可停止推荐。' },
|
||||
{ q: '可以查看谁喜欢过我吗?', a: '可以,在首页右上角点击「喜欢我的人」即可查看。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '活动参与',
|
||||
items: [
|
||||
{ q: '如何报名参加活动?', a: '在活动详情页点击「立即报名」,填写紧急联系人信息后提交。' },
|
||||
{ q: '活动报名后能否取消?', a: '可以,在「我的活动」中找到对应活动并取消报名。' },
|
||||
{ q: '活动是否需要审核?', a: '部分活动需要公益组织审核后参与,审核结果将在活动开始前通知您。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '隐私与安全',
|
||||
items: [
|
||||
{ q: '平台如何保护我的隐私?', a: '所有敏感信息(身份证、手机号)均加密存储,仅授权工作人员在案件上下文中可有限访问。' },
|
||||
{ q: '如何设置隐私权限?', a: '在「隐私设置」中可以控制是否在推荐中可见、是否允许活动邀请等。' },
|
||||
{ q: '如何拉黑某人?', a: '在推荐或缘分列表中点击该用户卡片,选择「拉黑」即可。拉黑后对方将不再看到您。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '公益红娘',
|
||||
items: [
|
||||
{ q: '什么是公益红娘服务?', a: '公益红娘是由丹阳市公益组织指派的专业人士,为您提供一对一的婚恋咨询服务。' },
|
||||
{ q: '如何申请红娘服务?', a: '在首页点击「申请公益红娘」,填写服务需求后等待工作人员联系。' },
|
||||
{ q: '红娘服务收费吗?', a: '不收费。本平台是公益性质的免费婚恋服务平台。' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
platformName: getApp().globalData.platformName,
|
||||
activeCategory: '',
|
||||
faqData: FAQ_DATA,
|
||||
expandedIds: {},
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.setNavigationBarTitle({ title: '帮助中心' });
|
||||
if (FAQ_DATA.length > 0) {
|
||||
this.setData({ activeCategory: FAQ_DATA[0].category });
|
||||
}
|
||||
},
|
||||
|
||||
onCategoryTap(e) {
|
||||
const category = e.currentTarget.dataset.category;
|
||||
this.setData({ activeCategory: category });
|
||||
},
|
||||
|
||||
onToggleItem(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const expanded = Object.assign({}, this.data.expandedIds);
|
||||
expanded[id] = !expanded[id];
|
||||
this.setData({ expandedIds: expanded });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationBarTitleText":"help-center","usingComponents":{}}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 帮助中心页 -->
|
||||
<view class="page">
|
||||
<!-- 分类导航 -->
|
||||
<scroll-view scroll-x class="category-bar">
|
||||
<view
|
||||
wx:for="{{faqData}}"
|
||||
wx:key="category"
|
||||
class="category-item {{activeCategory === item.category ? 'active' : ''}}"
|
||||
data-category="{{item.category}}"
|
||||
bindtap="onCategoryTap"
|
||||
>
|
||||
{{item.category}}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- FAQ 列表 -->
|
||||
<scroll-view scroll-y class="faq-list">
|
||||
<block wx:for="{{faqData}}" wx:key="category" wx:for-item="cat">
|
||||
<view wx:if="{{activeCategory === cat.category}}">
|
||||
<view
|
||||
wx:for="{{cat.items}}"
|
||||
wx:key="q"
|
||||
wx:for-item="faq"
|
||||
class="faq-item"
|
||||
data-id="{{cat.category + '-' + faq.q}}"
|
||||
bindtap="onToggleItem"
|
||||
>
|
||||
<view class="faq-question">
|
||||
<text>{{faq.q}}</text>
|
||||
<text class="faq-arrow {{expandedIds[(cat.category + '-' + faq.q)] ? 'up' : ''}}">▼</text>
|
||||
</view>
|
||||
<view class="faq-answer" wx:if="{{expandedIds[cat.category + '-' + faq.q]}}">
|
||||
<text>{{faq.a}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,70 @@
|
||||
/* 帮助中心样式 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-bar {
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
padding: 0 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: inline-block;
|
||||
padding: 24rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
color: #E96B6B;
|
||||
border-bottom-color: #E96B6B;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
color: #2d2d2d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.faq-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.faq-arrow.up {
|
||||
transform: rotate(180deg);
|
||||
color: #E96B6B;
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 首页 - 每日推荐(重构:用 matchStore + recommend-card + appStore 统一编排)
|
||||
*
|
||||
* 关键不变量(AGENTS.md):
|
||||
* - 第 2 条:Required 不可自动放宽——推荐列表完全由后端按 PartnerPreference 过滤,
|
||||
* 前端只渲染后端返回的 recommendations,不做任何"放宽条件再查"的逻辑。
|
||||
* - 第 5 条:红娘不可替用户操作——所有 Like/Pass 由当前用户触发。
|
||||
* - 第 6 条:单向意向不泄露——likersCount 仅展示计数,不暴露具体身份。
|
||||
*/
|
||||
const { matchStore, appStore, userStore, authStore, blockUser, likeUser } = require('../../stores');
|
||||
const { ROUTE, to, RECOMMENDATION_STATE } = require('../../constants');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
greeting: '',
|
||||
userName: '',
|
||||
recommendations: [],
|
||||
recState: RECOMMENDATION_STATE.IDLE,
|
||||
likersCount: 0,
|
||||
actionLoadingUserId: '', // 正在操作的用户 id(Like/Pass 进行中)
|
||||
emptyActions: [
|
||||
{ text: '去「找对象」', event: 'discover', variant: 'primary' },
|
||||
{ text: '申请公益红娘', event: 'matchmaker', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setGreeting();
|
||||
// 绑定 matchStore 关键字段到 page data
|
||||
this._unsubMatch = matchStore.bindToPage(this, {
|
||||
recommendations: 'recommendations',
|
||||
recState: 'recState',
|
||||
});
|
||||
// 绑定 likersCount
|
||||
this._unsubMatchSub = matchStore.subscribe((state) => {
|
||||
if (state.likersCount !== this.data.likersCount) {
|
||||
this.setData({ likersCount: state.likersCount });
|
||||
}
|
||||
});
|
||||
// 绑定 userStore 用户名(用于问候语)
|
||||
this._unsubUser = userStore.subscribe((state) => {
|
||||
const name = state.profile?.displayName || '';
|
||||
if (name !== this.data.userName) this.setData({ userName: name });
|
||||
});
|
||||
this.setData({ userName: userStore.state.profile?.displayName || '' });
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 未登录则不请求推荐(bootstrap 已跳转登录页,此处为保险兜底)
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
// 每次显示刷新推荐(用户从聊天页返回后可能已 Like/Pass,需要新批次)
|
||||
this.loadRecommendations();
|
||||
// 异步刷新 likersCount(Tab 徽标也会更新)
|
||||
matchStore.fetchLikersCount().catch(() => {});
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this._unsubMatch && this._unsubMatch();
|
||||
this._unsubMatchSub && this._unsubMatchSub();
|
||||
this._unsubUser && this._unsubUser();
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadRecommendations().finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
// 加载更多推荐
|
||||
matchStore.loadMoreRecommendations().catch(() => {});
|
||||
},
|
||||
|
||||
setGreeting() {
|
||||
const hour = new Date().getHours();
|
||||
let greeting = '早上好';
|
||||
if (hour >= 12 && hour < 17) greeting = '下午好';
|
||||
else if (hour >= 17) greeting = '晚上好';
|
||||
this.setData({ greeting });
|
||||
},
|
||||
|
||||
async loadRecommendations() {
|
||||
try {
|
||||
await matchStore.fetchRecommendations();
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
}
|
||||
},
|
||||
|
||||
onViewDetail(e) {
|
||||
const { userId } = e.detail;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
async onLike(e) {
|
||||
const { userId } = e.detail;
|
||||
if (this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
const result = await likeUser(userId);
|
||||
if (result && result.mutualMatch) {
|
||||
// 双向匹配达成:弹祝福 + 引导去聊天
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
setTimeout(() => {
|
||||
wx.showModal({
|
||||
title: '互相喜欢',
|
||||
content: '你们已经互相喜欢,是否现在开始聊天?',
|
||||
confirmText: '开始聊天',
|
||||
cancelText: '稍后',
|
||||
confirmColor: '#E96B6B',
|
||||
success: (res) => {
|
||||
if (res.confirm && result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
},
|
||||
});
|
||||
}, 800);
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
async onPass(e) {
|
||||
const { userId } = e.detail;
|
||||
if (this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
await matchStore.pass(userId);
|
||||
// Pass 是单向意向,不打扰用户(AGENTS 第 6 条隐式)
|
||||
} catch (err) {
|
||||
// Pass 失败不提示,避免打扰
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
async onBlock(e) {
|
||||
const { userId } = e.detail;
|
||||
const target = this.data.recommendations.find((r) => r.userId === userId);
|
||||
const name = target?.displayName || '该用户';
|
||||
// 危险动作二次确认(AGENTS 第 4 条)
|
||||
const confirmed = await appStore.confirm({
|
||||
title: '拉黑该用户',
|
||||
content: `拉黑后 ${name} 将无法与你互动,你也不会再收到对方的推荐与消息。此操作可由你随时解除。`,
|
||||
confirmText: '确认拉黑',
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await blockUser(userId, '用户主动拉黑');
|
||||
appStore.toastSuccess('已拉黑');
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
}
|
||||
},
|
||||
|
||||
onViewLikers() {
|
||||
wx.navigateTo({ url: ROUTE.DETAIL.LIKERS });
|
||||
},
|
||||
|
||||
onEmptyAction(e) {
|
||||
const { event } = e.detail;
|
||||
if (event === 'discover') {
|
||||
wx.switchTab({ url: ROUTE.TAB.DISCOVER });
|
||||
} else if (event === 'matchmaker') {
|
||||
wx.navigateTo({ url: ROUTE.DETAIL.MATCHMAKER_APPLY });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationBarTitleText": "善缘丹阳",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark",
|
||||
"backgroundColor": "#FAFAF8",
|
||||
"usingComponents": {
|
||||
"ui-section-header": "/components/section-header/index",
|
||||
"ui-recommend-card": "/components/recommend-card/index",
|
||||
"ui-empty": "/components/empty/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-status-badge": "/components/status-badge/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 首页 - V1.1 新版(U18)
|
||||
* 结构:品牌区 → 用户状态条 → Banner → 8入口 → 推荐 → 服务建议 → 活动 → 服务站 → 安全提醒
|
||||
*/
|
||||
const authStore = require('../../stores/authStore')
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
unreadCount: 0,
|
||||
userState: null,
|
||||
isOnboarding: false,
|
||||
isVerified: false,
|
||||
isPaused: false,
|
||||
isSuccessful: false,
|
||||
onboardingMessage: '',
|
||||
onboardingActions: [],
|
||||
statusChecks: [],
|
||||
banners: [],
|
||||
currentBannerIndex: 0,
|
||||
quickEntries: [],
|
||||
recommendations: [],
|
||||
recState: 'idle',
|
||||
actionLoadingUserId: '',
|
||||
serviceRecommendations: [],
|
||||
activities: [],
|
||||
serviceLocation: null,
|
||||
articles: [],
|
||||
safetyNotice: {
|
||||
text: '实名认证和人工审核可以提高信息真实性,交往过程中仍请谨慎涉及借款、投资和个人隐私。',
|
||||
ctaText: '查看婚恋反诈指南',
|
||||
ctaTarget: ''
|
||||
},
|
||||
emptyActions: [
|
||||
{ text: '查看择偶条件', event: 'preference', variant: 'secondary' },
|
||||
{ text: '申请公益红娘', event: 'matchmaker', variant: 'primary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.checkAuthAndLoad()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (authStore.state.isLoggedIn) {
|
||||
this.loadHomeData()
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadHomeData().finally(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
// ==================== 核心加载 ====================
|
||||
|
||||
async checkAuthAndLoad() {
|
||||
if (!authStore.state.isLoggedIn) {
|
||||
this.setData({
|
||||
isOnboarding: true,
|
||||
onboardingMessage: '登录后即可开始寻找合适的人',
|
||||
onboardingActions: [{ text: '去登录', event: 'login', variant: 'primary' }],
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.loadHomeData()
|
||||
},
|
||||
|
||||
async loadHomeData() {
|
||||
try {
|
||||
const res = await api.get('/home')
|
||||
this.processHomeData(res)
|
||||
} catch (err) {
|
||||
console.error('首页数据加载失败', err)
|
||||
this.setData({ recState: 'error' })
|
||||
}
|
||||
},
|
||||
|
||||
processHomeData(data) {
|
||||
const { userState, banners, quickEntries, recommendations,
|
||||
serviceRecommendations, activities, serviceLocation, safetyNotice } = data
|
||||
|
||||
const relStatus = userState?.relationshipStatus || ''
|
||||
const profileStatus = userState?.profileStatus || ''
|
||||
const eligStatus = userState?.localEligibilityStatus || 'not_declared'
|
||||
|
||||
const isOnboarding = !userState || profileStatus !== 'approved'
|
||||
const isPaused = relStatus === 'paused'
|
||||
const isSuccessful = relStatus === 'matched_successfully'
|
||||
const isVerified = eligStatus === 'verified'
|
||||
|
||||
const checks = [
|
||||
{ label: '已实名', done: userState?.verified || false },
|
||||
{ label: '本地资格', done: eligStatus === 'verified' || eligStatus === 'pending_review' },
|
||||
{ label: '资料已审核', done: profileStatus === 'approved' || profileStatus === 'completed' },
|
||||
]
|
||||
|
||||
this.setData({
|
||||
userState,
|
||||
isOnboarding, isVerified, isPaused, isSuccessful,
|
||||
onboardingMessage: this.buildOnboardingMessage(userState, profileStatus, eligStatus),
|
||||
onboardingActions: this.buildOnboardingActions(userState, profileStatus, eligStatus),
|
||||
statusChecks: checks,
|
||||
banners, quickEntries,
|
||||
recommendations: recommendations || [],
|
||||
recState: (recommendations && recommendations.length > 0) ? 'loaded' : 'empty',
|
||||
serviceRecommendations, activities, serviceLocation,
|
||||
safetyNotice: safetyNotice || this.data.safetyNotice,
|
||||
})
|
||||
},
|
||||
|
||||
buildOnboardingMessage(state, profileStatus, eligStatus) {
|
||||
if (!state) return '登录后即可开始寻找合适的人'
|
||||
if (eligStatus === 'not_declared') return '完成本地资格声明后即可开始寻找合适的人'
|
||||
if (eligStatus === 'needs_evidence') return '补充本地资格佐证后即可开始寻找合适的人'
|
||||
if (profileStatus !== 'approved' && profileStatus !== 'completed') return '资料审核通过后即可开始寻找合适的人'
|
||||
return '完成资料后开始寻找合适的人'
|
||||
},
|
||||
|
||||
buildOnboardingActions(state, profileStatus, eligStatus) {
|
||||
if (!state) return [{ text: '去登录', event: 'login', variant: 'primary' }]
|
||||
const actions = []
|
||||
if (eligStatus === 'not_declared' || eligStatus === 'needs_evidence') {
|
||||
actions.push({ text: '声明本地资格', event: 'local-eligibility', variant: 'primary' })
|
||||
}
|
||||
if (profileStatus !== 'approved' && profileStatus !== 'completed') {
|
||||
actions.push({ text: '完善资料', event: 'profile', variant: 'secondary' })
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
actions.push({ text: '开始寻找', event: 'discover', variant: 'primary' })
|
||||
}
|
||||
return actions
|
||||
},
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
onBannerChange(e) {
|
||||
this.setData({ currentBannerIndex: e.detail.current })
|
||||
},
|
||||
|
||||
onBannerTap(e) {
|
||||
const { targetType, targetValue } = e.currentTarget.dataset.item
|
||||
if (targetType === 'activity' && targetValue) {
|
||||
wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${targetValue}` })
|
||||
} else if (targetType === 'matchmaker') {
|
||||
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
|
||||
} else if (targetType === 'service_location' && targetValue) {
|
||||
wx.navigateTo({ url: `/pages/local-eligibility/local-eligibility?from=service_station&id=${targetValue}` })
|
||||
}
|
||||
},
|
||||
|
||||
onEntryTap(e) {
|
||||
const { code, enabled, targetPage } = e.currentTarget.dataset.entry
|
||||
if (!enabled) {
|
||||
wx.showToast({ title: '完成资料后可使用此功能', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (targetPage.startsWith('pages/')) {
|
||||
wx.navigateTo({ url: targetPage })
|
||||
}
|
||||
},
|
||||
|
||||
onViewDetail(e) {
|
||||
const userId = e.detail?.userId || e.currentTarget.dataset?.userId
|
||||
if (userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${userId}` })
|
||||
},
|
||||
|
||||
onLike(e) {
|
||||
const userId = e.detail?.userId
|
||||
if (!userId) return
|
||||
this.setData({ actionLoadingUserId: userId })
|
||||
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
|
||||
},
|
||||
|
||||
onPass(e) {
|
||||
const userId = e.detail?.userId
|
||||
if (!userId) return
|
||||
this.setData({ actionLoadingUserId: userId })
|
||||
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
|
||||
},
|
||||
|
||||
onEmptyAction(e) {
|
||||
const event = e.currentTarget.dataset.event
|
||||
switch (event) {
|
||||
case 'login':
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
break
|
||||
case 'discover':
|
||||
wx.switchTab({ url: '/pages/discover/discover' })
|
||||
break
|
||||
case 'matchmaker':
|
||||
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
|
||||
break
|
||||
case 'preference':
|
||||
wx.navigateTo({ url: '/pages/profile-preference/profile-preference' })
|
||||
break
|
||||
case 'local-eligibility':
|
||||
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility' })
|
||||
break
|
||||
case 'profile':
|
||||
wx.navigateTo({ url: '/pages/profile-basic/profile-basic' })
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
onResume() {
|
||||
wx.showToast({ title: '功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
onSeeMore() { wx.switchTab({ url: '/pages/discover/discover' }) },
|
||||
onSeeAllActivities() { wx.switchTab({ url: '/pages/activity/activity' }) },
|
||||
onActivityTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (id) wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${id}` })
|
||||
},
|
||||
onStationTap() {
|
||||
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility?from=service_station' })
|
||||
},
|
||||
onServiceTap(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
switch (type) {
|
||||
case 'matchmaker': wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
|
||||
break
|
||||
case 'counseling':
|
||||
case 'premarital':
|
||||
wx.navigateTo({ url: `/pages/local-eligibility/local-eligibility?from=${type}` })
|
||||
break
|
||||
}
|
||||
},
|
||||
onSafetyTap() {
|
||||
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility?from=anti_fraud' })
|
||||
},
|
||||
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
|
||||
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return ''
|
||||
const d = new Date(timestamp)
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,214 @@
|
||||
<!-- U18 首页 - V1.1 新版结构 -->
|
||||
<view class="page">
|
||||
|
||||
<!-- ========== 品牌区 ========== -->
|
||||
<view class="brand-bar">
|
||||
<view class="brand-info">
|
||||
<text class="brand-name">善缘丹阳</text>
|
||||
<text class="brand-slogan">丹阳公益婚恋服务平台</text>
|
||||
</view>
|
||||
<view class="brand-actions">
|
||||
<view class="bell-btn" bindtap="onMessages">
|
||||
<text class="bell-icon">🔔</text>
|
||||
<block wx:if="{{unreadCount > 0}}">
|
||||
<text class="badge">{{unreadCount > 9 ? '9+' : unreadCount}}</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 用户状态条 ========== -->
|
||||
<view class="status-strip" wx:if="{{userState}}">
|
||||
<!-- 未完成准入 -->
|
||||
<block wx:if="{{isOnboarding}}">
|
||||
<view class="status-prompt">
|
||||
<text class="prompt-text">{{onboardingMessage}}</text>
|
||||
<view class="prompt-actions" wx:if="{{onboardingActions.length > 0}}">
|
||||
<view
|
||||
wx:for="{{onboardingActions}}"
|
||||
wx:key="event"
|
||||
class="prompt-btn {{item.variant}}"
|
||||
bindtap="onEmptyAction"
|
||||
data-event="{{item.event}}"
|
||||
>{{item.text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 已验证 -->
|
||||
<block wx:elif="{{isVerified}}">
|
||||
<view class="status-checks">
|
||||
<text class="status-check {{item.done ? 'done' : 'pending'}}"
|
||||
wx:for="{{statusChecks}}" wx:key="label">
|
||||
{{item.done ? '✓' : '○'}} {{item.label}}
|
||||
</text>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 已暂停 -->
|
||||
<block wx:elif="{{isPaused}}">
|
||||
<view class="paused-hint">
|
||||
<text>你已暂停新的对象推荐</text>
|
||||
<view class="resume-btn" bindtap="onResume">恢复寻找</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 已成功牵手 -->
|
||||
<block wx:elif="{{isSuccessful}}">
|
||||
<view class="successful-hint">
|
||||
<text>祝福你们正在认真了解彼此</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- ========== Banner 轮播 ========== -->
|
||||
<view class="banner-section" wx:if="{{banners.length > 0}}">
|
||||
<swiper
|
||||
class="banner-swiper"
|
||||
indicator-dots
|
||||
autoplay
|
||||
circular
|
||||
interval="4500"
|
||||
indicator-active-color="#E96B6B"
|
||||
bindchange="onBannerChange"
|
||||
>
|
||||
<swiper-item wx:for="{{banners}}" wx:key="id">
|
||||
<view class="banner-card" bindtap="onBannerTap" data-item="{{item}}">
|
||||
<image wx:if="{{item.imageFileId}}"
|
||||
src="{{item.imageFileId}}"
|
||||
mode="aspectFill"
|
||||
class="banner-img" />
|
||||
<view class="banner-overlay">
|
||||
<text class="banner-title">{{item.title}}</text>
|
||||
<text class="banner-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
<view class="banner-btn" wx:if="{{item.buttonText}}">{{item.buttonText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- ========== 8个核心入口 ========== -->
|
||||
<view class="entries-section">
|
||||
<view class="entries-grid">
|
||||
<view
|
||||
wx:for="{{quickEntries}}"
|
||||
wx:key="code"
|
||||
class="entry-item {{!item.enabled ? 'disabled' : ''}}"
|
||||
bindtap="onEntryTap"
|
||||
data-entry="{{item}}"
|
||||
>
|
||||
<view class="entry-icon-bg {{item.iconClass}}">
|
||||
<text class="entry-icon">{{item.icon}}</text>
|
||||
</view>
|
||||
<text class="entry-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 今日推荐 ========== -->
|
||||
<view class="section" wx:if="{{recommendations.length > 0 || recState === 'loading'}}">
|
||||
<ui-section-header title="今日推荐"
|
||||
subtitle="根据双方条件,为你筛选更合适的人"
|
||||
more="{{true}}"
|
||||
bind:more="onSeeMore" />
|
||||
<block wx:if="{{recState === 'loading' && recommendations.length === 0}}">
|
||||
<view class="skeleton-card" wx:for="{{3}}" wx:key="index">
|
||||
<view class="skeleton-avatar"></view>
|
||||
<view class="skeleton-info">
|
||||
<view class="skeleton-line w60"></view>
|
||||
<view class="skeleton-line w40"></view>
|
||||
<view class="skeleton-line w80"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{recommendations.length > 0}}">
|
||||
<view wx:for="{{recommendations}}" wx:key="userId" class="rec-card">
|
||||
<ui-recommend-card
|
||||
recommendation="{{item}}"
|
||||
action-loading="{{actionLoadingUserId === item.userId}}"
|
||||
bind:view="onViewDetail"
|
||||
bind:like="onLike"
|
||||
bind:pass="onPass"
|
||||
/>
|
||||
</view>
|
||||
</block>
|
||||
<view wx:if="{{recState === 'finished' && recommendations.length > 0}}" class="end-tip">
|
||||
已展示今日全部推荐 · 明天再来看看
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 无推荐空状态 ========== -->
|
||||
<view class="section" wx:if="{{!isOnboarding && recommendations.length === 0 && recState !== 'loading'}}">
|
||||
<ui-empty
|
||||
illustration="heart"
|
||||
title="暂时没有找到完全符合双方条件的人"
|
||||
description="你可以调整择偶条件,或申请公益红娘帮你推荐"
|
||||
actions="{{emptyActions}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- ========== 善缘公益服务 ========== -->
|
||||
<view class="section service-reco-section" wx:if="{{serviceRecommendations.length > 0}}">
|
||||
<ui-section-header title="善缘公益服务" />
|
||||
<view wx:for="{{serviceRecommendations}}" wx:key="type"
|
||||
class="service-card" bindtap="onServiceTap" data-type="{{item.type}}">
|
||||
<view class="service-card-inner">
|
||||
<text class="service-title">{{item.title}}</text>
|
||||
<text class="service-desc">{{item.description}}</text>
|
||||
<view class="service-cta">{{item.ctaText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 近期公益活动 ========== -->
|
||||
<view class="section activity-section" wx:if="{{activities.length > 0}}">
|
||||
<ui-section-header title="近期公益活动" more="{{true}}"
|
||||
bind:more="onSeeAllActivities" />
|
||||
<scroll-view scroll-x class="activity-scroll">
|
||||
<view wx:for="{{activities}}" wx:key="id"
|
||||
class="activity-h-card" bindtap="onActivityTap" data-id="{{item.id}}">
|
||||
<image wx:if="{{item.coverFileId}}" src="{{item.coverFileId}}"
|
||||
mode="aspectFill" class="activity-cover" />
|
||||
<view class="activity-info">
|
||||
<text class="activity-title">{{item.title}}</text>
|
||||
<text class="activity-meta" wx:if="{{item.startAt}}">{{formatDate(item.startAt)}}</text>
|
||||
<text class="activity-meta" wx:if="{{item.locationName}}">{{item.locationName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 善缘服务站 ========== -->
|
||||
<view class="section" wx:if="{{serviceLocation}}">
|
||||
<view class="station-card" bindtap="onStationTap">
|
||||
<view class="station-card-inner">
|
||||
<view class="station-icon-area">
|
||||
<text class="station-icon">📍</text>
|
||||
</view>
|
||||
<view class="station-info">
|
||||
<text class="station-name">{{serviceLocation.name}}</text>
|
||||
<text class="station-short">{{serviceLocation.shortName}}</text>
|
||||
<view class="station-services">
|
||||
<text wx:for="{{serviceLocation.services}}" wx:key="*this"
|
||||
class="station-tag">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="station-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 安全提醒 ========== -->
|
||||
<view class="safety-notice">
|
||||
<view class="safety-notice-inner">
|
||||
<text class="safety-icon">🛡</text>
|
||||
<view class="safety-text-area">
|
||||
<text class="safety-title">善缘提醒</text>
|
||||
<text class="safety-body">{{safetyNotice.text}}</text>
|
||||
</view>
|
||||
<text class="safety-cta" bindtap="onSafetyTap">{{safetyNotice.ctaText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区 -->
|
||||
<view class="safe-area-bottom"></view>
|
||||
</view>
|
||||
@@ -0,0 +1,246 @@
|
||||
/* U18 首页 - V1.1 新版样式 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #FAFAF8;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* ========== 品牌区 ========== */
|
||||
.brand-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx 16rpx;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.brand-info { display: flex; flex-direction: column; }
|
||||
|
||||
.brand-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #E96B6B;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: 22rpx;
|
||||
color: #888;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.bell-btn {
|
||||
position: relative;
|
||||
width: 64rpx; height: 64rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.bell-icon { font-size: 32rpx; }
|
||||
|
||||
.badge {
|
||||
position: absolute; top: -4rpx; right: -4rpx;
|
||||
min-width: 28rpx; height: 28rpx; line-height: 28rpx;
|
||||
padding: 0 6rpx;
|
||||
background: #E96B6B; color: #fff;
|
||||
font-size: 20rpx; border-radius: 14rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== 用户状态条 ========== */
|
||||
.status-strip {
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-checks { display: flex; gap: 24rpx; flex-wrap: wrap; }
|
||||
|
||||
.status-check {
|
||||
font-size: 24rpx; color: #666;
|
||||
}
|
||||
.status-check.done { color: #2E7D32; }
|
||||
.status-check.pending { color: #F57C00; }
|
||||
|
||||
.status-prompt { padding: 16rpx 0; }
|
||||
|
||||
.prompt-text {
|
||||
font-size: 26rpx; color: #555;
|
||||
display: block; margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.prompt-actions { display: flex; gap: 16rpx; }
|
||||
|
||||
.prompt-btn {
|
||||
padding: 12rpx 28rpx; border-radius: 8rpx;
|
||||
font-size: 26rpx; font-weight: 500;
|
||||
}
|
||||
.prompt-btn.primary { background: #E96B6B; color: #fff; }
|
||||
.prompt-btn.secondary { background: #fff; color: #E96B6B; border: 1rpx solid #E96B6B; }
|
||||
|
||||
.paused-hint, .successful-hint { padding: 16rpx 0; text-align: center; }
|
||||
.paused-hint text { font-size: 26rpx; color: #888; display: block; margin-bottom: 12rpx; }
|
||||
.resume-btn {
|
||||
display: inline-block; padding: 10rpx 32rpx;
|
||||
background: #E96B6B; color: #fff;
|
||||
border-radius: 8rpx; font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* ========== Banner ========== */
|
||||
.banner-section { padding: 16rpx 24rpx; }
|
||||
|
||||
.banner-swiper {
|
||||
width: 100%; height: 340rpx;
|
||||
border-radius: 16rpx; overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-card { width: 100%; height: 100%; position: relative; }
|
||||
.banner-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.6));
|
||||
color: #fff;
|
||||
}
|
||||
.banner-title { font-size: 30rpx; font-weight: 600; display: block; margin-bottom: 4rpx; }
|
||||
.banner-subtitle { font-size: 22rpx; opacity: 0.9; display: block; margin-bottom: 8rpx; }
|
||||
.banner-btn {
|
||||
display: inline-block; padding: 8rpx 20rpx;
|
||||
background: rgba(255,255,255,0.25); border: 1rpx solid rgba(255,255,255,0.6);
|
||||
border-radius: 20rpx; font-size: 22rpx; color: #fff;
|
||||
}
|
||||
|
||||
/* ========== 8个核心入口 ========== */
|
||||
.entries-section {
|
||||
background: #fff;
|
||||
margin: 0 24rpx; border-radius: 16rpx;
|
||||
padding: 24rpx 16rpx; margin-top: -8rpx;
|
||||
position: relative; z-index: 10;
|
||||
}
|
||||
|
||||
.entries-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.entry-item {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
.entry-item.disabled { opacity: 0.5; }
|
||||
|
||||
.entry-icon-bg {
|
||||
width: 72rpx; height: 72rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.entry-icon-bg.icon-dating { background: #FFF0F0; }
|
||||
.entry-icon-bg.icon-matchmaker { background: #FFF8E6; }
|
||||
.entry-icon-bg.icon-activity { background: #E8F5E9; }
|
||||
.entry-icon-bg.icon-counseling { background: #E3F2FD; }
|
||||
.entry-icon-bg.icon-class { background: #F3E5F5; }
|
||||
.entry-icon-bg.icon-premarital { background: #FFF3E0; }
|
||||
.entry-icon-bg.icon-shield { background: #E8EAF6; }
|
||||
.entry-icon-bg.icon-location { background: #E0F2F1; }
|
||||
|
||||
.entry-icon { font-size: 32rpx; }
|
||||
.entry-label { font-size: 22rpx; color: #333; text-align: center; line-height: 1.3; }
|
||||
|
||||
/* ========== 通用 Section ========== */
|
||||
.section { margin-top: 24rpx; padding: 0 24rpx; }
|
||||
|
||||
/* ========== 推荐卡片 ========== */
|
||||
.rec-card { margin-bottom: 16rpx; }
|
||||
.end-tip { text-align: center; font-size: 24rpx; color: #999; padding: 16rpx 0; }
|
||||
|
||||
/* ========== 骨架屏 ========== */
|
||||
.skeleton-card {
|
||||
display: flex; align-items: center;
|
||||
padding: 16rpx 0; margin-bottom: 12rpx;
|
||||
}
|
||||
.skeleton-avatar {
|
||||
width: 88rpx; height: 112rpx;
|
||||
background: #eee; border-radius: 8rpx; margin-right: 16rpx;
|
||||
}
|
||||
.skeleton-info { flex: 1; }
|
||||
.skeleton-line {
|
||||
height: 24rpx; background: #eee;
|
||||
border-radius: 4rpx; margin-bottom: 8rpx;
|
||||
}
|
||||
.skeleton-line.w60 { width: 60%; }
|
||||
.skeleton-line.w40 { width: 40%; }
|
||||
.skeleton-line.w80 { width: 80%; }
|
||||
|
||||
/* ========== 善缘公益服务 ========== */
|
||||
.service-card {
|
||||
background: linear-gradient(135deg, #FFF8F0 0%, #FFF0F0 100%);
|
||||
border-radius: 16rpx; padding: 24rpx; margin-bottom: 12rpx;
|
||||
}
|
||||
.service-card-inner { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.service-title { font-size: 28rpx; font-weight: 600; color: #333; }
|
||||
.service-desc { font-size: 24rpx; color: #666; line-height: 1.5; }
|
||||
.service-cta {
|
||||
display: inline-block; margin-top: 8rpx;
|
||||
padding: 8rpx 20rpx; background: #E96B6B;
|
||||
color: #fff; border-radius: 20rpx;
|
||||
font-size: 24rpx; width: fit-content;
|
||||
}
|
||||
|
||||
/* ========== 近期活动 ========== */
|
||||
.activity-scroll { white-space: nowrap; margin: 0 -4rpx; }
|
||||
.activity-h-card {
|
||||
display: inline-block; width: 480rpx; margin: 0 4rpx 12rpx;
|
||||
background: #fff; border-radius: 12rpx; overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
.activity-cover { width: 100%; height: 270rpx; background: #f0f0f0; }
|
||||
.activity-info { padding: 12rpx 16rpx; }
|
||||
.activity-title { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 4rpx; }
|
||||
.activity-meta { font-size: 22rpx; color: #888; display: block; }
|
||||
|
||||
/* ========== 善缘服务站 ========== */
|
||||
.station-card {
|
||||
background: #fff; border-radius: 16rpx; overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); margin-top: 24rpx;
|
||||
}
|
||||
.station-card-inner {
|
||||
display: flex; align-items: center; padding: 24rpx; gap: 16rpx;
|
||||
}
|
||||
.station-icon-area {
|
||||
width: 80rpx; height: 80rpx; background: #E8F5E9;
|
||||
border-radius: 16rpx; display: flex; align-items: center;
|
||||
justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.station-icon { font-size: 40rpx; }
|
||||
.station-info { flex: 1; min-width: 0; }
|
||||
.station-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
|
||||
.station-short { font-size: 22rpx; color: #888; display: block; margin-top: 2rpx; }
|
||||
.station-services { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 8rpx; }
|
||||
.station-tag {
|
||||
font-size: 20rpx; padding: 2rpx 12rpx;
|
||||
background: #F5F5F5; border-radius: 12rpx; color: #666;
|
||||
}
|
||||
.station-arrow { font-size: 36rpx; color: #ccc; flex-shrink: 0; }
|
||||
|
||||
/* ========== 安全提醒 ========== */
|
||||
.safety-notice {
|
||||
margin: 24rpx 24rpx 0;
|
||||
background: #FFF9E6; border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
.safety-notice-inner { display: flex; align-items: flex-start; gap: 12rpx; }
|
||||
.safety-icon { font-size: 32rpx; flex-shrink: 0; margin-top: 2rpx; }
|
||||
.safety-text-area { flex: 1; }
|
||||
.safety-title { font-size: 24rpx; font-weight: 600; color: #8B6914; display: block; margin-bottom: 4rpx; }
|
||||
.safety-body { font-size: 22rpx; color: #8B6914; line-height: 1.5; display: block; }
|
||||
.safety-cta { font-size: 24rpx; color: #E96B6B; flex-shrink: 0; padding-left: 12rpx; }
|
||||
|
||||
/* ========== 底部安全区 ========== */
|
||||
.safe-area-bottom { height: env(safe-area-inset-bottom); }
|
||||
@@ -0,0 +1,66 @@
|
||||
// pages/local-eligibility-supplement/local-eligibility-supplement.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "补充本地资格佐证",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 本地资格补充核验页 U63
|
||||
// 用户上传佐证材料(工作证明/居住证明等)
|
||||
|
||||
Page({
|
||||
data: {
|
||||
eligibilityType: '',
|
||||
evidenceTypes: [
|
||||
{ value: 'employment_proof', label: '工作证明', desc: '劳动合同/工牌/在职证明等' },
|
||||
{ value: 'residence_proof', label: '居住证明', desc: '房产证/租赁合同/水电账单等' },
|
||||
{ value: 'household_proof', label: '户籍证明', desc: '户口本/户籍页等' },
|
||||
{ value: 'other', label: '其他材料', desc: '能证明您在丹阳的材料' }
|
||||
],
|
||||
selectedType: '',
|
||||
uploadedFileIds: [] as string[]
|
||||
},
|
||||
|
||||
onLoad(options: Record<string, string>) {
|
||||
if (options.type) {
|
||||
this.setData({ eligibilityType: options.type })
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// TODO: P04-V11 加载当前用户的已上传佐证
|
||||
},
|
||||
|
||||
// 选择佐证类型
|
||||
onEvidenceTypeSelect(e: WechatMiniprogram.TouchEvent) {
|
||||
this.setData({ selectedType: e.currentTarget.dataset.type })
|
||||
},
|
||||
|
||||
// 上传图片
|
||||
onChooseImage() {
|
||||
wx.chooseMedia({
|
||||
count: 6,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
// TODO: P04-V11 上传到对象存储,获取fileId
|
||||
res.tempFiles.forEach(file => {
|
||||
console.log('选择的图片:', file.tempFilePath)
|
||||
})
|
||||
wx.showToast({ title: '上传成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提交审核
|
||||
onSubmit() {
|
||||
if (!this.data.selectedType) {
|
||||
wx.showToast({ title: '请选择佐证类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.data.uploadedFileIds.length === 0) {
|
||||
wx.showToast({ title: '请上传佐证材料', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// TODO: P04-V11 调用API提交
|
||||
wx.showLoading({ title: '提交中...' })
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已提交审核', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 删除已上传图片
|
||||
onRemoveImage(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const ids = this.data.uploadedFileIds
|
||||
ids.splice(index, 1)
|
||||
this.setData({ uploadedFileIds: ids })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- U63 本地资格补充核验页 -->
|
||||
<view class="page">
|
||||
<!-- 说明文案 -->
|
||||
<view class="header">
|
||||
<text class="title">为了完成本地资格核验</text>
|
||||
<text class="desc">
|
||||
请补充一项能够说明您在丹阳工作或居住的材料。
|
||||
材料仅用于资格审核,不对其他用户展示。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 已声明的资格类型 -->
|
||||
<view class="type-tag" wx:if="{{eligibilityType}}">
|
||||
<text>已声明:{{getEligibilityLabel(eligibilityType)}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 佐证类型选择 -->
|
||||
<view class="section">
|
||||
<text class="section-title">选择佐证类型</text>
|
||||
<view class="evidence-list">
|
||||
<view
|
||||
wx:for="{{evidenceTypes}}"
|
||||
wx:key="value"
|
||||
class="evidence-card {{selectedType === item.value ? 'active' : ''}}"
|
||||
bindtap="onEvidenceTypeSelect"
|
||||
data-type="{{item.value}}"
|
||||
>
|
||||
<text class="evidence-label">{{item.label}}</text>
|
||||
<text class="evidence-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<view class="section">
|
||||
<text class="section-title">上传佐证材料</text>
|
||||
<view class="upload-area">
|
||||
<view class="upload-hint">
|
||||
<text class="hint-text">支持 JPG/PNG,最多 6 张</text>
|
||||
</view>
|
||||
<view class="image-grid">
|
||||
<view
|
||||
wx:for="{{uploadedFileIds}}"
|
||||
wx:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image src="{{item}}" mode="aspectFill" class="image-preview" />
|
||||
<view class="image-remove" bindtap="onRemoveImage" data-index="{{index}}">×</view>
|
||||
</view>
|
||||
<view class="upload-btn" bindtap="onChooseImage">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 隐私提示 -->
|
||||
<view class="privacy-note">
|
||||
<text class="note-text">🔒 仅用于资格审核,不对其他用户展示</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="btn-primary" bindtap="onSubmit">
|
||||
提交审核
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,200 @@
|
||||
/* U63 本地资格补充核验页 */
|
||||
.page {
|
||||
padding: 24rpx;
|
||||
background: #FAFAF8;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
background: #E8F5E9;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.type-tag text {
|
||||
font-size: 24rpx;
|
||||
color: #2E7D32;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.evidence-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.evidence-card {
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid #eee;
|
||||
}
|
||||
|
||||
.evidence-card.active {
|
||||
border-color: #E96B6B;
|
||||
background: #FFF5F5;
|
||||
}
|
||||
|
||||
.evidence-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.evidence-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
position: relative;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-remove {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border: 2rpx dashed #ccc;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.privacy-note {
|
||||
background: #F0F7FF;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
font-size: 24rpx;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #E96B6B;
|
||||
color: #fff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user