init: initial commit

This commit is contained in:
2026-10-07 09:28:43 +08:00
commit a68a4ef411
251 changed files with 38279 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
const { getLayout } = require('../../utils/layout');
// pages/punch/punch.js - 情侣打卡
const { api, getImageUrl } = require('../../utils/request');
Page({
onResize: function() { this.setData(getLayout()); },
data: { loadError: false,
checkins: [],
coupleInfo: null,
myAvatar: '', partnerAvatar: '',
myName: '我', partnerName: 'TA',
weekBoth: 0, weekMy: 0, weekPartner: 0,
todayMy: 0, todayPartner: 0,
loading: true
},
onLoad: function() {
this.setData(getLayout());
this.loadCoupleInfo();
this.loadData();
},
onShow: function() { this.loadCoupleInfo(); this.loadData(); },
onPullDownRefresh: async function() {
await this.loadCoupleInfo();
await this.loadData();
wx.stopPullDownRefresh();
},
// 从接口加载情侣头像和名称
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({
coupleInfo: info,
myAvatar: getImageUrl(me.avatar || ''),
partnerAvatar: getImageUrl(partner.avatar || ''),
myName: me.nickname || '我',
partnerName: partner.nickname || 'TA'
});
},
loadData: async function() {
if (this._pageLoading) return;
this._pageLoading = true;
this.setData({loading:true, loadError:false});
try {
const [res, stats] = await Promise.all([
api.checkinList(),
api.checkinStats().catch(() => ({}))
]);
const list = (res.items || res || []).map(function(item) {
item.type = item.type || 'both';
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
item.type_label = typeLabels[item.type] || '双方';
item.frequency = item.frequency || 'daily';
var freqLabels = { daily: '每天', weekly: '每周', monthly: '每月', once: '一次', unlimited: '不限' };
item.freq_label = freqLabels[item.frequency] || '每天';
var periodLabels = { daily: '今日', weekly: '本周', monthly: '本月', once: '本次', unlimited: '' };
item.period_label = periodLabels[item.frequency] || '今日';
item.my_photo_url = item.my_photo ? getImageUrl(item.my_photo) : '';
item.partner_photo_url = item.partner_photo ? getImageUrl(item.partner_photo) : '';
// 打卡人头像
item.creator_avatar_url = item.creator_avatar ? getImageUrl(item.creator_avatar) : '';
item.my_checkin_avatar_url = item.my_checkin_avatar ? getImageUrl(item.my_checkin_avatar) : '';
item.partner_checkin_avatar_url = item.partner_checkin_avatar ? getImageUrl(item.partner_checkin_avatar) : '';
return item;
});
var data = {
checkins: list,
weekBoth: stats.week_both_count || 0,
weekMy: stats.week_my_count || 0,
weekPartner: stats.week_partner_count || 0,
todayMy: stats.today_my_count || 0,
todayPartner: stats.today_partner_count || 0,
loading: false
};
this.setData(data);
} catch(e) {
this.setData({loadError:true}); console.error(e); this.setData({ loading: false }); }
finally { this._pageLoading = false; this.setData({loading:false}); }
},
// ===== 点击卡片 → 详情弹窗 =====
openDetail: function(e) {
var idx = e.currentTarget.dataset.index;
var item = this.data.checkins[idx];
wx.navigateTo({ url: '/pages/checkin-detail/checkin-detail?id=' + item.id });
},
// ===== 跳转发布页 =====
showAddModal: function() { wx.navigateTo({ url: '/pages/publish/publish?type=checkin' }); },
// ===== 删除 =====
deleteCheckin: function(e) {
var id = e.currentTarget.dataset.id;
wx.showModal({
title: '确认删除', content: '确定要删除这个打卡项吗?',
success: async (res) => {
if (res.confirm) { try { await api.checkinDelete({ id: id }); wx.showToast({ title: '已删除', icon: 'success' }); this.loadData(); } catch(err) {} }
}
});
},
goBack: function() { wx.navigateBack(); },
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
});
+8
View File
@@ -0,0 +1,8 @@
{
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"usingComponents": {
"app-nav": "/components/app-nav/app-nav",
"page-state": "/components/page-state/page-state"
}
}
+97
View File
@@ -0,0 +1,97 @@
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
<launch-transition />
<!-- pages/punch/punch.wxml - 情侣打卡 -->
<app-nav title="打卡" back="{{true}}" />
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
<view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">LITTLE PROMISES</text><text class="j-page-title">把约定变成习惯</text></view><button class="j-button j-button-small" bindtap="showAddModal">+ 添加</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<!-- 顶部 Banner - 卡片式 -->
<view class="punch-banner-card" wx:if="{{!loading && !loadError}}">
<view class="promise-summary-head"><text class="c-small-title">一起坚持的小约定</text><view class="promise-avatars"><view class="banner-avatar-circle"><image wx:if="{{myAvatar}}" src="{{myAvatar}}" mode="aspectFill" /><text wx:else>我</text></view><view class="banner-avatar-circle"><image wx:if="{{partnerAvatar}}" src="{{partnerAvatar}}" mode="aspectFill" /><text wx:else>TA</text></view></view></view>
<view class="banner-stat-grid">
<view class="banner-stat-card">
<text class="stat-num">{{weekBoth}}</text>
<text class="stat-label">本周共同打卡</text>
</view>
<view class="banner-stat-card">
<text class="stat-num">{{todayMy}}</text>
<text class="stat-label">我今天</text>
</view>
<view class="banner-stat-card">
<text class="stat-num">{{todayPartner}}</text>
<text class="stat-label">TA今天</text>
</view>
</view>
</view>
<!-- 打卡列表 -->
<view class="checkin-cards" wx:if="{{!loading && !loadError && checkins.length > 0}}">
<view class="checkin-card" wx:for="{{checkins}}" wx:key="id" bindtap="openDetail" data-index="{{index}}" bindlongpress="deleteCheckin" data-id="{{item.id}}">
<!-- 主题 -->
<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">{{item.freq_label}}</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>
</view>
<page-state wx:if="{{!loading && !loadError && !checkins.length}}" title="从一个小约定开始" description="一起运动、按时吃饭,也可以成为日常仪式" action="添加打卡" bind:action="showAddModal" />
</view>
</scroll-view>
<!-- 浮动按钮 -->
</view>
+11
View File
@@ -0,0 +1,11 @@
@import '../../styles/common.wxss';
@import '../../styles/design.wxss';
@import '../../styles/journal.wxss';
@import '../../styles/companion.wxss';
.punch-banner-card { background:#F1EBE2; border:1rpx solid #E9DDD0; padding:28rpx; border-radius:16rpx; margin-bottom:32rpx; }
.promise-summary-head { display:flex; justify-content:space-between; align-items:center; gap:16rpx; margin-bottom:24rpx; }
.promise-avatars { display:flex; padding-right:8rpx; }.banner-avatar-circle { width:44rpx; height:44rpx; border-radius:50%; overflow:hidden; border:3rpx solid #F1EBE2; background:#F4E2E3; color:#943C58; font-size:20rpx; display:flex; align-items:center; justify-content:center; }.banner-avatar-circle + .banner-avatar-circle { margin-left:-8rpx; }.banner-avatar-circle image { width:100%; height:100%; }
.banner-stat-grid { display:flex; gap:10rpx; }.banner-stat-card { flex:1; display:flex; flex-direction:column; align-items:flex-start; border-right:1rpx solid #DFD2C3; padding-left:20rpx; }.banner-stat-card:first-child { padding-left:0; }.banner-stat-card:last-child { border:0; }
.stat-num { font-size:44rpx; line-height:1.2; font-weight:500; color:#943C58; }.stat-label { color:#85776D; font-size:21rpx; line-height:1.5; margin-top:8rpx; }
.checkin-cards { display:flex; flex-direction:column; gap:20rpx; }
.checkin-card:active { opacity:.8; }