init: initial commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/checkin-detail/checkin-detail.js
|
||||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
const { createSwipeBack } = require('../../utils/util');
|
||||
const { requestSubscribeAfterAction } = require('../../utils/subscribe');
|
||||
const COOLDOWN = 5000;
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false, item: null, logs: [], freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (id) {
|
||||
this.setData({ detailId: parseInt(id) });
|
||||
this.loadCoupleInfo();
|
||||
this.loadDetail(parseInt(id));
|
||||
}
|
||||
var self = this;
|
||||
var swipe = createSwipeBack(function() { wx.navigateBack(); });
|
||||
this.onTouchStart = swipe.onTouchStart;
|
||||
this.onTouchEnd = swipe.onTouchEnd;
|
||||
},
|
||||
|
||||
noop: function() {},
|
||||
retryDetail: function() { this.loadDetail(this.data.detailId); },
|
||||
onShow: function() {
|
||||
if (this.data.detailId) {
|
||||
this.loadCoupleInfo();
|
||||
this.loadDetail(this.data.detailId);
|
||||
}
|
||||
},
|
||||
|
||||
// 从接口加载情侣头像和名称
|
||||
loadCoupleInfo: function() {
|
||||
var self = this;
|
||||
api.coupleInfo().then(function(info) {
|
||||
self._applyCoupleInfo(info);
|
||||
}).catch(function(e) { console.error(e); });
|
||||
},
|
||||
|
||||
_applyCoupleInfo: function(info) {
|
||||
if (!info) return;
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var me = (p1.id == myUid) ? p1 : p2;
|
||||
var partner = (p1.id == myUid) ? p2 : p1;
|
||||
this.setData({
|
||||
myAvatar: getImageUrl(me.avatar || ''),
|
||||
partnerAvatar: getImageUrl(partner.avatar || ''),
|
||||
myName: me.nickname || '我',
|
||||
partnerName: partner.nickname || 'TA'
|
||||
});
|
||||
},
|
||||
|
||||
loadDetail: async function(id) {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
try {
|
||||
var data = await api.checkinDetail(id);
|
||||
var item = data.item || {};
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var freqLabels = { daily: '每天', weekly: '每周', monthly: '每月', once: '一次', unlimited: '不限' };
|
||||
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
|
||||
var freqLabel = freqLabels[item.frequency] || '每天';
|
||||
var typeLabel = typeLabels[item.type] || '双方';
|
||||
// 创建人头像URL
|
||||
item.creator_avatar_url = item.creator_avatar ? getImageUrl(item.creator_avatar) : '';
|
||||
// 使用本次页面加载的情侣信息
|
||||
var myAvatar = this.data.myAvatar;
|
||||
var partnerAvatar = this.data.partnerAvatar;
|
||||
var myName = this.data.myName;
|
||||
var partnerName = this.data.partnerName;
|
||||
var logs = (data.logs || []).map(function(l) {
|
||||
l.time = l.created_at ? l.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
l.photoUrl = l.photo ? getImageUrl(l.photo) : '';
|
||||
return l;
|
||||
});
|
||||
// 根据打卡权限类型映射统计数据
|
||||
var isCreator = (myUid == item.created_by);
|
||||
var showMyStats = (item.type == 'both') || (item.type == 'first');
|
||||
var showPartnerStats = (item.type == 'both') || (item.type == 'her');
|
||||
var checkinerIsMe = (item.type == 'first' && isCreator) || (item.type == 'her' && !isCreator);
|
||||
var displayAvatar = (item.type == 'both' || checkinerIsMe) ? myAvatar : partnerAvatar;
|
||||
var displayName = (item.type == 'both' || checkinerIsMe) ? myName : partnerName;
|
||||
var checkinToday = checkinerIsMe ? (data.my_today || 0) : (data.partner_today || 0);
|
||||
var checkinWeek = checkinerIsMe ? (data.my_week || 0) : (data.partner_week || 0);
|
||||
var checkinMonth = checkinerIsMe ? (data.my_month || 0) : (data.partner_month || 0);
|
||||
var checkinYear = checkinerIsMe ? (data.my_year || 0) : (data.partner_year || 0);
|
||||
var checkinStreak = checkinerIsMe ? (item.my_streak || 0) : (item.partner_streak || 0);
|
||||
var pageData = {
|
||||
item: item,
|
||||
logs: logs,
|
||||
freqLabel: freqLabel,
|
||||
typeLabel: typeLabel,
|
||||
myStreak: (item.type == 'both') ? (item.my_streak || 0) : checkinStreak,
|
||||
partnerStreak: (item.type == 'both') ? (item.partner_streak || 0) : 0,
|
||||
showMyStats: showMyStats,
|
||||
showPartnerStats: showPartnerStats,
|
||||
myToday: (item.type == 'both') ? (data.my_today || 0) : checkinToday,
|
||||
myWeek: (item.type == 'both') ? (data.my_week || 0) : checkinWeek,
|
||||
myMonth: (item.type == 'both') ? (data.my_month || 0) : checkinMonth,
|
||||
myYear: (item.type == 'both') ? (data.my_year || 0) : checkinYear,
|
||||
partnerToday: (item.type == 'both') ? (data.partner_today || 0) : 0,
|
||||
partnerWeek: (item.type == 'both') ? (data.partner_week || 0) : 0,
|
||||
partnerMonth: (item.type == 'both') ? (data.partner_month || 0) : 0,
|
||||
partnerYear: (item.type == 'both') ? (data.partner_year || 0) : 0,
|
||||
myAvatar: displayAvatar,
|
||||
myName: displayName,
|
||||
partnerAvatar: partnerAvatar,
|
||||
partnerName: partnerName
|
||||
};
|
||||
this.setData(pageData);
|
||||
} catch(e) {
|
||||
this.setData({loadError:true}); console.error(e); }
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
// ===== 打卡操作 =====
|
||||
showCheckinModal: function() {
|
||||
var now = Date.now();
|
||||
if (now - this.data.lastActionTime < COOLDOWN) {
|
||||
wx.showToast({ title: '操作太快,请稍后', icon: 'none' }); return;
|
||||
}
|
||||
this.data.lastActionTime = now;
|
||||
this.setData({ showCheckModal: true, checkPhoto: '', checkNote: '' });
|
||||
},
|
||||
|
||||
onCheckNoteInput: function(e) { this.setData({ checkNote: e.detail.value }); },
|
||||
|
||||
chooseCheckPhoto: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
|
||||
success: function(res) { self.setData({ checkPhoto: res.tempFilePaths[0] }); }
|
||||
});
|
||||
},
|
||||
|
||||
submitCheck: async function() {
|
||||
var item = this.data.item;
|
||||
if (!item) return;
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
requestSubscribeAfterAction('checkin');
|
||||
|
||||
try {
|
||||
var photoPath = '';
|
||||
if (this.data.checkPhoto) {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
try {
|
||||
var upResult = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: this.data.checkPhoto,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'checkins' }
|
||||
});
|
||||
photoPath = upResult.path || '';
|
||||
} catch(e) {
|
||||
// 内容安全违规:抛出错误中断打卡并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
photoPath = '';
|
||||
}
|
||||
wx.hideLoading();
|
||||
}
|
||||
var res = await api.checkinDo({ id: item.id, photo: photoPath, note: this.data.checkNote });
|
||||
this.setData({ showCheckModal: false });
|
||||
var toast = '打卡成功 ✅';
|
||||
if (item.type === 'both' && item.partner_done_today !== true && !res.both_done) {
|
||||
toast = '已打卡,等待对方';
|
||||
}
|
||||
wx.showToast({ title: toast, icon: 'success' });
|
||||
// 刷新详情数据
|
||||
this.loadDetail(item.id);
|
||||
} catch(e) { wx.showToast({ title: e.error || '打卡失败', icon: 'none' }); }
|
||||
},
|
||||
|
||||
hideCheckModal: function() { this.setData({ showCheckModal: false }); },
|
||||
|
||||
// ===== 图片预览 =====
|
||||
previewPhoto: function(e) {
|
||||
var url = e.currentTarget.dataset.url;
|
||||
if (url) {
|
||||
wx.previewImage({ urls: [url], current: url });
|
||||
}
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/checkin-detail/checkin-detail.wxml - 打卡详情 -->
|
||||
<app-nav title="打卡详情" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="c-body">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载详情" /><page-state wx:elif="{{loadError}}" kind="error" title="详情加载失败" action="重试" bind:action="retryDetail" />
|
||||
<block wx:else>
|
||||
|
||||
<!-- 打卡信息卡片(与列表卡片一致) -->
|
||||
<view class="detail-card" wx:if="{{item}}">
|
||||
<!-- 主题 -->
|
||||
<view class="card-row card-theme">
|
||||
<text class="card-icon" wx:if="{{item.icon && item.icon !== '✅'}}">{{item.icon}}</text><image wx:else class="card-icon" src="/images/check.svg" mode="aspectFit" />
|
||||
<text class="card-title">{{item.title}}</text>
|
||||
</view>
|
||||
<!-- 打卡频率 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">打卡频率</text>
|
||||
<text class="card-value">{{freqLabel}}</text>
|
||||
</view>
|
||||
<!-- 参与人(双人:双方昵称+头像 / 单人:对应昵称+头像) -->
|
||||
<view class="card-row" wx:if="{{item.type == 'both'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{myName ? myName.charAt(0) : '我'}}</text>
|
||||
<text class="chip-name">{{myName}}</text>
|
||||
</view>
|
||||
<text class="chip-sep">·</text>
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{partnerName ? partnerName.charAt(0) : 'TA'}}</text>
|
||||
<text class="chip-name">{{partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-row" wx:elif="{{item.type == 'first' || item.type == 'her'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{item.type == 'first' && myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<image wx:elif="{{item.type == 'her' && partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{item.type == 'first' ? (myName ? myName.charAt(0) : '我') : (partnerName ? partnerName.charAt(0) : 'TA')}}</text>
|
||||
<text class="chip-name">{{item.type == 'first' ? myName : partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 创建人 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">创建人</text>
|
||||
<view class="card-creator-info">
|
||||
<image wx:if="{{item.creator_avatar_url}}" class="creator-avatar" src="{{item.creator_avatar_url}}" mode="aspectFill" />
|
||||
<text wx:else class="creator-avatar-fallback">{{item.creator_name ? item.creator_name.charAt(0) : '?'}}</text>
|
||||
<text class="creator-name">{{item.creator_name || '未知'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 连续统计卡片 -->
|
||||
<view class="streak-section">
|
||||
<!-- 我的连续(根据权限控制显示) -->
|
||||
<view class="streak-card" wx:if="{{showMyStats}}">
|
||||
<view class="streak-header">
|
||||
<image wx:if="{{myAvatar}}" class="streak-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="streak-avatar-fallback">{{myName ? myName.charAt(0) : '我'}}</text>
|
||||
<text class="streak-user">{{myName}}</text>
|
||||
</view>
|
||||
<view class="streak-grid">
|
||||
<view class="streak-item"><text class="streak-num">{{myToday}}</text><text class="streak-label">当天</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myWeek}}</text><text class="streak-label">本周</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myMonth}}</text><text class="streak-label">本月</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myYear}}</text><text class="streak-label">今年</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- TA连续(根据权限控制显示) -->
|
||||
<view class="streak-card" wx:if="{{showPartnerStats}}">
|
||||
<view class="streak-header">
|
||||
<image wx:if="{{partnerAvatar}}" class="streak-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="streak-avatar-fallback">{{partnerName ? partnerName.charAt(0) : 'TA'}}</text>
|
||||
<text class="streak-user">{{partnerName}}</text>
|
||||
</view>
|
||||
<view class="streak-grid">
|
||||
<view class="streak-item"><text class="streak-num">{{partnerToday}}</text><text class="streak-label">当天</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerWeek}}</text><text class="streak-label">本周</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerMonth}}</text><text class="streak-label">本月</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerYear}}</text><text class="streak-label">今年</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 打卡按钮(独立居中,匹配全局UI) -->
|
||||
<button class="detail-checkin-btn" wx:if="{{item.can_checkin}}" bindtap="showCheckinModal" hover-class="none">记录这一次</button>
|
||||
|
||||
<!-- 打卡记录列表 -->
|
||||
<text class="section-title">打卡记录</text>
|
||||
<view class="log-list" wx:if="{{logs.length > 0}}">
|
||||
<view class="log-item" wx:for="{{logs}}" wx:key="id">
|
||||
<view class="log-dot" style="background:{{item.avatar ? '#B74768' : '#9C27B0'}};"></view>
|
||||
<view class="log-card">
|
||||
<view class="log-row">
|
||||
<text class="log-user">{{item.nickname || '我们'}}</text>
|
||||
<text class="log-time">{{item.time}}</text>
|
||||
</view>
|
||||
<text wx:if="{{item.note}}" class="log-note">{{item.note}}</text>
|
||||
<view wx:if="{{item.photo && item.photoUrl}}" class="log-photo-wrap" bindtap="previewPhoto" data-url="{{item.photoUrl}}">
|
||||
<image src="{{item.photoUrl}}" mode="aspectFill" class="log-photo" />
|
||||
<view class="log-photo-polaroid">
|
||||
<text class="polaroid-date">{{item.time}}</text>
|
||||
<text class="polaroid-icon">📸</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" wx:else>
|
||||
<text>📭</text>
|
||||
<text>暂无打卡记录</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 打卡弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showCheckModal}}" bindtap="hideCheckModal">
|
||||
<view class="modal" catchtap="noop" style="max-width:600rpx;">
|
||||
<text class="title">{{item.icon || '✅'}} {{item.title}}</text>
|
||||
<text class="modal-subtitle">记录这一刻</text>
|
||||
<view class="input-group"><label>备注</label><textarea class="input" style="min-height:100rpx;" placeholder="一句话..." value="{{checkNote}}" bindinput="onCheckNoteInput" maxlength="60" /></view>
|
||||
<view class="input-group"><label>照片</label>
|
||||
<view class="photo-upload-box" bindtap="chooseCheckPhoto">
|
||||
<image wx:if="{{checkPhoto}}" src="{{checkPhoto}}" mode="aspectFill" class="photo-upload-img" />
|
||||
<view wx:else class="photo-upload-placeholder"><text>+</text><text>添加照片</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<button class="btn btn-outline" bindtap="hideCheckModal" hover-class="none">取消</button>
|
||||
<button class="btn btn-primary" bindtap="submitCheck" hover-class="none">确认打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,20 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.streak-section { margin-top:28rpx; display:flex; flex-direction:column; gap:20rpx; }
|
||||
.streak-card { background:#FAF5EE; border-top:1rpx solid #E9DDD0; padding:24rpx 20rpx; border-radius:12rpx; }
|
||||
.streak-header { display:flex; align-items:center; gap:12rpx; margin-bottom:20rpx; }
|
||||
.streak-avatar,.streak-avatar-fallback { width:38rpx; height:38rpx; border-radius:50%; }.streak-avatar-fallback { background:#F4E2E3; color:#943C58; display:flex; align-items:center; justify-content:center; font-size:21rpx; }
|
||||
.streak-user { font-size:25rpx; font-weight:500; color:#352E2A; }.streak-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12rpx; }.streak-item { display:flex; flex-direction:column; align-items:center; gap:8rpx; }
|
||||
.streak-num { font-size:36rpx; font-weight:500; color:#943C58; }.streak-label { font-size:21rpx; color:#85776D; }
|
||||
.detail-checkin-btn { width:100%; margin:24rpx 0 40rpx; padding:0; height:84rpx; line-height:84rpx; border-radius:18rpx; background:#B74768; color:#FFFDF8; font-size:28rpx; font-weight:500; box-shadow:none; }.detail-checkin-btn::after { border:0; }
|
||||
.section-title { display:block; font-size:29rpx; font-weight:600; color:#352E2A; margin:36rpx 0 24rpx; }
|
||||
.log-list { border-left:1rpx solid #DFD2C3; margin-left:8rpx; padding-left:28rpx; }.log-item { position:relative; margin-bottom:24rpx; }
|
||||
.log-dot { position:absolute; left:-36rpx; top:24rpx; width:14rpx; height:14rpx; border:4rpx solid #FAF5EE; border-radius:50%; }
|
||||
.log-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:12rpx; padding:24rpx; }.log-row { display:flex; align-items:center; justify-content:space-between; gap:14rpx; flex-wrap:wrap; }.log-user { color:#352E2A; font-size:25rpx; font-weight:500; }.log-time { color:#A39386; font-size:20rpx; }
|
||||
.log-note { display:block; font-size:26rpx; line-height:1.7; color:#65594F; margin-top:14rpx; }
|
||||
.log-photo-wrap { padding:10rpx; margin-top:20rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; }.log-photo { width:100%; height:280rpx; display:block; }.log-photo-polaroid { display:none; }
|
||||
.empty-state { padding:64rpx 20rpx; font-size:25rpx; color:#85776D; text-align:center; }
|
||||
.modal-overlay { z-index:1200; }.modal .title { display:block; color:#352E2A; text-align:left; }.modal-subtitle { text-align:left; }.modal-actions { display:flex; gap:20rpx; margin-top:28rpx; }.modal-actions .btn { flex:1; min-width:0; padding:22rpx 12rpx; }
|
||||
.photo-upload-box { width:160rpx; height:160rpx; border:1rpx dashed #D5C5B3; border-radius:16rpx; overflow:hidden; background:#FAF5EE; }.photo-upload-img { width:100%; height:100%; }.photo-upload-placeholder { height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8rpx; color:#85776D; font-size:22rpx; }.photo-upload-placeholder text:first-child { font-size:44rpx; }
|
||||
Reference in New Issue
Block a user