更新最新代码
@@ -0,0 +1,161 @@
|
||||
// app.js
|
||||
const { api } = require('./utils/request');
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
authState: 'loading',
|
||||
authMessage: '',
|
||||
userId: null,
|
||||
token: null,
|
||||
isConnected: true,
|
||||
networkType: 'wifi'
|
||||
},
|
||||
|
||||
onLaunch: function() {
|
||||
this.removeLegacyDataCaches();
|
||||
this._authListeners = [];
|
||||
// 冷启动始终换取新 token,所有业务请求共享这一次登录。
|
||||
this.silentLogin().catch(function() {});
|
||||
this.checkNetwork();
|
||||
wx.onNetworkStatusChange((res) => {
|
||||
this.globalData.isConnected = res.isConnected;
|
||||
this.globalData.networkType = res.networkType;
|
||||
});
|
||||
},
|
||||
|
||||
onRelationChanged: function() {
|
||||
if (this._relationRedirect) return;
|
||||
wx.removeStorageSync('petHasPet');
|
||||
this._relationRedirect = true;
|
||||
setTimeout(() => { this._relationRedirect=false; wx.reLaunch({url:'/pages/index/index'}); }, 0);
|
||||
},
|
||||
onShow: function() {
|
||||
const sync = async () => {
|
||||
if (this._relationSync || this.globalData.authState !== 'ready') return;
|
||||
this._relationSync = true;
|
||||
const before = String(wx.getStorageSync('coupleId') || '');
|
||||
try {
|
||||
await api.checkAuth();
|
||||
if (before !== String(wx.getStorageSync('coupleId') || '')) {
|
||||
this.onRelationChanged();
|
||||
}
|
||||
} catch(e) {} finally { this._relationSync = false; }
|
||||
};
|
||||
sync();
|
||||
if (!this._relationTimer) this._relationTimer = setInterval(sync, 30000);
|
||||
},
|
||||
onHide: function() { if(this._relationTimer) clearInterval(this._relationTimer); this._relationTimer=null; },
|
||||
|
||||
setAuthState: function(state, message) {
|
||||
this.globalData.authState = state;
|
||||
this.globalData.authMessage = message || '';
|
||||
(this._authListeners || []).slice().forEach(function(listener) { listener(state, message || ''); });
|
||||
},
|
||||
observeAuth: function(listener) {
|
||||
if (!this._authListeners) this._authListeners = [];
|
||||
this._authListeners.push(listener);
|
||||
listener(this.globalData.authState, this.globalData.authMessage);
|
||||
return () => { this._authListeners = this._authListeners.filter(function(item) { return item !== listener; }); };
|
||||
},
|
||||
ensureAuth: function() {
|
||||
if (this._authPromise) return this._authPromise;
|
||||
if (this.globalData.authState === 'error') return Promise.reject({ error: this.globalData.authMessage, authFailed: true });
|
||||
if (this.globalData.authState === 'ready' && wx.getStorageSync('token')) return Promise.resolve();
|
||||
return this.silentLogin();
|
||||
},
|
||||
// 多个旧 token 请求同时返回 401 时,只刷新一次;较晚返回的旧请求复用新 token。
|
||||
refreshAuth: function(failedToken) {
|
||||
if (this._authPromise) return this._authPromise;
|
||||
if (this.globalData.authState === 'ready' && wx.getStorageSync('token') !== failedToken) return Promise.resolve();
|
||||
return this.silentLogin();
|
||||
},
|
||||
|
||||
removeLegacyDataCaches: function() {
|
||||
try {
|
||||
var keys = wx.getStorageInfoSync().keys || [];
|
||||
var exact = {
|
||||
indexCache: true, profileCache: true, discoverStatsCache: true,
|
||||
songsCache: true, albumsCache: true, diaryCache: true,
|
||||
timelineCache: true, timelineBgCache: true, checkinsCache: true,
|
||||
coupleInfoCache: true, petCache: true, petAchievementCache: true,
|
||||
period_cache_data: true, bg_cache: true, subscribe_status_cache: true,
|
||||
_albums_inited: true, myLocation: true
|
||||
};
|
||||
keys.forEach(function(key) {
|
||||
if (exact[key] || /^(offline_|albumCache_|diaryDetailCache_|timelineDetailCache_|checkinDetailCache_|historyCache_|period_cache_data:|timelineBg_)/.test(key)) {
|
||||
wx.removeStorageSync(key);
|
||||
}
|
||||
});
|
||||
} catch (e) { console.warn('清理旧数据缓存失败', e); }
|
||||
},
|
||||
|
||||
// ★ 获取用户位置(使用 wx.getFuzzyLocation 模糊定位)
|
||||
getLocationRegion: function() {
|
||||
return new Promise(function(resolve) {
|
||||
wx.getFuzzyLocation({
|
||||
type: 'wgs84',
|
||||
success: function(loc) {
|
||||
// 直接存储经纬度字符串,不依赖地图 API
|
||||
var region = loc.latitude.toFixed(4) + ',' + loc.longitude.toFixed(4);
|
||||
resolve(region);
|
||||
},
|
||||
fail: function(err) {
|
||||
console.log('获取模糊位置失败:', err);
|
||||
resolve('');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
silentLogin: function() {
|
||||
if (this._authPromise) return this._authPromise;
|
||||
this.setAuthState('loading');
|
||||
this._authPromise = this.performLogin().then((res) => {
|
||||
this.setAuthState('ready');
|
||||
return res;
|
||||
}).catch((error) => {
|
||||
wx.removeStorageSync('token');
|
||||
this.globalData.token = null;
|
||||
var failure = { error: (error && error.error) || '登录失败,请检查网络后重试', authFailed: true };
|
||||
this.setAuthState('error', failure.error);
|
||||
throw failure;
|
||||
}).finally(() => { this._authPromise = null; });
|
||||
return this._authPromise;
|
||||
},
|
||||
performLogin: async function() {
|
||||
var loginRes = await new Promise(function(resolve, reject) {
|
||||
var done = false;
|
||||
var timer = setTimeout(function() {
|
||||
if (!done) { done = true; reject({ error: '登录超时,请重试' }); }
|
||||
}, 10000);
|
||||
wx.login({
|
||||
success: function(res) { if (!done) { done = true; clearTimeout(timer); resolve(res); } },
|
||||
fail: function(err) { if (!done) { done = true; clearTimeout(timer); reject(err); } }
|
||||
});
|
||||
});
|
||||
if (!loginRes.code) throw { error: '未能获取微信登录凭证,请重试' };
|
||||
// 定位授权不阻塞登录,城市信息由页面后续的定位接口更新。
|
||||
var res = await api.wechatLogin(loginRes.code, '');
|
||||
if (!res || !res.token || !res.user_id) throw { error: '登录响应不完整,请重试' };
|
||||
wx.setStorageSync('token', res.token);
|
||||
wx.setStorageSync('userId', res.user_id);
|
||||
wx.setStorageSync('coupleId', res.couple_id || '');
|
||||
wx.setStorageSync('displayId', String(res.display_id || ''));
|
||||
wx.setStorageSync('nickname', res.nickname || '');
|
||||
wx.setStorageSync('region', res.region || '');
|
||||
wx.setStorageSync('hasLoggedIn', true);
|
||||
this.globalData.token = res.token;
|
||||
this.globalData.userId = res.user_id;
|
||||
return res;
|
||||
},
|
||||
|
||||
// 检查当前网络状态
|
||||
checkNetwork: function() {
|
||||
wx.getNetworkType({
|
||||
success: (res) => {
|
||||
this.globalData.isConnected = res.networkType !== 'none';
|
||||
this.globalData.networkType = res.networkType;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/discover/discover",
|
||||
"pages/timeline/timeline",
|
||||
"pages/punch/punch",
|
||||
"pages/diary/diary",
|
||||
"pages/albums/albums",
|
||||
"pages/album/album",
|
||||
"pages/lovewords/lovewords",
|
||||
"pages/settings/settings",
|
||||
"pages/history/history",
|
||||
"pages/profile/profile",
|
||||
"pages/publish/publish",
|
||||
"pages/checkin-detail/checkin-detail",
|
||||
"pages/timeline-detail/timeline-detail",
|
||||
"pages/period/period",
|
||||
"pages/unbind/unbind",
|
||||
"pages/diary_detail/diary_detail",
|
||||
"pages/diary_edit/diary_edit",
|
||||
"pages/pet/pet",
|
||||
"pages/pet/achievements",
|
||||
"pages/smoking-detail/smoking-detail"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FDE4EC",
|
||||
"backgroundColorTop": "#FDE4EC",
|
||||
"backgroundColorBottom": "#FFF7FA"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"permission": {
|
||||
"scope.userFuzzyLocation": {
|
||||
"desc": "你的位置信息将用于在个人主页展示所在城市"
|
||||
},
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于在发布日记或照片时标记地理位置"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos": [
|
||||
"getFuzzyLocation"
|
||||
],
|
||||
"usingComponents": {
|
||||
"launch-transition": "/components/launch-transition/launch-transition"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#8B827B",
|
||||
"selectedColor": "#B74768",
|
||||
"backgroundColor": "#FFF0F5",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "我们"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/discover/discover",
|
||||
"text": "记录"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/* ========================================
|
||||
全局设计系统 (Design Tokens)
|
||||
情侣小程序统一视觉规范
|
||||
======================================== */
|
||||
page {
|
||||
/* 主色板 */
|
||||
--primary: #B74768; /* 主色:玫瑰粉 */
|
||||
--primary-deep: #943C58; /* 深玫红 */
|
||||
--primary-light: #F4E2E3; /* 浅粉底 */
|
||||
--primary-gradient: linear-gradient(135deg, #B74768 0%, #D5A6AE 100%);
|
||||
--primary-gradient-deep: linear-gradient(135deg, #943C58 0%, #B74768 100%);
|
||||
|
||||
/* 辅助色 */
|
||||
--gold: #FFB84D; /* 金色:成就/倒计时 */
|
||||
--gold-light: #FFF3DF;
|
||||
--success: #2ECC71; /* 成功 */
|
||||
--success-light: #E8F9EF;
|
||||
|
||||
/* 中性色 */
|
||||
--text-main: #333333;
|
||||
--text-sub: #888888;
|
||||
--text-light: #BBBBBB;
|
||||
--bg: #FFF7FA;
|
||||
--bg-soft: #F3EBE1;
|
||||
--white: #FFFFFF;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 12rpx;
|
||||
--radius-md: 20rpx;
|
||||
--radius-lg: 32rpx;
|
||||
--radius-full: 999rpx;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 4rpx 16rpx rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 8rpx 30rpx rgba(255,107,129,0.10);
|
||||
--shadow-lg: 0 16rpx 48rpx rgba(255,107,129,0.18);
|
||||
|
||||
/* 间距 */
|
||||
--gap-sm: 16rpx;
|
||||
--gap-md: 24rpx;
|
||||
--gap-lg: 32rpx;
|
||||
|
||||
/* 过渡 */
|
||||
--transition: all 0.3s ease;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-main);
|
||||
background-color: var(--bg);
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 隐藏所有滚动条 */
|
||||
::-webkit-scrollbar { display: none; }
|
||||
scroll-view ::-webkit-scrollbar { display: none; }
|
||||
|
||||
|
||||
/* 通用容器 */
|
||||
.container {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: var(--white);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx 60rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--primary-color);
|
||||
border: 2rpx solid var(--primary-color);
|
||||
border-radius: 50rpx;
|
||||
padding: 22rpx 60rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.input-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #eee;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* Flex布局 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 文字 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* 间距 */
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.mt-40 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mb-40 {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
公共卡片类(设计系统)
|
||||
======================================== */
|
||||
.card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--gap-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.card-primary {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--gap-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* 渐变文字 */
|
||||
.text-gradient {
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* 胶囊标签 */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 22rpx;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* 图标圆底 */
|
||||
.icon-bubble {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
动效类(设计系统)
|
||||
======================================== */
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(30rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.06); }
|
||||
}
|
||||
|
||||
@keyframes floaty {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12rpx); }
|
||||
}
|
||||
|
||||
.anim-fade-up { animation: fadeUp 0.5s ease both; }
|
||||
.anim-fade-in { animation: fadeIn 0.6s ease both; }
|
||||
.anim-pulse { animation: pulse 2s ease-in-out infinite; }
|
||||
.anim-floaty { animation: floaty 3s ease-in-out infinite; }
|
||||
|
||||
/* 错峰入场:配合 .anim-fade-up 使用 */
|
||||
.delay-1 { animation-delay: 0.1s; }
|
||||
.delay-2 { animation-delay: 0.2s; }
|
||||
.delay-3 { animation-delay: 0.3s; }
|
||||
.delay-4 { animation-delay: 0.4s; }
|
||||
.delay-5 { animation-delay: 0.5s; }
|
||||
.delay-6 { animation-delay: 0.6s; }
|
||||
|
||||
/* 按压反馈 */
|
||||
.press {
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
.press:active {
|
||||
transform: scale(0.96);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
空状态(设计系统)
|
||||
======================================== */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 40rpx;
|
||||
}
|
||||
.empty-illustration {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 72rpx;
|
||||
margin-bottom: var(--gap-md);
|
||||
}
|
||||
.empty-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-sub);
|
||||
margin-bottom: var(--gap-lg);
|
||||
text-align: center;
|
||||
}
|
||||
.empty-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: var(--white);
|
||||
font-size: 28rpx;
|
||||
border-radius: var(--radius-full);
|
||||
padding: 20rpx 64rpx;
|
||||
border: none;
|
||||
}
|
||||
.empty-btn::after { border: none; }
|
||||
|
||||
@import './styles/typography.wxss';
|
||||
@@ -0,0 +1,12 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
Component({
|
||||
properties: { title: String, back: Boolean },
|
||||
data: { statusBarHeight: 20, barHeight: 44, navSide: 100 },
|
||||
lifetimes: { attached: function() { this.setData(getLayout()); } },
|
||||
pageLifetimes: { resize: function() { this.setData(getLayout()); } },
|
||||
methods: { goBack: function() {
|
||||
this.triggerEvent('back');
|
||||
if (getCurrentPages().length > 1) wx.navigateBack();
|
||||
else wx.switchTab({ url: '/pages/index/index' });
|
||||
} }
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1,6 @@
|
||||
<view class="nav" style="padding-top:{{statusBarHeight}}px;">
|
||||
<view class="bar" style="height:{{barHeight}}px;padding:0 {{navSide}}px;">
|
||||
<view wx:if="{{back}}" class="back" bindtap="goBack" aria-label="返回"><image src="/images/back.svg" /></view>
|
||||
<text class="title">{{title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,5 @@
|
||||
.nav{position:fixed;top:0;left:0;right:0;z-index:900;background:#FDE4EC;box-sizing:border-box}.bar{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box}.title{font-size:32rpx;font-weight:600;color:#352E2A;white-space:nowrap}.back{position:absolute;left:20rpx;top:0;bottom:0;width:80rpx;display:flex;align-items:center;justify-content:center}.back image{width:36rpx;height:36rpx}
|
||||
/* 二级页面导航标题也统一,组件隔离下需在组件内部设置字体。 */
|
||||
.title { font-family:'Kaiti SC','STKaiti','KaiTi',cursive; font-weight:400; font-style:normal; letter-spacing:1rpx; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,20 @@
|
||||
const { tabForPage, paths } = require('../../utils/navigation');
|
||||
Component({
|
||||
data: { active: '' },
|
||||
lifetimes: { attached: function() { this.syncActive(); } },
|
||||
pageLifetimes: { show: function() { this.syncActive(); } },
|
||||
methods: {
|
||||
syncActive: function() {
|
||||
var pages = getCurrentPages();
|
||||
var current = pages[pages.length - 1];
|
||||
this.setData({ active: tabForPage(current && current.route) });
|
||||
},
|
||||
navigate: function(e) {
|
||||
var tab = e.currentTarget.dataset.tab;
|
||||
if (!paths[tab]) return;
|
||||
var pages = getCurrentPages();
|
||||
// 当前主页面重复点击不重新加载。
|
||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
||||
wx.switchTab({ url: '/' + paths[tab] });
|
||||
}
|
||||
} });
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1,11 @@
|
||||
<view class="tabs" wx:if="{{active}}">
|
||||
<view class="tab {{active === 'home' ? 'active' : ''}}" data-tab="home" bindtap="navigate" hover-class="tab-pressed" aria-label="我们" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'home' ? '/components/app-tabbar/icons/us-active.svg' : '/components/app-tabbar/icons/us.svg'}}" mode="aspectFit" /></view><text class="tab-label">我们</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'records' ? 'active' : ''}}" data-tab="records" bindtap="navigate" hover-class="tab-pressed" aria-label="记录" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'records' ? '/components/app-tabbar/icons/journal-active.svg' : '/components/app-tabbar/icons/journal.svg'}}" mode="aspectFit" /></view><text class="tab-label">记录</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'profile' ? 'active' : ''}}" data-tab="profile" bindtap="navigate" hover-class="tab-pressed" aria-label="我的" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'profile' ? '/components/app-tabbar/icons/me-active.svg' : '/components/app-tabbar/icons/me.svg'}}" mode="aspectFit" /></view><text class="tab-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,43 @@
|
||||
.tabs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFF0F5;
|
||||
border-top: 1rpx solid #F3DCE5;
|
||||
padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 0;
|
||||
color: #8B827B;
|
||||
}
|
||||
.tab-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 54rpx;
|
||||
border-radius: 32rpx;
|
||||
background: transparent;
|
||||
transition: background-color .18s ease;
|
||||
}
|
||||
.tab-icon image { width: 48rpx; height: 48rpx; }
|
||||
.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; }
|
||||
.active { color: #B74768; }
|
||||
.active .tab-icon { background: #FADCE7; }
|
||||
.active .tab-label { font-weight: 600; }
|
||||
.tab-pressed .tab-icon { background: #FCE5ED; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7.5c-3.4-2-7.4-2-11-.7v16c3.6-1.3 7.6-1.3 11 .7 3.4-2 7.4-2 11-.7v-16c-3.6-1.3-7.6-1.3-11 .7Z"/><path d="M14 7.5v16M7 11h3.5M7 15h3.5M18 11h3M18 15h3"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7.5c-3.4-2-7.4-2-11-.7v16c3.6-1.3 7.6-1.3 11 .7 3.4-2 7.4-2 11-.7v-16c-3.6-1.3-7.6-1.3-11 .7Z"/><path d="M14 7.5v16M7 11h3.5M7 15h3.5M18 11h3M18 15h3"/></svg>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="14" cy="10" r="4.5"/><path d="M5 23v-1.4a6 6 0 0 1 6-6h6a6 6 0 0 1 6 6V23Z"/></svg>
|
||||
|
After Width: | Height: | Size: 253 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="14" cy="10" r="4.5"/><path d="M5 23v-1.4a6 6 0 0 1 6-6h6a6 6 0 0 1 6 6V23Z"/></svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 23S4 17.2 4 10.6A5.3 5.3 0 0 1 14 8a5.3 5.3 0 0 1 10 2.6C24 17.2 14 23 14 23Z"/><path d="M9 11.5c0-1.3.8-2.3 2-2.6"/></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 23S4 17.2 4 10.6A5.3 5.3 0 0 1 14 8a5.3 5.3 0 0 1 10 2.6C24 17.2 14 23 14 23Z"/><path d="M9 11.5c0-1.3.8-2.3 2-2.6"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
@@ -0,0 +1,56 @@
|
||||
Component({
|
||||
data: { visible: false, intro: false, state: 'loading', message: '' },
|
||||
lifetimes: {
|
||||
attached: function() {
|
||||
this._startedAt = Date.now();
|
||||
try { this._showIntro = !wx.getStorageSync('launchIntroSeen'); } catch (e) { this._showIntro = false; }
|
||||
if (this._showIntro) {
|
||||
this.setData({ visible: true, intro: true });
|
||||
// 设备首次展示即记住,跳转页面或再次启动都不会重复展示。
|
||||
try { wx.setStorageSync('launchIntroSeen', true); } catch (e) {}
|
||||
}
|
||||
this._unobserve = getApp().observeAuth((state, message) => {
|
||||
clearTimeout(this._exitTimer);
|
||||
if (state === 'ready') {
|
||||
this._retrying = false;
|
||||
if (this._showIntro) {
|
||||
this._exitTimer = setTimeout(() => {
|
||||
this._showIntro = false;
|
||||
this.setData({ visible: false, intro: false });
|
||||
}, Math.max(0, 350 - (Date.now() - this._startedAt)));
|
||||
} else {
|
||||
this.setData({ visible: false, intro: false });
|
||||
}
|
||||
} else if (state === 'error') {
|
||||
this._showIntro = false;
|
||||
this._retrying = true;
|
||||
this.setData({ visible: true, intro: false, state: state, message: message });
|
||||
} else {
|
||||
this.setData({ visible: !!(this._showIntro || this._retrying), intro: !!this._showIntro, state: state, message: message });
|
||||
}
|
||||
});
|
||||
},
|
||||
detached: function() {
|
||||
clearTimeout(this._exitTimer);
|
||||
if (this._unobserve) this._unobserve();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
preventMove: function() {},
|
||||
retry: async function() {
|
||||
try {
|
||||
await getApp().silentLogin();
|
||||
// 重新加载失败的页面,保留分享链接、相册 id 等原始入口参数。
|
||||
var pages = getCurrentPages();
|
||||
var page = pages[pages.length - 1];
|
||||
if (!page) return;
|
||||
var query = Object.keys(page.options || {}).map(function(key) {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(page.options[key]);
|
||||
}).join('&');
|
||||
wx.reLaunch({ url: '/' + page.route + (query ? '?' + query : '') });
|
||||
} catch (error) {
|
||||
// App 已发布失败状态,保留重试画面。
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1,16 @@
|
||||
<view wx:if="{{visible}}" class="launch-screen" catchtouchmove="preventMove">
|
||||
<view class="launch-content">
|
||||
<block wx:if="{{intro}}">
|
||||
<image class="launch-mark launch-animated" src="/images/share.png" mode="aspectFit" />
|
||||
<text class="launch-title">我们past</text>
|
||||
<text class="launch-caption">让所有的爱都有迹可循</text>
|
||||
</block>
|
||||
<view wx:if="{{state === 'loading'}}" class="launch-progress"><view></view><view></view><view></view></view>
|
||||
<block wx:else>
|
||||
<text class="launch-title">连接暂时没有成功</text>
|
||||
<text class="launch-error">{{message}}</text>
|
||||
<button class="launch-retry" bindtap="retry">重新连接</button>
|
||||
</block>
|
||||
</view>
|
||||
<text wx:if="{{intro}}" class="launch-footer">PAGES OF US</text>
|
||||
</view>
|
||||
@@ -0,0 +1,18 @@
|
||||
.launch-screen{position:fixed;top:0;right:0;bottom:0;left:0;z-index:99999;background:linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);display:flex;align-items:center;justify-content:center;color:#352E2A;}
|
||||
.launch-content{display:flex;align-items:center;flex-direction:column;width:100%;padding:0 60rpx;box-sizing:border-box;transform:translateY(-40rpx);}
|
||||
.launch-mark{display:block;width:152rpx;height:152rpx;line-height:152rpx;font-size:120rpx;text-align:center;}
|
||||
.launch-animated{animation:book-breathe 1.8s ease-in-out infinite;}
|
||||
.launch-title{font-size:44rpx;font-weight:600;letter-spacing:8rpx;margin:48rpx 0 18rpx;}
|
||||
.launch-caption{font-size:26rpx;color:#85776D;}
|
||||
.launch-progress{display:flex;gap:12rpx;margin-top:54rpx;}
|
||||
.launch-progress view{width:8rpx;height:8rpx;border-radius:50%;background:#B74768;animation:dot-breathe 1.2s ease-in-out infinite;}
|
||||
.launch-progress view:nth-child(2){animation-delay:.15s;}.launch-progress view:nth-child(3){animation-delay:.3s;}
|
||||
.launch-error{margin-top:42rpx;font-size:26rpx;line-height:1.6;text-align:center;color:#85776D;}
|
||||
.launch-retry{margin:28rpx 0 0;width:300rpx;background:#B74768;color:#FFFDF8;border-radius:18rpx;font-size:28rpx;line-height:84rpx;}.launch-retry::after{border:0;}
|
||||
.launch-footer{position:absolute;bottom:calc(56rpx + env(safe-area-inset-bottom));font-size:20rpx;letter-spacing:6rpx;color:#A39386;}
|
||||
@keyframes book-breathe{0%,100%{transform:translateY(0) rotate(-3deg);}50%{transform:translateY(-10rpx) rotate(1deg);}}
|
||||
@keyframes dot-breathe{0%,100%{opacity:.25;}50%{opacity:1;}}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1 @@
|
||||
Component({ properties: { kind: { type: String, value: 'empty' }, title: String, description: String, action: String }, methods: { act: function() { this.triggerEvent('action'); } } });
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="state state-{{kind}}"><view class="mark {{kind}}">{{kind === 'loading' ? '…' : kind === 'error' ? '!' : '♡'}}</view><text class="title">{{title}}</text><text wx:if="{{description}}" class="desc">{{description}}</text><button wx:if="{{action}}" bindtap="act">{{action}}</button></view>
|
||||
@@ -0,0 +1,8 @@
|
||||
.state{padding:64rpx 32rpx;display:flex;flex-direction:column;align-items:center;text-align:center;gap:16rpx}.mark{width:88rpx;height:88rpx;line-height:88rpx;border-radius:28rpx;background:#F4E2E3;color:#B74768;font-size:44rpx}.title{color:#352E2A;font-size:30rpx;font-weight:600}.desc{color:#85776D;font-size:26rpx;line-height:1.6}.state button{margin-top:12rpx;background:#B74768;color:#FFFDF8;font-size:26rpx;border-radius:20rpx;padding:0 32rpx}.state button:after{border:0}
|
||||
.state button{min-height:80rpx;line-height:1.4;display:flex;align-items:center;justify-content:center;padding:20rpx 40rpx;box-sizing:border-box;flex-shrink:0;min-width:220rpx}
|
||||
|
||||
.state-empty { background:#FFFDF8; border:1rpx dashed #E9DDD0; border-radius:12rpx 50rpx 12rpx 50rpx; margin:20rpx 0; }.mark { background:transparent; border-radius:0; }.state button { border-radius:26rpx; }.state .desc { font-size:24rpx; line-height:1.8; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,94 @@
|
||||
const { mergeUnique } = require('../../utils/list-pagination');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const cache = require('../../utils/data-cache');
|
||||
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
||||
Component({
|
||||
properties: { detailed: Boolean },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], logsPage:1, logsTotal:0, logsHasMore:false, logsLoadingMore:false, logsMoreError:false, ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
lifetimes: { attached(){this.load();} },
|
||||
pageLifetimes: { show(){this.load();} },
|
||||
methods: {
|
||||
async load(force) {
|
||||
if(this._loading) return this._loading;
|
||||
if(force)cache.invalidate(['smokingInfo']);
|
||||
this.setData({error:false});
|
||||
const target = this.data.viewUserId;
|
||||
this._loading=Promise.all([api.smokingInfo(target), api.coupleInfo().catch(()=>null)]).then(([info,profile])=>{if(target===this.data.viewUserId){this._profile=profile;this.applyInfo(info);}}).catch(()=>this.setData({error:true})).finally(()=>{this._loading=null;if(target===this.data.viewUserId)this.setData({loading:false});});
|
||||
return this._loading;
|
||||
},
|
||||
applyInfo(info) {
|
||||
this._logsGeneration = (this._logsGeneration || 0) + 1;
|
||||
const logs=(info.logs||[]).map(log=>({...log,undone:log.undone === true || Number(log.undone) === 1,title:titles[log.kind]||log.kind,change:Number(log.delta)>0?'+'+log.delta:String(log.delta),time:String(log.created_at||'').slice(5,16),money:Number(log.amount_cents)>0?(Number(log.amount_cents)/100).toFixed(2):''}));
|
||||
let lastLabel='还没有记录';
|
||||
if(info.last_time){const minutes=Math.max(0,Math.floor((Date.now()-new Date(String(info.last_time).replace(/-/g,'/')).getTime())/60000));lastLabel=minutes<60?'距上次 '+minutes+' 分钟':minutes<1440?'距上次 '+Math.floor(minutes/60)+' 小时':'距上次 '+Math.floor(minutes/1440)+' 天';}
|
||||
const map={};(info.daily||[]).forEach(row=>{map[row.day]=Number(row.count);});const daily=[];
|
||||
for(let ago=6;ago>=0;ago--){const day=new Date();day.setDate(day.getDate()-ago);const key=day.getFullYear()+'-'+String(day.getMonth()+1).padStart(2,'0')+'-'+String(day.getDate()).padStart(2,'0');daily.push({day:key,label:ago===0?'今天':(day.getMonth()+1)+'/'+day.getDate(),count:map[key]||0});}
|
||||
const max=Math.max(1,...daily.map(day=>day.count));daily.forEach(day=>day.height=Math.max(4,day.count/max*100));
|
||||
const periodStats=[{key:'week',title:'本周',range:'周一至今'},{key:'month',title:'本月',range:'本月1日至今'}].map(period=>({
|
||||
...period,
|
||||
count:info[period.key+'_count'] == null ? '—' : Number(info[period.key+'_count']),
|
||||
money:info[period.key+'_purchase_cents'] == null ? '—' : (Number(info[period.key+'_purchase_cents'])/100).toFixed(2)
|
||||
}));
|
||||
const profile=this._profile||{};
|
||||
const ownerId=Number(info.user_id || this.data.viewUserId || wx.getStorageSync('userId'));
|
||||
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
||||
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
||||
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
||||
this.setData({info,logs,daily,periodStats,logsPage:Number(info.logs_page)||1,logsTotal:Number(info.logs_total)||logs.length,logsHasMore:info.logs_has_more===true,logsLoadingMore:false,logsMoreError:false,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||
},
|
||||
async loadMoreLogs(){
|
||||
if(this._loading || this.data.loading || this.data.saving || this.data.logsLoadingMore || !this.data.logsHasMore)return;
|
||||
const generation=this._logsGeneration,target=this.data.viewUserId,page=this.data.logsPage+1;
|
||||
this.setData({logsLoadingMore:true,logsMoreError:false});
|
||||
try{
|
||||
const info=await api.smokingInfo(target,{page,page_size:20});
|
||||
if(generation!==this._logsGeneration || target!==this.data.viewUserId)return;
|
||||
this.applyInfo({...info,logs:mergeUnique(this.data.info.logs||[],info.logs||[])});
|
||||
}catch(e){if(generation===this._logsGeneration)this.setData({logsMoreError:true});}
|
||||
finally{if(generation===this._logsGeneration)this.setData({logsLoadingMore:false});}
|
||||
},
|
||||
avatarFailed(){this.setData({ownerAvatar:''});},
|
||||
retry(){this.load(true);},
|
||||
openDetail(){if(!this.properties.detailed)wx.navigateTo({url:'/pages/smoking-detail/smoking-detail'});},
|
||||
async switchOwner(e){if(this.data.saving)return;this._logsGeneration=(this._logsGeneration||0)+1;this.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
replenish(){if(this.data.saving)return;this.setData({sheet:true,mode:'purchase',quantity:'1',packSize:'20',amount:'',note:''});},
|
||||
mode(e){if(this.data.saving)return;const mode=e.currentTarget.dataset.mode;this.setData({mode,quantity:mode==='adjust'?String(this.data.info.stock):'1',amount:'',note:''});},
|
||||
input(e){if(!this.data.saving)this.setData({[e.currentTarget.dataset.field]:e.detail.value});},
|
||||
close(){if(!this.data.saving)this.setData({sheet:false});},
|
||||
preventMove(){},
|
||||
smoke(){return this.act({kind:'consume',quantity:1});},
|
||||
receiveSmoke(){return this.act({kind:'receive_smoke',quantity:1});},
|
||||
giveSmoke(){
|
||||
if(this.data.saving || !this.data.info || this.data.info.read_only || !this.data.info.available)return;
|
||||
if(Number(this.data.info.stock)<1)return wx.showToast({title:'库存不足,请先补充库存',icon:'none'});
|
||||
return this.act({kind:'give',quantity:1});
|
||||
},
|
||||
submit() {
|
||||
const mode=this.data.mode;
|
||||
const quantityText=this.data.quantity.trim(),packText=this.data.packSize.trim();
|
||||
if(!/^\d+$/.test(quantityText)||(mode==='purchase'&&!/^\d+$/.test(packText)))return wx.showToast({title:'请填写整数数量',icon:'none'});
|
||||
let quantity=Number(quantityText);if(mode==='purchase'){if(Number(packText)<1||Number(packText)>100)return wx.showToast({title:'每包数量应为1到100支',icon:'none'});quantity*=Number(packText);}
|
||||
if(quantity>(mode==='adjust'?1000000:10000)||(mode!=='adjust'&&quantity<1))return wx.showToast({title:'数量超出范围',icon:'none'});
|
||||
const money=this.data.amount.trim();if(mode==='purchase'&&money&&!/^\d+(\.\d{1,2})?$/.test(money))return wx.showToast({title:'金额最多两位小数',icon:'none'});
|
||||
const amount=mode==='purchase'?Math.round(Number(money||0)*100):0;
|
||||
if(amount>100000000)return wx.showToast({title:'金额超出范围',icon:'none'});
|
||||
return this.act({kind:mode,quantity,amount_cents:amount,note:this.data.note.trim()});
|
||||
},
|
||||
undo(e){if(this.data.saving)return;wx.showModal({title:'撤销这条记录?',content:'恢复该操作前的库存变化和抽烟次数。赠出一支仅扣库存,撤销会加回一支;“收到并抽一支”会同时撤销收到和抽烟。',success:res=>{if(res.confirm)this.act({kind:'undo',original_id:Number(e.currentTarget.dataset.id),quantity:0});}});},
|
||||
async act(payload) {
|
||||
if(this.data.saving||!this.data.info||this.data.info.read_only||!this.data.info.available)return;
|
||||
const signature=JSON.stringify(payload);
|
||||
if(!this._pendingAction||this._pendingAction.signature!==signature)this._pendingAction={signature,key:'smoke-'+Date.now()+'-'+Math.random().toString(36).slice(2)};
|
||||
this.setData({saving:true});
|
||||
try {
|
||||
const result=await api.smokingAction({...payload,request_key:this._pendingAction.key});
|
||||
if(!result.success)throw result;
|
||||
this._pendingAction=null;this.applyInfo(result.info);this.setData({sheet:false});this.triggerEvent('updated');wx.showToast({title:'已记录',icon:'none'});
|
||||
} catch(error) {
|
||||
// 明确的业务拒绝不会提交;网络不确定时保留请求标识,重试不会重复扣减。
|
||||
if(error&&error._statusCode&&error._statusCode!==500)this._pendingAction=null;
|
||||
wx.showToast({title:error&&error.error||'请求失败,请重试',icon:'none'});
|
||||
} finally {this.setData({saving:false});}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true,"usingComponents":{}}
|
||||
@@ -0,0 +1,30 @@
|
||||
<view class="smoke-card">
|
||||
<view class="smoke-head" bindtap="openDetail"><view><text class="smoke-eyebrow">默认事件 · 按支记录</text><text class="smoke-title">抽烟记录</text></view><text class="smoke-link" wx:if="{{!detailed}}">明细 ›</text></view>
|
||||
<view class="owners" wx:if="{{detailed && info && info.partner_id}}"><view class="owner {{!viewUserId ? 'selected' : ''}}" data-user="0" bindtap="switchOwner">我的记录</view><view class="owner {{viewUserId ? 'selected' : ''}}" data-user="{{info.partner_id}}" bindtap="switchOwner">TA的记录</view></view>
|
||||
<view wx:if="{{loading}}" class="smoke-hint">正在加载记录…</view>
|
||||
<view wx:elif="{{error}}" class="smoke-hint" bindtap="retry">记录加载失败,点击重试</view>
|
||||
<block wx:elif="{{info}}">
|
||||
<view class="smoke-stats"><view><text class="smoke-number">{{info.today_count}}<text>支</text></text><text class="smoke-label">今日已抽</text></view><view><text class="smoke-number">{{info.stock}}<text>支</text></text><text class="smoke-label">剩余库存</text></view></view>
|
||||
<text class="smoke-hint">{{info.read_only ? info.owner_name + ' · 仅查看' : lastLabel}}</text>
|
||||
<text class="smoke-hint" wx:if="{{!info.available}}">绑定伴侣后可记录个人库存和打卡</text>
|
||||
<block wx:elif="{{!info.read_only}}">
|
||||
<view class="smoke-actions"><button class="smoke-primary" disabled="{{saving || info.stock < 1}}" bindtap="smoke">抽一支,打卡</button><button class="smoke-secondary" disabled="{{saving}}" bindtap="replenish">+ 补充库存</button></view>
|
||||
<view class="smoke-inline-actions"><view class="smoke-inline" bindtap="receiveSmoke">{{saving ? '正在保存…' : '别人递来一支,当场抽掉 →'}}</view><view class="smoke-inline smoke-give {{saving || info.stock < 1 ? 'smoke-give-disabled' : ''}}" bindtap="giveSmoke">递给别人一支 →</view></view>
|
||||
<view class="smoke-hint" wx:if="{{info.stock < 1}}">还没有库存,先录入已有的烟或补充库存</view>
|
||||
</block>
|
||||
<view wx:if="{{!detailed && logs.length && !info.read_only}}" class="smoke-recent"><text>{{logs[0].title}} · {{logs[0].time}}</text><text wx:if="{{!logs[0].undone && logs[0].kind !== 'undo'}}" data-id="{{logs[0].id}}" bindtap="undo">撤销</text></view>
|
||||
<block wx:if="{{detailed}}">
|
||||
<view class="section-title">抽烟与购买统计</view>
|
||||
<view class="smoke-periods"><view class="smoke-period" wx:for="{{periodStats}}" wx:key="key"><view class="smoke-period-head"><text>{{item.title}}</text><text>{{item.range}}</text></view><view class="smoke-period-row"><text>已抽</text><text>{{item.count}} 支</text></view><view class="smoke-period-row"><text>买烟金额</text><text>¥{{item.money}}</text></view></view></view>
|
||||
<text class="smoke-period-hint">购买金额按已填写金额累计,已撤销记录不计入</text>
|
||||
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view>
|
||||
<view class="week-chart"><view class="chart-day" wx:for="{{daily}}" wx:key="day"><text>{{item.count}}</text><view class="chart-track"><view class="chart-bar" style="height:{{item.height}}%" /></view><text>{{item.label}}</text></view></view>
|
||||
<view class="section-title">库存与打卡流水<text>共 {{logsTotal}} 条</text></view>
|
||||
<view class="smoke-hint" wx:if="{{!logs.length}}">从补充库存开始,记录每一次消耗</view>
|
||||
<view class="ledger {{item.undone ? 'undone' : ''}}" wx:for="{{logs}}" wx:key="id"><view class="ledger-avatar"><image wx:if="{{ownerAvatar}}" src="{{ownerAvatar}}" mode="aspectFill" binderror="avatarFailed" /><text wx:else>{{ownerInitial}}</text></view><view class="ledger-main"><view class="ledger-owner">{{ownerName}}</view><text>{{item.title}}{{item.undone ? ' · 已撤销' : ''}}</text><text>{{item.time}}{{item.money ? ' · ¥' + item.money : ''}}</text><text wx:if="{{item.note}}">{{item.note}}</text></view><view class="ledger-right"><text>{{item.change}} 支 · 余 {{item.balance}}</text><text wx:if="{{!info.read_only && !item.undone && item.kind !== 'undo'}}" data-id="{{item.id}}" bindtap="undo">撤销</text></view></view>
|
||||
<view class="smoke-hint smoke-ledger-footer" wx:if="{{logs.length}}"><text wx:if="{{logsLoadingMore}}">正在加载…</text><text wx:elif="{{logsMoreError}}" bindtap="loadMoreLogs">加载失败,点击重试</text><text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多流水</text><text wx:else>已加载全部流水</text></view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<view class="smoke-mask" wx:if="{{sheet}}" catchtouchmove="preventMove"><view class="smoke-backdrop" bindtap="close" /><view class="smoke-sheet"><view class="sheet-grip" /><view class="sheet-head"><text>补充与调整库存</text><text bindtap="close">关闭</text></view><view class="sheet-tabs"><view class="{{mode === 'purchase' ? 'selected' : ''}}" data-mode="purchase" bindtap="mode">买烟</view><view class="{{mode === 'receive' ? 'selected' : ''}}" data-mode="receive" bindtap="mode">别人递烟</view><view class="{{mode === 'adjust' ? 'selected' : ''}}" data-mode="adjust" bindtap="mode">调整库存</view></view>
|
||||
<scroll-view class="sheet-body" scroll-y><text class="field-label">{{mode === 'purchase' ? '购买包数' : (mode === 'adjust' ? '盘点后实际剩余(支)' : '收到数量(支)')}}</text><input class="smoke-input" type="number" value="{{quantity}}" data-field="quantity" bindinput="input" disabled="{{saving}}" /><block wx:if="{{mode === 'purchase'}}"><text class="field-label">每包支数</text><input class="smoke-input" type="number" value="{{packSize}}" data-field="packSize" bindinput="input" disabled="{{saving}}" /><text class="field-label">购买金额(选填,元)</text><input class="smoke-input" type="digit" value="{{amount}}" data-field="amount" bindinput="input" disabled="{{saving}}" /></block><text class="field-label">备注(选填)</text><input class="smoke-input" value="{{note}}" maxlength="200" data-field="note" bindinput="input" disabled="{{saving}}" /><text class="smoke-hint">{{mode === 'adjust' ? '库存将调整为填写的数量,不计入抽烟打卡。' : '只增加库存,不计入抽烟打卡。'}}</text></scroll-view><button class="smoke-primary sheet-save" loading="{{saving}}" disabled="{{saving}}" bindtap="submit">{{mode === 'adjust' ? '确认调整' : '确认补充'}}</button></view></view>
|
||||
@@ -0,0 +1,18 @@
|
||||
.smoke-card{padding:32rpx;border:1rpx solid #F1DCE4;border-radius:28rpx;background:#FFFCFD;margin-bottom:28rpx;color:#352E2A}.smoke-head{display:flex;justify-content:space-between;align-items:center}.smoke-eyebrow{display:block;font-size:21rpx;color:#A39386;margin-bottom:10rpx}.smoke-title{font-size:33rpx;font-weight:600}.smoke-link{font-size:24rpx;color:#B74768}.smoke-stats{display:flex;margin:30rpx 0 20rpx}.smoke-stats>view{flex:1}.smoke-stats>view+view{padding-left:32rpx;border-left:1rpx solid #F1DCE4}.smoke-number{display:block;font-size:54rpx;color:#B74768;line-height:1.2}.smoke-number text{font-size:23rpx;margin-left:10rpx;color:#A39386}.smoke-label{display:block;font-size:23rpx;color:#85776D;margin-top:10rpx}.smoke-hint{display:block;font-size:22rpx;color:#A39386;line-height:1.7;margin:12rpx 0}.smoke-actions{display:flex;gap:16rpx;margin-top:24rpx}.smoke-primary,.smoke-secondary{flex:1;margin:0;border-radius:20rpx;font-size:25rpx;height:88rpx;min-height:88rpx;padding:0;display:flex;align-items:center;justify-content:center;line-height:normal;box-sizing:border-box}.smoke-primary{background:#B74768;color:#FFF}.smoke-secondary{background:#FFF0F5;color:#B74768;border:1rpx solid #F1DCE4}button::after{border:0}.smoke-primary[disabled]{background:#F0E2E8;color:#A39386}.smoke-inline{font-size:22rpx;color:#B74768;padding:24rpx 0 10rpx}.smoke-recent{display:flex;justify-content:space-between;gap:16rpx;border-top:1rpx solid #F1DCE4;margin-top:20rpx;padding-top:20rpx;font-size:22rpx;color:#A39386}.smoke-recent text+text{color:#B74768}.owners,.sheet-tabs{display:flex;gap:10rpx;margin:24rpx 0}.owner,.sheet-tabs>view{flex:1;text-align:center;background:#FFF5F8;color:#85776D;padding:18rpx 8rpx;border-radius:16rpx;font-size:24rpx}.owners .selected,.sheet-tabs .selected{background:#F4E2E3;color:#B74768;font-weight:600}.section-title{font-size:27rpx;font-weight:600;display:flex;justify-content:space-between;margin:40rpx 0 22rpx}.section-title text{font-size:20rpx;color:#A39386;font-weight:400}.week-chart{display:flex;gap:10rpx}.chart-day{flex:1;text-align:center;font-size:20rpx;color:#A39386}.chart-track{height:110rpx;display:flex;align-items:flex-end;justify-content:center;margin:12rpx 0}.chart-bar{width:24rpx;border-radius:8rpx;background:#D5A6AE}.ledger{display:flex;justify-content:space-between;gap:20rpx;padding:24rpx 0;border-top:1rpx solid #F1DCE4;font-size:24rpx}.ledger-main{flex:1;min-width:0}.ledger-main text{display:block;overflow-wrap:break-word}.ledger-main text+text{font-size:21rpx;color:#A39386;margin-top:8rpx}.ledger-right{text-align:right;font-size:22rpx}.ledger-right text{display:block}.ledger-right text+text{color:#B74768;padding-top:14rpx}.undone{opacity:.55}.smoke-mask{position:fixed;inset:0;z-index:1300;display:flex;align-items:flex-end}.smoke-backdrop{position:absolute;inset:0;background:rgba(53,46,42,.35)}.smoke-sheet{position:relative;width:100%;max-height:90vh;background:#FFFCFD;border-radius:40rpx 40rpx 0 0;padding:16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));box-sizing:border-box}.sheet-grip{width:64rpx;height:7rpx;background:#EBD5DE;border-radius:8rpx;margin:0 auto 26rpx}.sheet-head{display:flex;justify-content:space-between;font-size:30rpx;font-weight:600}.sheet-head text+text{font-size:24rpx;color:#B74768;font-weight:400}.sheet-body{height:600rpx;max-height:52vh}.field-label{display:block;font-size:24rpx;color:#85776D;margin:20rpx 0 12rpx}.smoke-input{height:84rpx;width:100%;background:#FFF5F8;border:1rpx solid #F1DCE4;border-radius:16rpx;padding:0 24rpx;box-sizing:border-box;font-size:27rpx;color:#352E2A}.sheet-save{width:100%;margin-top:24rpx}
|
||||
|
||||
.ledger-avatar{width:54rpx;height:54rpx;flex-shrink:0;border-radius:50%;overflow:hidden;background:#FADCE7;color:#B74768;display:flex;align-items:center;justify-content:center;font-size:24rpx}
|
||||
.ledger-avatar image{width:100%;height:100%}
|
||||
.ledger-owner{font-size:24rpx;color:#64424D;margin-bottom:10rpx;overflow-wrap:break-word}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
.smoke-inline-actions{display:flex;align-items:center;gap:20rpx}
|
||||
.smoke-inline-actions .smoke-inline:first-child{flex:1;min-width:0}
|
||||
.smoke-give{flex-shrink:0;text-align:right}
|
||||
.smoke-give-disabled{color:#A39386;opacity:.65}
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
|
||||
|
||||
.smoke-ledger-footer{text-align:center;padding-top:20rpx}
|
||||
@@ -0,0 +1,23 @@
|
||||
const { tabForPage, paths } = require('../utils/navigation');
|
||||
Component({
|
||||
data: { active: '', hidden: false },
|
||||
lifetimes: { attached: function() { this.syncActive(); } },
|
||||
pageLifetimes: { show: function() { this.syncActive(); } },
|
||||
methods: {
|
||||
setHidden: function(hidden) { this.setData({ hidden: !!hidden }); },
|
||||
syncActive: function() {
|
||||
var pages = getCurrentPages();
|
||||
var current = pages[pages.length - 1];
|
||||
this.setData({ active: tabForPage(current && current.route), hidden: !!(current && current.data && (current.data.publishOpen || current.data.showEditProfileModal || current.data.showEditPartnerModal)) });
|
||||
},
|
||||
navigate: function(e) {
|
||||
var tab = e.currentTarget.dataset.tab;
|
||||
if (!paths[tab]) return;
|
||||
var pages = getCurrentPages();
|
||||
// 当前主页面重复点击不重新加载。
|
||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
||||
if (this._switching) return;
|
||||
this._switching = true;
|
||||
wx.switchTab({ url: '/' + paths[tab], complete: () => { this._switching = false; } });
|
||||
}
|
||||
} });
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1,11 @@
|
||||
<view class="tabs" wx:if="{{active && !hidden}}">
|
||||
<view class="tab {{active === 'home' ? 'active' : ''}}" data-tab="home" bindtap="navigate" hover-class="tab-pressed" aria-label="我们" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'home' ? '/components/app-tabbar/icons/us-active.svg' : '/components/app-tabbar/icons/us.svg'}}" mode="aspectFit" /></view><text class="tab-label">我们</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'records' ? 'active' : ''}}" data-tab="records" bindtap="navigate" hover-class="tab-pressed" aria-label="记录" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'records' ? '/components/app-tabbar/icons/journal-active.svg' : '/components/app-tabbar/icons/journal.svg'}}" mode="aspectFit" /></view><text class="tab-label">记录</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'profile' ? 'active' : ''}}" data-tab="profile" bindtap="navigate" hover-class="tab-pressed" aria-label="我的" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'profile' ? '/components/app-tabbar/icons/me-active.svg' : '/components/app-tabbar/icons/me.svg'}}" mode="aspectFit" /></view><text class="tab-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,43 @@
|
||||
.tabs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFF0F5;
|
||||
border-top: 1rpx solid #F3DCE5;
|
||||
padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 0;
|
||||
color: #8B827B;
|
||||
}
|
||||
.tab-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 54rpx;
|
||||
border-radius: 32rpx;
|
||||
background: transparent;
|
||||
|
||||
}
|
||||
.tab-icon image { width: 48rpx; height: 48rpx; }
|
||||
.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; }
|
||||
.active { color: #B74768; }
|
||||
.active .tab-icon { background: #FADCE7; }
|
||||
.active .tab-label { font-weight: 600; }
|
||||
.tab-pressed .tab-icon { background: #FCE5ED; }
|
||||
|
||||
@import '../styles/typography.wxss';
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="18" rx="3"/>
|
||||
<circle cx="8" cy="10" r="2"/>
|
||||
<path d="M22 16l-4-4-3 3-3-3-4 4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5-7 7 7 7"/></svg>
|
||||
|
After Width: | Height: | Size: 185 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 198 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5V4a1 1 0 0 1 1-1h9l6 6v10.5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"/>
|
||||
<polyline points="14 3 14 9 20 9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 11 12 14 22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18" stroke="#FF6B81" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M6 6L18 18" stroke="#FF6B81" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 248 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><rect width="80" height="80" rx="40" fill="#F8EDF0"/><circle cx="40" cy="29" r="13" fill="#D5A8B6"/><path d="M16 68c0-17 10-26 24-26s24 9 24 26" fill="#D5A8B6"/></svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ff4d4f">
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#666">
|
||||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="heartGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#B74768"/>
|
||||
<stop offset="1" stop-color="#B74768"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 爱心主体:粉色渐变,圆润饱满 -->
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
fill="url(#heartGrad)"/>
|
||||
<!-- 高光:左上角小亮点,更可爱 -->
|
||||
<ellipse cx="8.2" cy="7.6" rx="1.5" ry="1.1" fill="#B74768" opacity="0.55"/>
|
||||
<!-- 底部小高光 -->
|
||||
<ellipse cx="12" cy="13.2" rx="3.2" ry="1.4" fill="#B74768" opacity="0.25"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#F47FA5" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
|
After Width: | Height: | Size: 268 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5z"/>
|
||||
<path d="M9 21V12h6v9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h5l3-8 4 16 3-8h5"/></svg>
|
||||
|
After Width: | Height: | Size: 195 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2c-3 5-7 8-7 13a7 7 0 0 0 14 0c0-5-4-8-7-13Z"/></svg>
|
||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.5"/><path d="M12 9.5C7 4 9 2 12 2s5 2 0 7.5M14.5 12C20 7 22 9 22 12s-2 5-7.5 0M12 14.5C17 20 15 22 12 22s-5-2 0-7.5M9.5 12C4 17 2 15 2 12s2-5 7.5 0"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M5 21h14M4 7l8-3 8 3M4 7 1 14h6L4 7ZM20 7l-3 7h6l-3-7Z"/></svg>
|
||||
|
After Width: | Height: | Size: 235 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 14a4 4 0 0 0 8 0M8.5 8.5h.01M15.5 8.5h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 248 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14V5a3 3 0 0 1 6 0v9a5 5 0 1 1-6 0Z"/><path d="M12 7v10"/><circle cx="12" cy="18" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#85776D"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>
|
||||
|
After Width: | Height: | Size: 180 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18V5l12-2v13"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<circle cx="18" cy="16" r="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none"><ellipse cx="13" cy="15" rx="4" ry="6" fill="#B74768" transform="rotate(-25 13 15)"/><ellipse cx="24" cy="10" rx="4" ry="6" fill="#B74768"/><ellipse cx="35" cy="15" rx="4" ry="6" fill="#B74768" transform="rotate(25 35 15)"/><path d="M12 32c0-7 7-14 12-14s12 7 12 14c0 9-8 4-12 4s-12 5-12-4" fill="#B74768"/></svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="6" x2="12" y2="18"/>
|
||||
<line x1="6" y1="12" x2="18" y2="12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 245 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="8" r="4"/>
|
||||
<path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 824 B |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="10" y="8" width="28" height="31" rx="5" fill="#F9E1E9" transform="rotate(9 24 24)" stroke="#D8B8C0"/><rect x="8" y="11" width="29" height="30" rx="5" fill="#FFFDF8"/><path d="M12 28l7-7 7 7 4-4 3 4v6H12Z" fill="#DFEEE5" stroke="#9AAF9F"/><circle cx="29" cy="19" r="3" fill="#F8B3C9" stroke="none"/><path d="M19 37h7" stroke="#D8B8C0"/></svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8h23a4 4 0 0 1 4 4v27H13a5 5 0 0 1-5-5V13a5 5 0 0 1 4-5Z" fill="#FFFDF8"/><path d="M14 8v31M8 34a5 5 0 0 1 5-5h26" stroke="#BFA1AA"/><path d="M28 8v13l4-3 4 3V8" fill="#F8B3C9" stroke="#D4819B"/><path d="M20 17h4M20 23h10" stroke="#CDB4BA"/></svg>
|
||||
|
After Width: | Height: | Size: 439 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="10" width="30" height="30" rx="8" fill="#FFFDF8"/><path d="M17 25l6 6 12-15" stroke="#D4819B" stroke-width="3.3"/><path d="M17 8v5M30 8v5"/><circle cx="11" cy="37" r="4" fill="#F8B3C9" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M24 40V27m0 9q-9 0-12-8 9-1 12 8m0-3q8-1 11-7" stroke="#91AC9B"/><path d="M24 28C14 28 10 21 12 14c5 0 9 3 12 7 3-4 7-7 12-7 2 7-2 14-12 14Z" fill="#F8B3C9" stroke="#D4819B"/><path d="M17 16q1-8 7-10 6 2 7 10l-7 9Z" fill="#FCE4EC" stroke="#D4819B"/><path d="M7 10v4M5 12h4" stroke="#D8B8C0"/></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 31V13l21-5v20"/><path d="M18 19l21-5" stroke="#D4819B"/><ellipse cx="12.5" cy="33" rx="6.5" ry="5" fill="#F8B3C9" transform="rotate(-16 12.5 33)"/><ellipse cx="33.5" cy="30" rx="6.5" ry="5" fill="#F8B3C9" transform="rotate(-16 33.5 30)"/><path d="M9 12v5M6.5 14.5h5" stroke="#CDB4BA"/></svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 20 10 9l11 7a25 25 0 0 1 7 0l10-7v11c3 3 4 7 3 11-1 7-9 11-17 11S9 38 7 32c-1-5 0-9 3-12Z" fill="#FFFDF8"/><path d="m13 14 5 4-5 2Zm22 0-5 4 5 2Z" fill="#F8B3C9" stroke="none"/><path d="M16 26v2M31 26v2" stroke-width="2.8"/><path d="m21 31 3 2 3-2Z" fill="#D4819B" stroke="#D4819B"/><path d="M24 33v3m0 0q-4 3-6 0m6 0q4 3 6 0M5 28l7 2M4 34l8-1M36 30l7-2M36 33l8 1" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 583 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="13" height="13" rx="4" fill="#F8D5E0"/><rect x="27" y="8" width="13" height="13" rx="4" fill="#DFEEE5"/><rect x="8" y="27" width="13" height="13" rx="4" fill="#F7E8CA"/><rect x="27" y="27" width="13" height="13" rx="4" fill="#EAE1F4"/></svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="11" width="30" height="29" rx="7" fill="#FFFDF8"/><path d="M9 20h30M17 8v8M31 8v8"/><path d="M24 34s-8-4.7-8-9a4.3 4.3 0 0 1 8-2 4.3 4.3 0 0 1 8 2c0 4.3-8 9-8 9Z" fill="#F8B3C9" stroke="#D4819B"/></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B74768" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,529 @@
|
||||
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { currentUserLocation } = require('../../utils/user-location');
|
||||
const { photoForm, metadataRows } = require('../../utils/photo-metadata');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/album/album.js - 日记式分组相册
|
||||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
const { requestSubscribeAfterAction } = require('../../utils/subscribe');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
batches: [],
|
||||
showPhotoEditor: false, editPhotos: [], editPhotoIdx: 0, editBatchId: '',
|
||||
editForm: { taken_at: '', location: '', activity: '' }, savingPhoto: false, currentAddress: '',
|
||||
albumId: null,
|
||||
showUploadModal: false,
|
||||
uploadFilePaths: [],
|
||||
photoFormList: [], // 每张照片独立的表单数据
|
||||
selectedPhotoIdx: 0, // 当前选中编辑的照片索引
|
||||
uploadForm: { taken_at: '', location: '', activity: '' },
|
||||
uploading: false,
|
||||
showPreview: false,
|
||||
previewImages: [],
|
||||
previewIndex: 0,
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());var id = options && options.album_id;
|
||||
if (id) { this.setData({ albumId: id }); }
|
||||
this._pendingPhotoPicker = !!(id && options.upload === '1');
|
||||
this.loadData();
|
||||
},
|
||||
onShow: function() { this._visible = true; this.loadData(); },
|
||||
onHide: function() { this._visible = false; },
|
||||
|
||||
openPhotoEditor: function(e) {
|
||||
const batch = this.data.batches[Number(e.currentTarget.dataset.batch)];
|
||||
if (!batch || !batch.photos.length) return;
|
||||
this.setData({ showPhotoEditor: true, editBatchId: batch.batch_id, editPhotos: batch.photos, editPhotoIdx: 0, editForm: photoForm(batch.photos[0]) });
|
||||
this._loadCurrentAddress();
|
||||
},
|
||||
_loadCurrentAddress: function() {
|
||||
return currentUserLocation().then(location => { this.setData({ currentAddress: location.location }); return location; });
|
||||
},
|
||||
onEditPhotoInput: function(e) {
|
||||
const field = e.currentTarget.dataset.field;
|
||||
if (['taken_at', 'location', 'activity'].indexOf(field) < 0 || this.data.savingPhoto) return;
|
||||
this.setData({ editForm: { ...this.data.editForm, [field]: e.detail.value } });
|
||||
},
|
||||
clearPhotoDate: function() {
|
||||
if (!this.data.savingPhoto) this.setData({ editForm: { ...this.data.editForm, taken_at: '' } });
|
||||
},
|
||||
_confirmPhotoDiscard: function(next) {
|
||||
if (this.data.savingPhoto) return;
|
||||
const original = photoForm(this.data.editPhotos[this.data.editPhotoIdx] || {});
|
||||
if (JSON.stringify(original) === JSON.stringify(this.data.editForm)) { next(); return; }
|
||||
wx.showModal({ title: '放弃本次修改?', content: '当前照片的信息还没有保存。', confirmText: '放弃修改', success: res => { if (res.confirm && !this.data.savingPhoto) next(); } });
|
||||
},
|
||||
selectEditPhoto: function(e) {
|
||||
const index = Number(e.currentTarget.dataset.index);
|
||||
if (index === this.data.editPhotoIdx || !this.data.editPhotos[index]) return;
|
||||
this._confirmPhotoDiscard(() => this.setData({ editPhotoIdx: index, editForm: photoForm(this.data.editPhotos[index]) }));
|
||||
},
|
||||
closePhotoEditor: function() {
|
||||
this._confirmPhotoDiscard(() => this.setData({ showPhotoEditor: false }));
|
||||
},
|
||||
toggleMetadata: function(e) {
|
||||
const index = Number(e.currentTarget.dataset.batch);
|
||||
if (!this.data.batches[index]) return;
|
||||
this.setData({ ['batches[' + index + '].metadataExpanded']: !this.data.batches[index].metadataExpanded });
|
||||
},
|
||||
preventEditorMove: function() {},
|
||||
savePhotoInfo: async function() {
|
||||
if (this.data.savingPhoto) return;
|
||||
const photo = this.data.editPhotos[this.data.editPhotoIdx];
|
||||
if (!photo || !photo.id) return;
|
||||
const form = { taken_at: this.data.editForm.taken_at, location: this.data.editForm.location.trim(), activity: this.data.editForm.activity.trim() };
|
||||
this.setData({ savingPhoto: true });
|
||||
try {
|
||||
if (!form.location) Object.assign(form, await currentUserLocation());
|
||||
const result = await api.photoUpdate({ id: photo.id, lat: 0, lng: 0, ...form });
|
||||
if (result && result.success && result.location !== undefined) form.location = result.location;
|
||||
if (!result || !result.success) throw result || { error: '保存失败' };
|
||||
// 就地更新卡片和预览,不重载列表,不改变滚动位置。
|
||||
const batches = this.data.batches.map(batch => {
|
||||
const photos = batch.photos.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
|
||||
return { ...batch, photos, metadata: metadataRows(photos) };
|
||||
});
|
||||
const previewImages = this.data.previewImages.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
|
||||
this._photoRows = (this._photoRows || []).map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
|
||||
this.setData({ batches, photoItems: this._photoRows, previewImages, showPhotoEditor: false });
|
||||
wx.showToast({ title: '照片信息已保存', icon: 'success' });
|
||||
} catch (error) {
|
||||
wx.showToast({ title: (error && (error.error || error.message)) || '保存失败,请重试', icon: 'none' });
|
||||
} finally { this.setData({ savingPhoto: false }); }
|
||||
},
|
||||
|
||||
// 左滑返回相册列表
|
||||
touchStartX: 0, touchStartY: 0,
|
||||
onTouchStart: function(e) {
|
||||
if (this.data.showPreview) return;
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
this.touchStartY = e.touches[0].clientY;
|
||||
},
|
||||
onTouchEnd: function(e) {
|
||||
if (this.data.showPreview) return;
|
||||
var dx = this.touchStartX - e.changedTouches[0].clientX;
|
||||
var dy = Math.abs(this.touchStartY - e.changedTouches[0].clientY);
|
||||
if (dx < 20 && dy < 20) return;
|
||||
if (Math.abs(dx) > dy && dx > 80) this.goToAlbums();
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
// 分享到好友
|
||||
onShareAppMessage: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
path: '/pages/album/album?album_id=' + (this.data.albumId || ''),
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this,page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var self = this;
|
||||
var albumId = this.data.albumId;
|
||||
if (!albumId) {
|
||||
var albums = itemsOf(await api.albumList({page:1,page_size:1}));
|
||||
if (albums && albums.length > 0) {
|
||||
albumId = albums[0].id;
|
||||
this.setData({ albumId: albumId });
|
||||
}
|
||||
}
|
||||
if (albumId) {
|
||||
// 共享内存缓存;写操作成功后自动失效。
|
||||
var result = await api.photos(albumId, {page:ticket.page,page_size:20});
|
||||
if (!current(this,ticket)) return;
|
||||
var photos = ticket.page === 1 ? itemsOf(result) : mergeUnique(this._photoRows || [], itemsOf(result));
|
||||
// 根据 batch_id 分组
|
||||
var batchMap = {};
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var info = await api.coupleInfo();
|
||||
if (!current(this,ticket)) return;
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var nameMap = {};
|
||||
var avatarMap = {};
|
||||
nameMap[p1.id] = p1.nickname || p1.username || 'TA';
|
||||
nameMap[p2.id] = p2.nickname || p2.username || 'TA';
|
||||
avatarMap[p1.id] = getImageUrl(p1.avatar);
|
||||
avatarMap[p2.id] = getImageUrl(p2.avatar);
|
||||
|
||||
var defaultName = nameMap[p1.id] || nameMap[p2.id] || '情侣';
|
||||
var defaultAvatar = avatarMap[p1.id] || avatarMap[p2.id] || '';
|
||||
(photos || []).forEach(function(p) {
|
||||
var batchId = p.batch_id || p.created_at || 'default';
|
||||
if (!batchMap[batchId]) {
|
||||
var cid = parseInt(p.created_by) || 0;
|
||||
batchMap[batchId] = {
|
||||
batch_id: batchId,
|
||||
creator_name: nameMap[cid] || defaultName,
|
||||
creator_avatar: avatarMap[cid] || defaultAvatar,
|
||||
created_at: p.created_at || '',
|
||||
created_time: p.created_at ? p.created_at.substring(0, 16).replace('T', ' ') : '',
|
||||
photos: []
|
||||
};
|
||||
}
|
||||
batchMap[batchId].photos.push({
|
||||
...p,
|
||||
url: getImageUrl(p.path)
|
||||
});
|
||||
});
|
||||
// 按时间排序(最新的在最前)
|
||||
var batches = Object.values(batchMap);
|
||||
batches.sort(function(a, b) { return b.created_at > a.created_at ? 1 : -1; });
|
||||
batches.forEach(function(batch) { batch.metadata = metadataRows(batch.photos); });
|
||||
this._photoRows = photos;
|
||||
applyList(this,ticket,result,itemsOf(result),'photoItems',{batches:batches});
|
||||
} else { applyList(this,ticket,{items:[],total:0,has_more:false},[],'photoItems',{batches:[]}); }
|
||||
} catch (e) {
|
||||
failList(this,ticket); console.error(e); }
|
||||
finally {
|
||||
finishList(this,ticket);
|
||||
if (this._visible && this._pendingPhotoPicker && !this.data.loadError && this.data.albumId) {
|
||||
this._pendingPhotoPicker = false;
|
||||
this.chooseImage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
chooseImage: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 9,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
// 确保文件路径是字符串数组
|
||||
var paths = (res.tempFilePaths || []).map(function(p) { return String(p); });
|
||||
if (paths.length === 0) {
|
||||
wx.showToast({ title: '请选择照片', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
self._loadCurrentAddress();
|
||||
var formList = paths.map(function() { return { taken_at: '', location: '', activity: '' }; });
|
||||
self.setData({
|
||||
showUploadModal: true,
|
||||
uploadFilePaths: paths,
|
||||
photoFormList: formList,
|
||||
selectedPhotoIdx: 0,
|
||||
uploadForm: { taken_at: '', location: '', activity: '' }
|
||||
});
|
||||
},
|
||||
fail: function(err) {
|
||||
// 微信选图器关闭时也会触发 fail,主动取消无需提示错误。
|
||||
if (/cancel/i.test(String(err && err.errMsg || ''))) return;
|
||||
console.error('Choose image failed:', err);
|
||||
wx.showToast({ title: '选择照片失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
onUploadInput: function(e) {
|
||||
var field = e.currentTarget.dataset.field;
|
||||
var v = e.detail.value;
|
||||
// 同步更新当前显示的表单和 photoFormList
|
||||
var formList = this.data.photoFormList.slice();
|
||||
formList[this.data.selectedPhotoIdx] = Object.assign({}, formList[this.data.selectedPhotoIdx], { [field]: v });
|
||||
this.setData({
|
||||
['uploadForm.' + field]: v,
|
||||
photoFormList: formList
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 选中照片编辑 =====
|
||||
selectPhoto: function(e) {
|
||||
var idx = e.currentTarget.dataset.index;
|
||||
var form = this.data.photoFormList[idx] || { taken_at: '', location: '', activity: '' };
|
||||
this.setData({
|
||||
selectedPhotoIdx: idx,
|
||||
uploadForm: { taken_at: form.taken_at || '', location: form.location || '', activity: form.activity || '' }
|
||||
});
|
||||
},
|
||||
|
||||
removePhoto: function(e) {
|
||||
var idx = e.currentTarget.dataset.index;
|
||||
var paths = this.data.uploadFilePaths.filter(function(_, i) { return i !== idx; });
|
||||
var formList = this.data.photoFormList.filter(function(_, i) { return i !== idx; });
|
||||
if (paths.length === 0) {
|
||||
this.cancelUpload();
|
||||
} else {
|
||||
var newIdx = Math.min(this.data.selectedPhotoIdx, paths.length - 1);
|
||||
var f = formList[newIdx] || { taken_at: '', location: '', activity: '' };
|
||||
this.setData({
|
||||
uploadFilePaths: paths,
|
||||
photoFormList: formList,
|
||||
selectedPhotoIdx: newIdx,
|
||||
uploadForm: { taken_at: f.taken_at || '', location: f.location || '', activity: f.activity || '' }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addMorePhotos: function() {
|
||||
var self = this;
|
||||
var current = this.data.uploadFilePaths.length;
|
||||
wx.chooseImage({
|
||||
count: 9 - current,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
var newPaths = (res.tempFilePaths || []).map(function(p) { return String(p); });
|
||||
var newForms = newPaths.map(function() { return { taken_at: '', location: '', activity: '' }; });
|
||||
self.setData({
|
||||
uploadFilePaths: self.data.uploadFilePaths.concat(newPaths),
|
||||
photoFormList: self.data.photoFormList.concat(newForms)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 批量上传全部照片(每张使用各自的表单)=====
|
||||
uploadAll: async function() {
|
||||
if (this.data.uploading) return;
|
||||
var paths = this.data.uploadFilePaths;
|
||||
if (paths.length === 0) { wx.showToast({ title: '没有照片', icon: 'none' }); return; }
|
||||
|
||||
// 先保存当前编辑中的表单到 photoFormList
|
||||
var formList = this.data.photoFormList.slice();
|
||||
var curIdx = this.data.selectedPhotoIdx;
|
||||
formList[curIdx] = {
|
||||
taken_at: this.data.uploadForm.taken_at || '',
|
||||
location: this.data.uploadForm.location || '',
|
||||
activity: this.data.uploadForm.activity || ''
|
||||
};
|
||||
|
||||
this.setData({ uploading: true });
|
||||
var self = this;
|
||||
var successCount = 0;
|
||||
var failCount = 0;
|
||||
var failReasons = [];
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
await requestSubscribeAfterAction('photo');
|
||||
|
||||
try {
|
||||
// 上传前先预热 session,确保认证状态有效
|
||||
// 解决 Android 端首次上传时 session 未初始化的问题
|
||||
try {
|
||||
wx.showLoading({ title: '准备中...', mask: true });
|
||||
await api.checkAuth();
|
||||
wx.hideLoading();
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
console.warn('Session 预热失败(不影响后续上传)', e);
|
||||
}
|
||||
|
||||
var albumId = this.data.albumId;
|
||||
if (!albumId) {
|
||||
wx.showLoading({ title: '创建相册...' });
|
||||
var album = await api.albumAdd({ title: '默认相册', description: '我们的甜蜜照片' });
|
||||
albumId = album.id;
|
||||
this.setData({ albumId: albumId });
|
||||
wx.hideLoading();
|
||||
}
|
||||
var batchId = Date.now().toString();
|
||||
|
||||
// 自动定位:为未填写位置的照片自动获取地理位置(发布后自动显示)
|
||||
var autoLoc = null;
|
||||
if (formList.some(function(x) { return !String(x.location || '').trim(); })) autoLoc = await currentUserLocation();
|
||||
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
wx.showLoading({ title: '上传中 ' + (i + 1) + '/' + paths.length, mask: true });
|
||||
var f = formList[i] || { taken_at: '', location: '', activity: '' };
|
||||
try {
|
||||
// 按照日记上传逻辑:先上传到通用 upload 接口获取路径,再调用 API 保存到相册
|
||||
var uploadRes = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: paths[i],
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'photos' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var uploadedPath = uploadRes && (uploadRes.path || uploadRes.url || '');
|
||||
if (uploadedPath) {
|
||||
await api.photoAdd({
|
||||
album_id: String(albumId),
|
||||
path: uploadedPath,
|
||||
taken_at: f.taken_at || '',
|
||||
location: String(f.location || '').trim() || (autoLoc ? autoLoc.location : ''),
|
||||
activity: f.activity || '',
|
||||
batch_id: batchId,
|
||||
// 未手动填写位置时,自动定位并记录经纬度(发布后自动显示地址)
|
||||
lat: (f.location && f.location.trim()) ? 0 : (autoLoc ? autoLoc.lat : 0),
|
||||
lng: (f.location && f.location.trim()) ? 0 : (autoLoc ? autoLoc.lng : 0)
|
||||
});
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
failReasons.push('第' + (i+1) + '张: 上传返回无路径 - ' + JSON.stringify(uploadRes));
|
||||
}
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
failReasons.push('第' + (i+1) + '张: ' + (e.error || e.errMsg || e.message || JSON.stringify(e)));
|
||||
// 内容安全违规:明确提示用户
|
||||
if (e && e.error) {
|
||||
wx.showToast({ title: e.error, icon: 'none', duration: 2000 });
|
||||
}
|
||||
}
|
||||
wx.hideLoading();
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
failReasons.push('整体异常: ' + (e.error || e.errMsg || e.message || JSON.stringify(e)));
|
||||
}
|
||||
|
||||
this.setData({ uploading: false });
|
||||
|
||||
var msg = '上传完成 ' + successCount + '/' + paths.length;
|
||||
if (failCount > 0) msg += '(' + failCount + '张失败)';
|
||||
wx.showToast({ title: msg, icon: successCount > 0 ? 'success' : 'none', duration: 2000 });
|
||||
|
||||
// 如果有失败,输出详细原因到控制台
|
||||
if (failCount > 0 && failReasons.length > 0) {
|
||||
console.error('相册上传失败详情:', failReasons.join(' | '));
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
this.setData({ showUploadModal: false, uploadFilePaths: [], photoFormList: [], uploadForm: { taken_at: '', location: '', activity: '' } });
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
cancelUpload: function() {
|
||||
if (this.data.uploading) return;
|
||||
this.setData({ showUploadModal: false, uploadFilePaths: [], photoFormList: [], uploadForm: { taken_at: '', location: '', activity: '' } });
|
||||
},
|
||||
|
||||
// 上传页左滑返回
|
||||
_upTouchX: 0, _upTouchY: 0,
|
||||
onUploadTouchStart: function(e) {
|
||||
this._upTouchX = e.touches[0].clientX;
|
||||
this._upTouchY = e.touches[0].clientY;
|
||||
},
|
||||
onUploadTouchEnd: function(e) {
|
||||
var dx = this._upTouchX - e.changedTouches[0].clientX;
|
||||
var dy = Math.abs(this._upTouchY - e.changedTouches[0].clientY);
|
||||
if (dx < 20 && dy < 20) return;
|
||||
if (Math.abs(dx) > dy && dx > 80) {
|
||||
this.cancelUpload();
|
||||
}
|
||||
},
|
||||
|
||||
closePreview: function() { this.setData({ showPreview: false }); },
|
||||
onSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
|
||||
|
||||
// 长按明信片 - 保存原图到相册
|
||||
onPostcardLongPress: function(e) {
|
||||
var self = this;
|
||||
var url = e.currentTarget.dataset.url;
|
||||
if (!url) return;
|
||||
wx.showActionSheet({
|
||||
itemList: ['保存原图到相册'],
|
||||
success: function(res) {
|
||||
if (res.tapIndex === 0) self.savePreviewImage(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 保存原图:先通过 getImageInfo 获取本地临时路径再保存
|
||||
savePreviewImage: function(url) {
|
||||
var self = this;
|
||||
wx.showLoading({ title: '保存中...' });
|
||||
wx.getImageInfo({
|
||||
src: url,
|
||||
success: function(info) {
|
||||
wx.hideLoading();
|
||||
self._saveToAlbum(info.path);
|
||||
},
|
||||
fail: function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '获取图片失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_saveToAlbum: function(filePath) {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: filePath,
|
||||
success: function() {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' });
|
||||
},
|
||||
fail: function(err) {
|
||||
var msg = (err && err.errMsg) || '';
|
||||
if (msg.indexOf('auth') >= 0 || msg.indexOf('authorize') >= 0) {
|
||||
wx.showModal({
|
||||
title: '需要相册权限',
|
||||
content: '请在设置中开启「保存到相册」权限,才能保存原图哦',
|
||||
confirmText: '去设置',
|
||||
success: function(res) {
|
||||
if (res.confirm) wx.openSetting();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 预览时仅支持点击遮罩关闭
|
||||
onPreviewTouchStart: function() {},
|
||||
onPreviewTouchEnd: function() {},
|
||||
|
||||
// 点击单张照片 - 进入全屏明信片预览
|
||||
onPhotoTap: function(e) {
|
||||
var batchIdx = e.currentTarget.dataset.batch;
|
||||
var imgIdx = e.currentTarget.dataset.index;
|
||||
var batch = this.data.batches[batchIdx];
|
||||
if (!batch || !batch.photos || !batch.photos[imgIdx]) return;
|
||||
var allImages = [];
|
||||
batch.photos.forEach(function(p) {
|
||||
allImages.push({ id: p.id, url: p.url, taken_at: p.taken_at, location: p.location || '', activity: p.activity });
|
||||
});
|
||||
this.setData({ showPreview: true, previewImages: allImages, previewIndex: imgIdx });
|
||||
},
|
||||
|
||||
// 长按照片 - 删除照片
|
||||
onPhotoLongPress: function(e) {
|
||||
var self = this;
|
||||
var batchIdx = e.currentTarget.dataset.batch;
|
||||
var imgIdx = e.currentTarget.dataset.index;
|
||||
var batch = this.data.batches[batchIdx];
|
||||
if (!batch || !batch.photos || !batch.photos[imgIdx]) return;
|
||||
var photo = batch.photos[imgIdx];
|
||||
|
||||
wx.showModal({
|
||||
title: '删除照片',
|
||||
content: '确定要删除这张照片吗?',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
api.photoDelete({ id: photo.id }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
self.loadData();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goToAlbums: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["photos", "albumList"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/album/album.wxml -->
|
||||
<app-nav title="相册详情" back="{{true}}" />
|
||||
|
||||
|
||||
|
||||
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" wx:if="{{!showUploadModal}}">
|
||||
<view class="container"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">相册里的片段</text></view><button class="j-button j-button-small" bindtap="chooseImage">+ 照片</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
||||
<view class="moments-list" wx:if="{{!loading && !loadError && batches.length > 0}}">
|
||||
<view class="moments-item" wx:for="{{batches}}" wx:key="batch_id">
|
||||
<view class="moments-avatar">
|
||||
<image wx:if="{{item.creator_avatar}}" src="{{item.creator_avatar}}" mode="aspectFill" class="moments-avatar-img"></image>
|
||||
<view class="avatar-circle" wx:else>{{item.creator_name}}</view>
|
||||
</view>
|
||||
<view class="moments-body">
|
||||
<view class="moments-name-row">
|
||||
<text class="moments-name">{{item.creator_name || '情侣'}}</text>
|
||||
<text class="moments-count">{{item.photos.length}}张</text>
|
||||
<view class="moments-more" data-batch="{{index}}" catchtap="openPhotoEditor" hover-class="moments-more-pressed" aria-role="button" aria-label="编辑照片信息"><text>•••</text></view>
|
||||
</view>
|
||||
<text class="moments-time moments-upload-time">上传于 {{item.created_time}}</text>
|
||||
<view class="moments-images">
|
||||
<view class="album-img-wrap" wx:for="{{item.photos}}" wx:key="id" wx:for-index="imgIdx" data-batch="{{index}}" data-index="{{imgIdx}}" bindtap="onPhotoTap" bindlongpress="onPhotoLongPress">
|
||||
<image src="{{item.url}}" mode="aspectFill" class="album-img" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" wx:if="{{!loading && !loadError && !batches.length}}">
|
||||
<text>📷</text>
|
||||
<text>还没有照片</text>
|
||||
<text>点击上方添加照片</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部照片</text></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="preview-overlay" wx:if="{{showPreview}}" bindtap="closePreview">
|
||||
<swiper class="preview-swiper" current="{{previewIndex}}" bindchange="onSwiperChange" circular bindtouchstart="onPreviewTouchStart" bindtouchend="onPreviewTouchEnd">
|
||||
<swiper-item wx:for="{{previewImages}}" wx:key="*this">
|
||||
<view class="preview-postcard" bindtap="closePreview" bindlongpress="onPostcardLongPress" data-url="{{item.url}}">
|
||||
<view class="postcard-photo-frame">
|
||||
<image src="{{item.url}}" mode="aspectFit" class="postcard-photo" />
|
||||
<view class="postcard-seal">♥</view>
|
||||
<view class="postcard-stamp" catchtap="closePreview">
|
||||
<text class="stamp-heart">❤</text>
|
||||
<text class="stamp-text">LOVE</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="postcard-body">
|
||||
<view class="postcard-rule" wx:if="{{item.taken_at}}">
|
||||
<text class="rule-icon">📅</text>
|
||||
<text class="rule-text">{{item.taken_at}}</text>
|
||||
</view>
|
||||
<view class="postcard-rule" wx:if="{{item.location}}">
|
||||
<text class="rule-icon">📍</text>
|
||||
<text class="rule-text">{{item.location}}</text>
|
||||
</view>
|
||||
<view class="postcard-rule" wx:if="{{item.activity}}">
|
||||
<text class="rule-icon">💌</text>
|
||||
<text class="rule-text">{{item.activity}}</text>
|
||||
</view>
|
||||
<view class="postcard-rule" wx:if="{{!item.taken_at && !item.location && !item.activity}}">
|
||||
<text class="rule-text postcard-empty">— 我们past —</text>
|
||||
</view>
|
||||
<view class="postcard-footer">
|
||||
<text class="footer-slogan">✦ 让所有的爱都有迹可循 ✦</text>
|
||||
<text class="footer-hint">长按可保存原图</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="preview-counter">{{previewIndex + 1}} / {{previewImages.length}}</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 全屏上传页 -->
|
||||
<view wx:if="{{showUploadModal}}" class="upload-root" bindtouchstart="onUploadTouchStart" bindtouchend="onUploadTouchEnd">
|
||||
<view class="upload-nav">
|
||||
<view class="upload-back" catchtap="cancelUpload">
|
||||
<image class="back-icon" src="/images/back.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="upload-body" scroll-y show-scrollbar="{{false}}">
|
||||
<!-- 照片网格 -->
|
||||
<view class="photo-grid">
|
||||
<view class="photo-grid-item {{selectedPhotoIdx == index ? 'selected' : ''}}" wx:for="{{uploadFilePaths}}" wx:key="*this" bindtap="selectPhoto" data-index="{{index}}">
|
||||
<image src="{{item}}" mode="aspectFill" class="photo-grid-img" />
|
||||
<view class="photo-has-form" wx:if="{{photoFormList[index].taken_at || photoFormList[index].location || photoFormList[index].activity}}">✓</view>
|
||||
<view class="photo-del-btn" catchtap="removePhoto" data-index="{{index}}">
|
||||
<text class="photo-del-x">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 添加更多 -->
|
||||
<view class="photo-grid-item photo-grid-add" catchtap="addMorePhotos" wx:if="{{uploadFilePaths.length < 9}}">
|
||||
<text class="add-plus">+</text>
|
||||
<text class="add-label">添加</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选中照片的表单 -->
|
||||
<view class="upload-form-card" wx:if="{{uploadFilePaths.length > 0}}">
|
||||
<view class="form-card-head">
|
||||
<image src="{{uploadFilePaths[selectedPhotoIdx]}}" mode="aspectFill" class="form-preview" />
|
||||
<text class="form-card-desc">为第 {{selectedPhotoIdx + 1}} 张照片填写信息</text>
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>拍摄时间</label>
|
||||
<picker mode="date" value="{{uploadForm.taken_at}}" bindchange="onUploadInput" data-field="taken_at">
|
||||
<view class="input">{{uploadForm.taken_at || '选择日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>地点</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="{{currentAddress || '留空自动使用当前地址'}}" value="{{uploadForm.location}}" bindinput="onUploadInput" data-field="location" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>在做什么</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:第一次一起看日落" value="{{uploadForm.activity}}" bindinput="onUploadInput" data-field="activity" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态提示(无照片时) -->
|
||||
<view class="upload-empty" wx:if="{{uploadFilePaths.length == 0}}">
|
||||
<text class="empty-icon">📷</text>
|
||||
<text class="empty-desc">点击 + 添加照片</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="upload-footer">
|
||||
<button class="btn btn-primary" bindtap="uploadAll" hover-class="none" loading="{{uploading}}" wx:if="{{uploadFilePaths.length > 0}}">上传全部 ({{uploadFilePaths.length}}张)</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 照片信息编辑面板 -->
|
||||
<view class="photo-editor-mask" wx:if="{{showPhotoEditor}}" catchtouchmove="preventEditorMove">
|
||||
<view class="photo-editor-backdrop" catchtap="closePhotoEditor" />
|
||||
<view class="photo-editor-sheet">
|
||||
<view class="photo-editor-grip" />
|
||||
<view class="photo-editor-head"><view><text class="photo-editor-title">给照片补一段故事</text><text class="photo-editor-caption">{{editPhotos.length > 1 ? '选择照片,分别记录它的时刻' : '留住这一天,也留住当时的心情'}}</text></view><view class="photo-editor-close" catchtap="closePhotoEditor" aria-role="button" aria-label="关闭">×</view></view>
|
||||
<scroll-view class="photo-editor-body" scroll-y show-scrollbar="{{false}}">
|
||||
<scroll-view class="photo-editor-picker" scroll-x show-scrollbar="{{false}}" wx:if="{{editPhotos.length > 1}}">
|
||||
<view class="photo-editor-thumbs"><view wx:for="{{editPhotos}}" wx:key="id" class="photo-editor-thumb {{editPhotoIdx === index ? 'photo-editor-selected' : ''}}" data-index="{{index}}" catchtap="selectEditPhoto"><image src="{{item.url}}" mode="aspectFill" /><text>第 {{index + 1}} 张</text></view></view>
|
||||
</scroll-view>
|
||||
<view class="photo-editor-single" wx:else><image src="{{editPhotos[0].url}}" mode="aspectFill" /><text>编辑这张照片的信息</text></view>
|
||||
<view class="photo-editor-field"><view class="photo-editor-label"><text>拍摄时间</text><text class="photo-editor-clear" wx:if="{{editForm.taken_at}}" catchtap="clearPhotoDate">清除</text></view><picker mode="date" value="{{editForm.taken_at}}" disabled="{{savingPhoto}}" bindchange="onEditPhotoInput" data-field="taken_at"><view class="photo-editor-input {{!editForm.taken_at ? 'photo-editor-placeholder' : ''}}">{{editForm.taken_at || '选择拍摄日期'}}<text>⌄</text></view></picker></view>
|
||||
<view class="photo-editor-field"><text class="photo-editor-label">地点 · 留空使用当前地址</text><input class="photo-editor-input" placeholder="{{currentAddress || '留空自动使用当前地址'}}" placeholder-class="photo-editor-placeholder handwritten-placeholder" maxlength="200" value="{{editForm.location}}" disabled="{{savingPhoto}}" bindinput="onEditPhotoInput" data-field="location" /></view>
|
||||
<view class="photo-editor-field"><text class="photo-editor-label">在做什么</text><textarea class="photo-editor-story" placeholder="例如:第一次一起看日落" placeholder-class="photo-editor-placeholder handwritten-placeholder" maxlength="200" value="{{editForm.activity}}" disabled="{{savingPhoto}}" bindinput="onEditPhotoInput" data-field="activity" fixed="{{true}}" adjust-position="{{true}}" /></view>
|
||||
</scroll-view>
|
||||
<view class="photo-editor-actions"><button class="photo-editor-cancel" bindtap="closePhotoEditor" disabled="{{savingPhoto}}">取消</button><button class="j-button photo-editor-save" bindtap="savePhotoInfo" loading="{{savingPhoto}}" disabled="{{savingPhoto}}">保存这张照片</button></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,402 @@
|
||||
/* pages/album/album.wxss - 日记式分组相册 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
.upload-root {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
z-index: 1000;
|
||||
background: #FAFAFA;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.upload-nav {
|
||||
display: flex; align-items: center;
|
||||
padding: 96rpx 32rpx 16rpx;
|
||||
}
|
||||
.upload-back {
|
||||
width: 56rpx; height: 56rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
.upload-back .back-icon { width: 28rpx; height: 28rpx; }
|
||||
.upload-body {
|
||||
flex: 1; padding: 0 16rpx; overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 三宫格照片 - 精确三等分 */
|
||||
.photo-grid {
|
||||
display: flex; flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
padding: 8rpx 0 20rpx;
|
||||
}
|
||||
.photo-grid-item {
|
||||
width: calc((100% - 20rpx) / 3);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 14rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.photo-grid-item.selected {
|
||||
border: 4rpx solid #B74768;
|
||||
box-shadow: 0 0 0 2rpx rgba(255,107,157,0.3);
|
||||
}
|
||||
.photo-grid-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.photo-has-form {
|
||||
position: absolute;
|
||||
bottom: 6rpx;
|
||||
left: 6rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,107,157,0.85);
|
||||
color: white;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.photo-del-btn {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.photo-del-x {
|
||||
color: white;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 添加更多 */
|
||||
.photo-grid-add {
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2rpx;
|
||||
}
|
||||
.add-plus { font-size: 44rpx; color: #ccc; font-weight: 300; }
|
||||
.add-label { font-size: 20rpx; color: #bbb; }
|
||||
|
||||
/* 表单卡片 */
|
||||
.upload-form-card {
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.form-card-head {
|
||||
display: flex; align-items: center; gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #F4E2E3;
|
||||
}
|
||||
.form-preview {
|
||||
width: 80rpx; height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.form-card-desc {
|
||||
font-size: 24rpx; color: #999;
|
||||
}
|
||||
.upload-form-card .input-group { margin-bottom: 16rpx; }
|
||||
.upload-form-card .input-group:last-child { margin-bottom: 0; }
|
||||
.upload-form-card .input-group label { font-size: 26rpx; font-weight: 500; color: #555; display: block; margin-bottom: 8rpx; }
|
||||
.upload-form-card .input { padding: 18rpx 22rpx; font-size: 28rpx; background: #FFF5F8; border-radius: 14rpx; border: 1rpx solid #F4E2E3; }
|
||||
|
||||
/* 空状态 */
|
||||
.upload-empty {
|
||||
text-align: center; padding: 200rpx 40rpx;
|
||||
}
|
||||
.upload-empty .empty-icon { font-size: 80rpx; display: block; margin-bottom: 16rpx; }
|
||||
.upload-empty .empty-desc { font-size: 28rpx; color: #bbb; display: block; }
|
||||
|
||||
/* 底部上传按钮 */
|
||||
.upload-footer {
|
||||
padding: 12rpx 24rpx;
|
||||
padding-bottom: calc(12rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
|
||||
|
||||
/* ===== 留念卡片(复用日记样式) ===== */
|
||||
.moments-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.moments-item {
|
||||
display: flex; gap: 24rpx; background: white; border-radius: 24rpx;
|
||||
padding: 32rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.moments-avatar { flex-shrink: 0; }
|
||||
.moments-avatar-img { width: 72rpx; height: 72rpx; border-radius: 50%; }
|
||||
.avatar-circle {
|
||||
width: 72rpx; height: 72rpx; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #B74768, #B388FF);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 28rpx; font-weight: 700; color: white;
|
||||
}
|
||||
.moments-body { flex: 1; min-width: 0; }
|
||||
.moments-name-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.moments-name { font-size: 28rpx; font-weight: 600; color: #B74768; }
|
||||
.moments-count { font-size: 22rpx; color: #999; }
|
||||
.moments-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12rpx; }
|
||||
.moments-time { font-size: 22rpx; color: #999; font-weight: 500; flex-shrink: 0; margin-left: auto; }
|
||||
|
||||
/* ===== 朋友圈9宫格 ===== */
|
||||
.moments-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.album-img-wrap {
|
||||
width: 33.333%;
|
||||
height: 0;
|
||||
padding-bottom: 33.333%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.album-img-wrap:not(:nth-child(3n)) { padding-right: 4rpx; }
|
||||
.album-img-wrap:nth-child(n+4):nth-child(-n+6) { padding-top: 4rpx; }
|
||||
.album-img-wrap:nth-child(n+7) { padding-top: 4rpx; }
|
||||
.album-img {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
/* ===== 全屏明信片预览(导航栏下方整屏) ===== */
|
||||
.preview-overlay {
|
||||
position: fixed; top: var(--nav-height,64px); left: 0; right: 0;
|
||||
height: calc(100vh - var(--nav-height,64px));
|
||||
background: rgba(0,0,0,0.93); z-index: 9999;
|
||||
}
|
||||
.preview-swiper { width: 100%; height: 100%; }
|
||||
.preview-postcard {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 640rpx;
|
||||
background: #fffdf7;
|
||||
background-image:
|
||||
radial-gradient(circle at 12% 88%, rgba(255,107,157,0.05) 0, rgba(255,107,157,0.05) 60rpx, transparent 61rpx),
|
||||
radial-gradient(circle at 90% 8%, rgba(255,186,210,0.08) 0, rgba(255,186,210,0.08) 46rpx, transparent 47rpx);
|
||||
border: 1rpx solid #f5e3d0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20rpx 24rpx 18rpx;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
box-shadow: inset 0 0 0 6rpx rgba(255,255,255,0.6), 0 20rpx 60rpx rgba(0,0,0,0.35);
|
||||
}
|
||||
/* 左上角爱心印章(叠加在照片左上角) */
|
||||
.postcard-seal {
|
||||
position: absolute;
|
||||
top: 20rpx; left: 20rpx;
|
||||
width: 64rpx; height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.8);
|
||||
border: 3rpx double rgba(255,107,157,0.6);
|
||||
color: rgba(255,107,157,0.8);
|
||||
font-size: 28rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transform: rotate(-12deg);
|
||||
z-index: 3;
|
||||
}
|
||||
/* 右上角爱心邮票(叠加在照片右上角) */
|
||||
.postcard-stamp {
|
||||
position: absolute;
|
||||
top: 14rpx; right: 14rpx;
|
||||
width: 84rpx; height: 98rpx;
|
||||
background: #fff;
|
||||
border: 3rpx dashed #B74768;
|
||||
border-radius: 8rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
z-index: 3;
|
||||
transform: rotate(4deg);
|
||||
box-shadow: 0 4rpx 14rpx rgba(255,107,157,0.25);
|
||||
}
|
||||
.stamp-heart {
|
||||
font-size: 36rpx;
|
||||
line-height: 1.1;
|
||||
color: #B74768;
|
||||
}
|
||||
.stamp-text {
|
||||
font-size: 15rpx;
|
||||
letter-spacing: 2rpx;
|
||||
color: #B74768;
|
||||
font-weight: 700;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
/* 照片白边相框 */
|
||||
.postcard-photo-frame {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 220rpx;
|
||||
padding: 10rpx;
|
||||
background: #fff;
|
||||
border-radius: 10rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(180,120,90,0.18);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.postcard-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6rpx;
|
||||
background: #f2e8d5;
|
||||
display: block;
|
||||
}
|
||||
/* 底部书写区 */
|
||||
.postcard-body {
|
||||
flex-shrink: 0;
|
||||
margin-top: 14rpx;
|
||||
padding: 12rpx 4rpx 6rpx;
|
||||
border-top: 1rpx dashed #e8c9a8;
|
||||
}
|
||||
.postcard-rule {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10rpx;
|
||||
padding: 6rpx 0;
|
||||
line-height: 1.45;
|
||||
font-size: 24rpx;
|
||||
color: #6b4a2f;
|
||||
}
|
||||
.rule-icon {
|
||||
font-size: 22rpx;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.rule-text {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.postcard-empty {
|
||||
color: #c9b294;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.postcard-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8rpx;
|
||||
padding-top: 8rpx;
|
||||
border-top: 1rpx dashed rgba(232,201,168,0.6);
|
||||
}
|
||||
.footer-slogan {
|
||||
font-size: 20rpx;
|
||||
color: #B74768;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.footer-hint {
|
||||
font-size: 18rpx;
|
||||
color: #c9a98a;
|
||||
}
|
||||
.preview-counter {
|
||||
position: absolute; bottom: 24rpx; left: 50%; transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.55); color: white; padding: 8rpx 28rpx;
|
||||
border-radius: 40rpx; font-size: 26rpx;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 卡片上的照片故事与编辑入口。 */
|
||||
.moments-name-row { align-items:center; gap:14rpx; margin-bottom:0; }
|
||||
.moments-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.moments-count { flex-shrink:0; }
|
||||
.moments-more { margin-left:auto; flex-shrink:0; width:88rpx; height:88rpx; display:flex; align-items:center; justify-content:center; border-radius:50%; color:#85776D; font-size:28rpx; letter-spacing:3rpx; }
|
||||
.moments-more-pressed { background:#FFF0F5; }
|
||||
.moments-upload-time { display:block; margin:0 0 20rpx; font-size:20rpx; color:#A39386; }
|
||||
.moments-images { margin-top:20rpx; }
|
||||
.moments-metadata { margin-top:18rpx; padding:16rpx 20rpx; border-radius:20rpx; background:#FFF5F8; }
|
||||
.moment-note + .moment-note { border-top:1rpx solid #F1DCE4; margin-top:18rpx; padding-top:18rpx; }
|
||||
.moment-photo-label { display:block; font-size:20rpx; color:#B74768; margin-bottom:12rpx; }
|
||||
.moment-meta-row { display:flex; align-items:flex-start; gap:18rpx; margin-top:8rpx; font-size:24rpx; line-height:1.6; }
|
||||
.moment-meta-label { flex-shrink:0; color:#A39386; font-size:21rpx; padding-top:2rpx; }
|
||||
.moment-meta-value { color:#62504F; overflow-wrap:break-word; min-width:0; white-space:pre-wrap; }
|
||||
.moment-story { color:#352E2A; }
|
||||
.moment-add-note { margin-top:22rpx; padding:16rpx 0 2rpx; border-top:1rpx dashed #F1DCE4; color:#B74768; font-size:22rpx; line-height:1.6; }
|
||||
.photo-editor-mask { position:fixed; inset:0; z-index:1300; display:flex; align-items:flex-end; }
|
||||
.photo-editor-backdrop { position:absolute; inset:0; background:rgba(53,46,42,.35); }
|
||||
.photo-editor-sheet { position:relative; width:100%; max-height:90vh; display:flex; flex-direction:column; background:#FFFCFD; border-radius:40rpx 40rpx 0 0; padding:16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }
|
||||
.photo-editor-grip { width:64rpx; height:7rpx; border-radius:8rpx; background:#EBD5DE; margin:0 auto 26rpx; }
|
||||
.photo-editor-head { display:flex; align-items:flex-start; gap:20rpx; margin-bottom:24rpx; }
|
||||
.photo-editor-head > view:first-child { flex:1; min-width:0; }
|
||||
.photo-editor-title { display:block; font-size:33rpx; font-weight:600; color:#352E2A; }
|
||||
.photo-editor-caption { display:block; margin-top:10rpx; font-size:23rpx; color:#A39386; }
|
||||
.photo-editor-close { width:64rpx; height:64rpx; display:flex; align-items:center; justify-content:center; border-radius:50%; background:#FFF0F5; color:#85776D; font-size:40rpx; }
|
||||
.photo-editor-body { flex:none; height:760rpx; max-height:60vh; }
|
||||
.photo-editor-picker { width:100%; white-space:nowrap; margin-bottom:28rpx; }
|
||||
.photo-editor-thumbs { display:flex; gap:16rpx; padding:4rpx; }
|
||||
.photo-editor-thumb { flex-shrink:0; padding:6rpx; border:2rpx solid transparent; border-radius:20rpx; text-align:center; }
|
||||
.photo-editor-thumb image { display:block; width:104rpx; height:104rpx; border-radius:14rpx; }
|
||||
.photo-editor-thumb text { display:block; font-size:20rpx; color:#A39386; margin-top:8rpx; }
|
||||
.photo-editor-selected { border-color:#B74768; background:#FFF0F5; }.photo-editor-selected text { color:#B74768; }
|
||||
.photo-editor-single { display:flex; align-items:center; gap:20rpx; margin-bottom:28rpx; font-size:24rpx; color:#85776D; }.photo-editor-single image { width:100rpx; height:100rpx; border-radius:18rpx; }
|
||||
.photo-editor-field { margin-bottom:24rpx; }
|
||||
.photo-editor-label { display:flex; align-items:center; justify-content:space-between; color:#62504F; font-size:25rpx; margin-bottom:12rpx; }
|
||||
.photo-editor-clear { color:#B74768; font-size:22rpx; padding:4rpx 12rpx; }
|
||||
.photo-editor-input { display:flex; align-items:center; justify-content:space-between; width:100%; height:88rpx; padding:0 24rpx; border:1rpx solid #F1DCE4; border-radius:18rpx; background:#FFF5F8; color:#352E2A; font-size:26rpx; box-sizing:border-box; }
|
||||
.photo-editor-placeholder { color:#B4A3A6; }
|
||||
.photo-editor-story { width:100%; height:160rpx; padding:22rpx 24rpx; border:1rpx solid #F1DCE4; border-radius:18rpx; background:#FFF5F8; color:#352E2A; font-size:26rpx; line-height:1.6; box-sizing:border-box; }
|
||||
.photo-editor-actions { display:flex; align-items:stretch; gap:20rpx; padding-top:20rpx; }
|
||||
.photo-editor-cancel { flex:1; margin:0; border:1rpx solid #F1DCE4; border-radius:26rpx; background:#FFFCFD; font-size:26rpx; color:#85776D; line-height:88rpx; }
|
||||
.photo-editor-save { flex:2; }
|
||||
.photo-editor-actions .photo-editor-save,
|
||||
.photo-editor-actions .photo-editor-cancel {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
height:96rpx;
|
||||
min-height:96rpx;
|
||||
padding:0;
|
||||
margin:0;
|
||||
line-height:normal;
|
||||
font-size:27rpx;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.photo-editor-cancel::after { border:0; }
|
||||
|
||||
/* 默认只显示首张照片的两行摘要,完整信息由用户展开。 */
|
||||
.moment-summary-line { display:flex; align-items:center; gap:12rpx; min-width:0; font-size:22rpx; line-height:1.5; color:#85776D; }
|
||||
.moment-summary-badge { flex-shrink:0; font-size:19rpx; color:#B74768; }
|
||||
.moment-summary-date { flex-shrink:0; }
|
||||
.moment-summary-location { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.moment-summary-story { display:block; margin-top:6rpx; font-size:24rpx; line-height:1.5; color:#62504F; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.moment-metadata-toggle { display:flex; align-items:center; justify-content:space-between; gap:12rpx; min-height:44rpx; padding-top:8rpx; color:#B74768; font-size:21rpx; line-height:1.5; }
|
||||
.moments-metadata.is-expanded { padding:20rpx; }
|
||||
.moments-metadata.is-expanded .moment-metadata-toggle { margin-top:10rpx; border-top:1rpx solid #F1DCE4; padding-top:14rpx; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,178 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/albums/albums.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
albums: [], uploadMode:false,
|
||||
showMoreModal: false,
|
||||
showEditModal: false,
|
||||
currentAlbum: null,
|
||||
editForm: { title: '', desc: '' }
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
this.setData({uploadMode: !!(options && options.upload === '1')});
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadAlbums();
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadAlbums();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
// 分享到好友
|
||||
onShareAppMessage: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
path: '/pages/albums/albums',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
|
||||
noop: function() {},
|
||||
|
||||
loadAlbums: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var list = await api.albumList({page:ticket.page,page_size:20});
|
||||
var result = list;
|
||||
list = itemsOf(list).map(function(a) {
|
||||
a.cover = getImageUrl(a.cover);
|
||||
a.photo_count = a.photo_count || 0;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度
|
||||
a.location_text = a.location || ((a.lat && a.lng) ? (a.lat + ', ' + a.lng) : '');
|
||||
return a;
|
||||
});
|
||||
applyList(this, ticket, result, list, 'albums');
|
||||
} catch(e) { console.error(e); failList(this,ticket); } finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
openAlbum: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
wx.navigateTo({ url: '/pages/album/album?album_id=' + id + (this.data.uploadMode ? '&upload=1' : '') });
|
||||
},
|
||||
|
||||
// 显示更多菜单
|
||||
showMoreMenu: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
var title = e.currentTarget.dataset.title || '默认相册';
|
||||
var desc = e.currentTarget.dataset.desc || '';
|
||||
var cover = e.currentTarget.dataset.cover || '';
|
||||
this.setData({
|
||||
showMoreModal: true,
|
||||
currentAlbum: { id: id, title: title, description: desc, cover: cover }
|
||||
});
|
||||
},
|
||||
|
||||
hideMoreMenu: function() {
|
||||
this.setData({ showMoreModal: false });
|
||||
},
|
||||
|
||||
// 编辑相册
|
||||
editAlbum: function() {
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
this.setData({
|
||||
showMoreModal: false,
|
||||
showEditModal: true,
|
||||
editForm: { title: album.title, desc: album.description || '' }
|
||||
});
|
||||
},
|
||||
|
||||
hideEditModal: function() {
|
||||
this.setData({ showEditModal: false });
|
||||
},
|
||||
|
||||
onEditTitleInput: function(e) { this.setData({ 'editForm.title': e.detail.value }); },
|
||||
onEditDescInput: function(e) { this.setData({ 'editForm.desc': e.detail.value }); },
|
||||
|
||||
saveAlbumEdit: async function() {
|
||||
var self = this;
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
var title = this.data.editForm.title;
|
||||
if (!title) { wx.showToast({ title: '请输入相册标题', icon: 'none' }); return; }
|
||||
try {
|
||||
wx.showLoading({ title: '保存中...' });
|
||||
await api.albumUpdate({
|
||||
id: album.id,
|
||||
title: title,
|
||||
description: this.data.editForm.desc || '',
|
||||
// 保留原有地理位置
|
||||
lat: album.lat || 0,
|
||||
lng: album.lng || 0,
|
||||
location: album.location || ''
|
||||
});
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已保存 ✨', icon: 'success' });
|
||||
this.hideEditModal();
|
||||
this.loadAlbums();
|
||||
} catch(e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: e.error || '保存失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 删除相册
|
||||
deleteAlbum: function() {
|
||||
var self = this;
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
// 从相册列表中查找照片数量
|
||||
var albums = this.data.albums;
|
||||
var targetAlbum = albums.find(function(a) { return String(a.id) === String(album.id); });
|
||||
var count = targetAlbum ? (targetAlbum.photo_count || 0) : 0;
|
||||
var title = album.title || '该相册';
|
||||
|
||||
this.setData({ showMoreModal: false });
|
||||
|
||||
if (count > 0) {
|
||||
wx.showModal({
|
||||
title: '无法删除',
|
||||
content: '「' + title + '」中还有 ' + count + ' 张照片,请先删除里面的全部照片后再删除相册',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除「' + title + '」吗?',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
api.albumDelete({ id: album.id }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
self.loadAlbums();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, goToPublish: function() { wx.navigateTo({ url: '/pages/publish/publish?type=album' }); },
|
||||
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadAlbums'), {"loadAlbums": ["albumList"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/albums/albums.wxml - 相册列表 -->
|
||||
<app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">{{uploadMode ? '选择上传照片的相册' : '照片里的日常'}}</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 建相册</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在打开相册" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAlbums" />
|
||||
<view class="j-album-list" wx:else><view class="j-album-print" wx:for="{{albums}}" wx:key="id" bindtap="openAlbum" data-id="{{item.id}}"><image wx:if="{{item.cover}}" src="{{item.cover}}" mode="aspectFill" /><view wx:else class="j-album-placeholder"><image src="/images/album.svg" /><text>还没有照片</text></view><view class="j-row"><view><text class="j-story-title">{{item.title || '我们的相册'}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view><view class="j-link j-album-menu" catchtap="showMoreMenu" data-id="{{item.id}}" data-title="{{item.title}}" data-desc="{{item.description}}" data-cover="{{item.cover}}">•••</view></view><text wx:if="{{item.description}}" class="j-caption">{{item.description}}</text></view></view>
|
||||
<page-state wx:if="{{!loading && !loadError && !albums.length}}" title="相册还空着" description="为旅行、周末或日常,留一本相册" action="创建相册" bind:action="goToPublish" />
|
||||
</view>
|
||||
<!-- 操作菜单弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showMoreModal}}" bindtap="hideMoreMenu">
|
||||
<view class="action-sheet" catchtap="noop">
|
||||
<view class="action-item" bindtap="editAlbum">
|
||||
<image src="/images/edit.svg" mode="aspectFit" class="action-icon" />
|
||||
<text>编辑相册信息</text>
|
||||
</view>
|
||||
<view class="action-item danger" bindtap="deleteAlbum">
|
||||
<image src="/images/delete.svg" mode="aspectFit" class="action-icon" />
|
||||
<text>删除相册</text>
|
||||
</view>
|
||||
<view class="action-cancel" bindtap="hideMoreMenu">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑相册弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showEditModal}}" bindtap="hideEditModal">
|
||||
<view class="modal" catchtap="noop" style="width:90%;max-width:560rpx;">
|
||||
<button class="modal-close" bindtap="hideEditModal"><image src="/images/close.svg" mode="aspectFit" /></button>
|
||||
<text class="title">编辑相册</text>
|
||||
<view class="input-group">
|
||||
<label>相册标题</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:旅行记录" value="{{editForm.title}}" bindinput="onEditTitleInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>描述(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="简单描述这个相册" value="{{editForm.desc}}" bindinput="onEditDescInput" />
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="saveAlbumEdit" hover-class="none">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部</text></view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,132 @@
|
||||
/* pages/albums/albums.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
|
||||
.album-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx 4rpx 300rpx;
|
||||
}
|
||||
.album-cell {
|
||||
width: calc(50% - 8rpx);
|
||||
margin: 0 4rpx 16rpx 4rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.album-cover {
|
||||
width: 100%;
|
||||
height: 260rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.album-info {
|
||||
padding: 20rpx 16rpx;
|
||||
padding-right: 60rpx;
|
||||
}
|
||||
.album-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.album-desc {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.album-count {
|
||||
font-size: 22rpx;
|
||||
color: var(--pink);
|
||||
font-weight: 500;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
/* 三个点菜单按钮 */
|
||||
.album-more-btn {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 12rpx;
|
||||
width: 56rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border-radius: 24rpx;
|
||||
z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.album-more-btn:active {
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.more-dots {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 操作菜单弹窗 */
|
||||
.action-sheet {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 16rpx 0;
|
||||
padding-bottom: calc(48rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
z-index: 1000;
|
||||
animation: slideUp 0.25s ease;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 40rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
.action-item .action-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.action-item.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.action-item.danger .action-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-cancel {
|
||||
margin: 12rpx 30rpx 0;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,226 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
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(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, 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;
|
||||
return 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._logsGeneration = (this._logsGeneration || 0) + 1;
|
||||
this.setData({ logsLoading: false, logsError: false });
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var data = await api.checkinDetail(id, { page: 1, page_size: 20 });
|
||||
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 = this.formatLogs(data.logs || []);
|
||||
// 根据打卡权限类型映射统计数据
|
||||
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,
|
||||
logsPage: 1, logsTotal: Number(data.total) || logs.length,
|
||||
logsHasMore: data.has_more === true, logsError: false,
|
||||
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}); }
|
||||
},
|
||||
|
||||
formatLogs: function(logs) {
|
||||
return logs.map(function(log) {
|
||||
return Object.assign({}, log, {
|
||||
time: log.created_at ? log.created_at.substring(0, 16).replace('T', ' ') : '',
|
||||
photoUrl: log.photo ? getImageUrl(log.photo) : ''
|
||||
});
|
||||
});
|
||||
},
|
||||
loadMoreLogs: async function() {
|
||||
if (this._pageLoading || this.data.loading || this.data.loadError || this.data.logsLoading || !this.data.logsHasMore) return;
|
||||
var generation = this._logsGeneration;
|
||||
var page = this.data.logsPage + 1;
|
||||
this.setData({ logsLoading: true, logsError: false });
|
||||
try {
|
||||
var data = await api.checkinDetail(this.data.detailId, { page: page, page_size: 20 });
|
||||
if (generation !== this._logsGeneration) return;
|
||||
var seen = new Set(this.data.logs.map(function(log) { return String(log.id); }));
|
||||
var added = this.formatLogs(data.logs || []).filter(function(log) {
|
||||
var id = String(log.id);
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id); return true;
|
||||
});
|
||||
this.setData({ logs: this.data.logs.concat(added), logsPage: page,
|
||||
logsTotal: Number(data.total) || 0, logsHasMore: data.has_more === true });
|
||||
} catch (e) {
|
||||
if (generation === this._logsGeneration) this.setData({ logsError: true });
|
||||
} finally {
|
||||
if (generation === this._logsGeneration) this.setData({ logsLoading: 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 || this._submittingCheck) return;
|
||||
this._submittingCheck = true;
|
||||
|
||||
try {
|
||||
// 为本人接收TA的提醒补充授权。
|
||||
await requestSubscribeAfterAction('checkin');
|
||||
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.hideLoading(); wx.showToast({ title: e.error || '打卡失败', icon: 'none' }); }
|
||||
finally { this._submittingCheck = false; }
|
||||
},
|
||||
|
||||
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.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadDetail": ["checkinDetail", "coupleInfo"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<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}}" bindscrolltolower="loadMoreLogs" lower-threshold="120" 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">打卡记录 · {{logsTotal}} 条</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>
|
||||
<view class="logs-footer" wx:if="{{logs.length > 0}}">
|
||||
<text wx:if="{{logsLoading}}">正在加载…</text>
|
||||
<text wx:elif="{{logsError}}" bindtap="loadMoreLogs">加载失败,点击重试</text>
|
||||
<text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多</text>
|
||||
<text wx:else>已加载全部记录</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 placeholder-class="handwritten-placeholder" 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,26 @@
|
||||
@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; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
.logs-footer { padding:28rpx 0; text-align:center; color:#85776D; font-size:23rpx; }
|
||||
@@ -0,0 +1,167 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// Diary journal with server pagination.
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { diaryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
scrollTop: 0,
|
||||
tabType: 'all' // 'all' 全部 | 'favorite' 收藏
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
this.setData({ scrollTop: getScrollPosition('diary') });
|
||||
},
|
||||
onShow: function() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 收藏 tab 切换已移除
|
||||
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('diary', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
this.setData({ page: 1, list: [], hasMore: true });
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
onScrollToLower: function() {
|
||||
if (this.data.loadingMore || !this.data.hasMore) return;
|
||||
this.loadMore();
|
||||
},
|
||||
|
||||
onShareAppMessage: function() {
|
||||
return { title: '让所有的爱有迹可循', path: '/pages/diary/diary', imageUrl: '/images/share.png' };
|
||||
},
|
||||
onShareTimeline: function() {
|
||||
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
|
||||
},
|
||||
|
||||
loadData: async function(page) {
|
||||
const ticket=startList(this,page);if(!ticket)return;
|
||||
try {
|
||||
const data=await api.diaryList({page:ticket.page,page_size:20});
|
||||
applyList(this,ticket,data,this._formatItems(itemsOf(data)),'list');
|
||||
this._loadPartnerInfo();
|
||||
} catch(e) { failList(this,ticket); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
_formatItems: function(items) {
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var partnerNames = {};
|
||||
var partnerAvatars = {};
|
||||
return items.map(function(item) {
|
||||
item.creator_name = partnerNames[item.created_by] || '我';
|
||||
item.creator_avatar = partnerAvatars[item.created_by] || '';
|
||||
item.created_time = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var imgs = item.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
item.imageUrls = imgs.map(function(p) { return getImageUrl(p); });
|
||||
// 生成摘要文本(截断内容,用于无图卡片展示)
|
||||
var summaryText = item.content || item.title || '';
|
||||
summaryText = summaryText.replace(/\s+/g, ' ');
|
||||
if (summaryText.length > 80) summaryText = summaryText.substring(0, 80) + '...';
|
||||
item.summary = summaryText;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度
|
||||
item.location_text = item.location || ((item.lat && item.lng) ? (item.lat + ', ' + item.lng) : '');
|
||||
return Object.assign(item, diaryEntry(item));
|
||||
});
|
||||
},
|
||||
|
||||
_loadPartnerInfo: async function() {
|
||||
try {
|
||||
var info = await api.coupleInfo();
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var names = {}, avatars = {};
|
||||
names[p1.id] = p1.nickname || p1.username || 'TA';
|
||||
names[p2.id] = p2.nickname || p2.username || 'TA';
|
||||
avatars[p1.id] = getImageUrl(p1.avatar);
|
||||
avatars[p2.id] = getImageUrl(p2.avatar);
|
||||
var oldList = this.data.list;
|
||||
var changed = false;
|
||||
var updates = {};
|
||||
for (var i = 0; i < oldList.length; i++) {
|
||||
var item = oldList[i];
|
||||
var newName = names[item.created_by] || '我';
|
||||
var newAvatar = avatars[item.created_by] || '';
|
||||
if (item.creator_name !== newName || item.creator_avatar !== newAvatar) {
|
||||
updates['list[' + i + '].creator_name'] = newName;
|
||||
updates['list[' + i + '].author'] = newName;
|
||||
updates['list[' + i + '].creator_avatar'] = newAvatar;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.setData(updates);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
_refreshPartnerInfo: function() {
|
||||
var self = this;
|
||||
api.coupleInfo().then(function(info) {
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var names = {}, avatars = {};
|
||||
names[p1.id] = p1.nickname || p1.username || 'TA';
|
||||
names[p2.id] = p2.nickname || p2.username || 'TA';
|
||||
avatars[p1.id] = getImageUrl(p1.avatar);
|
||||
avatars[p2.id] = getImageUrl(p2.avatar);
|
||||
var oldList = self.data.list;
|
||||
var changed = false;
|
||||
var updates = {};
|
||||
for (var i = 0; i < oldList.length; i++) {
|
||||
var item = oldList[i];
|
||||
var newName = names[item.created_by] || '我';
|
||||
var newAvatar = avatars[item.created_by] || '';
|
||||
if (item.creator_name !== newName || item.creator_avatar !== newAvatar) {
|
||||
updates['list[' + i + '].creator_name'] = newName;
|
||||
updates['list[' + i + '].author'] = newName;
|
||||
updates['list[' + i + '].creator_avatar'] = newAvatar;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
self.setData(updates);
|
||||
}
|
||||
}).catch(function() {});
|
||||
},
|
||||
|
||||
// 点击进入日记详情
|
||||
goToDetail: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
if (!id) return;
|
||||
saveScrollPosition('diary', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
wx.navigateTo({ url: '/pages/diary_detail/diary_detail?id=' + id });
|
||||
},
|
||||
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('diary', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
openJournalEntry: function(e) { this.goToDetail(e); },
|
||||
goToPublish: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/publish/publish?type=diary' }); },
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}"><app-nav title="日记" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bindscrolltolower="onScrollToLower" lower-threshold="100" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" />
|
||||
<block wx:else><block wx:for="{{list}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!list.length}}" title="故事,从今天开始" description="写下第一篇,记录一个平常又特别的瞬间" action="写日记" bind:action="goToPublish" /></block>
|
||||
<view class="j-caption j-center" wx:if="{{loadingMore}}">正在加载更多…</view></view><view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部日记</text></view>
|
||||
</scroll-view></view>
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,279 @@
|
||||
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache({
|
||||
noop: function() {},
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
diary: null,
|
||||
loading: true,
|
||||
comments: [],
|
||||
commentPage: 1, commentsLoadingMore: false, commentsMoreError: false, commentsTotal: 0,
|
||||
hasMoreComments: true,
|
||||
showCommentInput: false,
|
||||
commentText: '',
|
||||
commentInitialText: '',
|
||||
commentLength: 0,
|
||||
commentFocus: false,
|
||||
commentKeyboardHeight: 0,
|
||||
commentEditorHeight: 56,
|
||||
commentSending: false,
|
||||
replyTo: null,
|
||||
coverSwiperIndex: 0,
|
||||
previewImages: [],
|
||||
previewIndex: 0,
|
||||
showPreview: false,
|
||||
myUid: 0
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var diaryId = options.id;
|
||||
if (!diaryId) { wx.showToast({ title: '参数错误', icon: 'none' }); return; }
|
||||
this.setData({ diaryId: parseInt(diaryId), myUid: wx.getStorageSync('userId') || 0 });
|
||||
this.loadDiary();
|
||||
this.loadComments();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
if (this.data.diaryId) { this.loadDiary(); this.loadComments(); }
|
||||
},
|
||||
|
||||
loadDiary: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var data = await api.diaryDetail(this.data.diaryId);
|
||||
data = this._formatDiary(data);
|
||||
this.setData({ diary: data, myUid: wx.getStorageSync('userId') || 0, loading: false });
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
_formatDiary: function(data) {
|
||||
var myUid = wx.getStorageSync('userId') || 0;
|
||||
data.creator_name = data.creator_name || (data.created_by == myUid ? '我' : 'TA');
|
||||
data.creator_avatar = getImageUrl(data.creator_avatar);
|
||||
data.created_time = data.created_at ? data.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var imgs = data.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
data.imageUrls = imgs.map(function(p) { return getImageUrl(p); });
|
||||
data.comment_count = data.comment_count || 0;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度(经纬度为0的不显示)
|
||||
data.location_text = '';
|
||||
var hasCoord = data.lat && data.lng && parseFloat(data.lat) !== 0 && parseFloat(data.lng) !== 0;
|
||||
if (data.location) {
|
||||
data.location_text = data.location;
|
||||
} else if (hasCoord) {
|
||||
data.location_text = data.lat + ', ' + data.lng;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
loadComments: function() { return this._loadCommentsPage(1); },
|
||||
_loadCommentsPage: async function(page) {
|
||||
page = Number(page) || 1;
|
||||
if (this._commentsFirst || (page > 1 && (this.data.commentsLoadingMore || !this.data.hasMoreComments))) return;
|
||||
if (page === 1) { this._commentsFirst = true; this._commentsGeneration = (this._commentsGeneration || 0) + 1; }
|
||||
var generation = this._commentsGeneration;
|
||||
this.setData(page === 1 ? {commentsError:false,commentsMoreError:false,commentsLoadingMore:false} : {commentsLoadingMore:true,commentsMoreError:false});
|
||||
try {
|
||||
var result = await api.diaryCommentList({diary_id:this.data.diaryId,page:page,page_size:20});
|
||||
if (generation !== this._commentsGeneration) return;
|
||||
var added = itemsOf(result).map(function(comment) {
|
||||
return Object.assign({}, comment, {_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
|
||||
});
|
||||
this.setData({comments:page===1?added:mergeUnique(this.data.comments,added),commentPage:page,commentsTotal:totalOf(result),hasMoreComments:!Array.isArray(result)&&(result.has_more != null?!!result.has_more:page*20<totalOf(result))});
|
||||
} catch(e) {
|
||||
if (generation === this._commentsGeneration) this.setData(page===1?{commentsError:true}:{commentsMoreError:true});
|
||||
} finally {
|
||||
if (generation === this._commentsGeneration) { this._commentsFirst=false;this.setData({commentsLoadingMore:false}); }
|
||||
}
|
||||
},
|
||||
loadMoreComments: function() { return this._loadCommentsPage(this.data.commentPage+1); },
|
||||
|
||||
|
||||
onShareTap: function() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['分享给微信好友', '分享到朋友圈', '生成分享海报'],
|
||||
success: function(res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.shareAppMessage ? wx.shareAppMessage() : wx.showToast({ title: '请点击右上角分享', icon: 'none' });
|
||||
} else if (res.tapIndex === 1) {
|
||||
wx.showToast({ title: '请点击右上角分享到朋友圈', icon: 'none' });
|
||||
} else {
|
||||
wx.showToast({ title: '功能开发中', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onCoverSwiperChange: function(e) {
|
||||
this.setData({ coverSwiperIndex: e.detail.current });
|
||||
},
|
||||
|
||||
onImageTap: function(e) {
|
||||
var idx = this.data.coverSwiperIndex;
|
||||
var imgs = this.data.diary.imageUrls || [];
|
||||
this.setData({ showPreview: true, previewIndex: idx, previewImages: imgs });
|
||||
},
|
||||
closePreview: function() { this.setData({ showPreview: false }); },
|
||||
onPreviewSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
|
||||
|
||||
onCommentTap: function() {
|
||||
this.openComment(null);
|
||||
},
|
||||
onReplyTap: function(e) {
|
||||
var item = e.currentTarget.dataset.item;
|
||||
this.openComment({ id: item.id, user_id: item.user_id, nickname: item.nickname });
|
||||
},
|
||||
openComment: function(replyTo) {
|
||||
var sameTarget = (!replyTo && !this.data.replyTo) ||
|
||||
(replyTo && this.data.replyTo && replyTo.id === this.data.replyTo.id);
|
||||
var text = sameTarget ? this.data.commentText : '';
|
||||
this.setData({ showCommentInput: true, replyTo: replyTo, commentText: text,
|
||||
commentInitialText: text, commentLength: text.length, commentFocus: true,
|
||||
commentKeyboardHeight: 0, commentEditorHeight: 56 });
|
||||
},
|
||||
onCommentInput: function(e) {
|
||||
// 不把正文反复写回原生输入框,避免中文输入时光标跳动。
|
||||
this.data.commentText = e.detail.value || '';
|
||||
this.setData({ commentLength: this.data.commentText.length });
|
||||
},
|
||||
onCommentKeyboardHeight: function(e) {
|
||||
var height = Math.max(0, Number(e.detail.height) || 0);
|
||||
if (this.data.showCommentInput) this.setData({ commentKeyboardHeight: height });
|
||||
},
|
||||
onCommentLineChange: function(e) {
|
||||
this.setData({ commentEditorHeight: Math.min(160, Math.max(56, Number(e.detail.height) || 56)) });
|
||||
},
|
||||
onCommentBlur: function() {
|
||||
this.setData({ commentFocus: false });
|
||||
},
|
||||
cancelComment: function() {
|
||||
if (this.data.commentSending) return;
|
||||
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||
wx.hideKeyboard();
|
||||
},
|
||||
onHide: function() {
|
||||
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||
},
|
||||
|
||||
submitComment: async function() {
|
||||
if (this.data.commentSending) return;
|
||||
var text = this.data.commentText.trim();
|
||||
if (!text) { wx.showToast({ title: '请输入评论内容', icon: 'none' }); return; }
|
||||
this.setData({ commentSending: true });
|
||||
try {
|
||||
var params = { diary_id: this.data.diaryId, content: text };
|
||||
if (this.data.replyTo) {
|
||||
params.parent_id = this.data.replyTo.id;
|
||||
params.reply_to_user_id = this.data.replyTo.user_id;
|
||||
params.reply_to_username = this.data.replyTo.nickname;
|
||||
}
|
||||
var res = await api.diaryCommentAdd(params);
|
||||
if (res.success) {
|
||||
this._commentsGeneration = (this._commentsGeneration || 0) + 1;
|
||||
this._commentsFirst = false;
|
||||
this.setData({commentsLoadingMore:false});
|
||||
var newComment = res.comment || {};
|
||||
if (newComment.created_at) {
|
||||
newComment._time = newComment.created_at.substring(0, 16).replace('T', ' ');
|
||||
} else if (newComment.create_time) {
|
||||
newComment._time = newComment.create_time.substring(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
var now = new Date();
|
||||
newComment._time = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
|
||||
}
|
||||
var newComments = this.data.comments.slice();
|
||||
newComments.unshift(newComment);
|
||||
this.setData({
|
||||
comments: newComments,
|
||||
commentsTotal: this.data.commentsTotal + 1,
|
||||
showCommentInput: false,
|
||||
replyTo: null,
|
||||
commentText: '',
|
||||
commentInitialText: '',
|
||||
commentLength: 0,
|
||||
commentFocus: false,
|
||||
commentKeyboardHeight: 0,
|
||||
'diary.comment_count': (this.data.diary.comment_count || 0) + 1
|
||||
});
|
||||
wx.hideKeyboard();
|
||||
wx.showToast({ title: '评论成功', icon: 'success' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '评论失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ commentSending: false });
|
||||
}
|
||||
},
|
||||
|
||||
onCommentLongPress: function(e) {
|
||||
var item = e.currentTarget.dataset.item;
|
||||
if (item.user_id != this.data.myUid) return;
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '删除评论',
|
||||
content: '确定要删除这条评论吗?',
|
||||
success: async function(res) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.diaryCommentDelete({ id: item.id });
|
||||
var list = self.data.comments.filter(function(c) { return c.id != item.id; });
|
||||
self.setData({
|
||||
comments: list,
|
||||
'diary.comment_count': Math.max(0, (self.data.diary.comment_count || 0) - 1)
|
||||
});
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
} catch (e) { wx.showToast({ title: '删除失败', icon: 'none' }); }
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onMoreTap: function() {
|
||||
var self = this;
|
||||
wx.showActionSheet({
|
||||
itemList: ['编辑', '删除'],
|
||||
success: function(res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.navigateTo({ url: '/pages/diary_edit/diary_edit?id=' + self.data.diaryId });
|
||||
} else if (res.tapIndex === 1) {
|
||||
self._deleteDiary();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_deleteDiary: function() {
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '删除日记',
|
||||
content: '确定要删除这篇日记吗?',
|
||||
success: async function(res) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.diaryDelete({ id: self.data.diaryId });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(function() { wx.navigateBack(); }, 1500);
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '删除失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }
|
||||
}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<app-nav title="日记详情" back="{{true}}" /><view class="page-bg">
|
||||
|
||||
<view class="read-state" wx:if="{{loading || loadError}}"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:else kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /></view>
|
||||
<scroll-view scroll-y bindscrolltolower="loadMoreComments" lower-threshold="120" class="read-scroll" wx:if="{{!loading && !loadError && diary}}" show-scrollbar="{{false}}" enhanced>
|
||||
<view class="read-page">
|
||||
<view class="read-author"><image class="read-avatar" src="{{diary.creator_avatar || '/images/default-avatar.svg'}}" mode="aspectFill" /><view class="read-author-info"><text class="read-name">{{diary.creator_name || 'TA'}}</text><text class="read-date">{{diary.created_time}}</text><text class="read-location" wx:if="{{diary.location_text}}">{{diary.location_text}}</text></view><view class="read-menu" catchtap="onMoreTap" aria-label="日记操作"><image src="/images/more.svg" mode="aspectFit" /></view></view>
|
||||
<view class="read-paper">
|
||||
<!-- ===== 图片区:有图轮播,无图用背景替代 ===== -->
|
||||
<view wx:if="{{diary.imageUrls && diary.imageUrls.length > 0}}">
|
||||
<view class="img-box">
|
||||
<swiper class="img-swiper" circular="{{false}}" indicator-dots="{{false}}" bindchange="onCoverSwiperChange" current="{{coverSwiperIndex}}">
|
||||
<swiper-item wx:for="{{diary.imageUrls}}" wx:key="*this" class="img-slide">
|
||||
<image src="{{item}}" mode="aspectFill" class="img-item" catchtap="onImageTap" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="img-count" wx:if="{{diary.imageUrls.length > 1}}">
|
||||
<text>{{coverSwiperIndex + 1}}/{{diary.imageUrls.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- ===== 内容区:白色卡片 ===== -->
|
||||
<view class="content-card">
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="title-text" wx:if="{{diary.title}}">{{diary.title}}</text>
|
||||
|
||||
<!-- 正文 -->
|
||||
<text class="content-text" wx:if="{{diary.content}}">{{diary.content}}</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<!-- ===== 评论区 ===== -->
|
||||
<view class="comment-box">
|
||||
<view class="comment-head">
|
||||
<text class="comment-head-text">留言 {{diary.comment_count || 0}}</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-empty" wx:if="{{comments.length === 0}}">
|
||||
<text>还没有留言,写下你读到这里的心情吧</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-item" wx:for="{{comments}}" wx:key="id" wx:for-item="c"
|
||||
data-item="{{c}}" catchlongpress="onCommentLongPress" catchtap="onReplyTap">
|
||||
<image class="comment-avatar" src="{{c.avatar_url || '/images/default-avatar.svg'}}" mode="aspectFill" />
|
||||
<view class="comment-body">
|
||||
<view class="comment-top">
|
||||
<text class="comment-name">{{c.nickname || '匿名'}}</text>
|
||||
<text class="comment-date">{{c._time || ''}}</text>
|
||||
</view>
|
||||
<text class="comment-content" wx:if="{{!c.reply_to_username}}">{{c.content}}</text>
|
||||
<text class="comment-content" wx:else>
|
||||
<text>回复 </text>
|
||||
<text class="comment-at">@{{c.reply_to_username}}</text>
|
||||
<text>:{{c.content}}</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="j-pagination" wx:if="{{commentsError}}" bindtap="loadComments">留言加载失败,点击重试</view>
|
||||
<view class="j-pagination" wx:elif="{{comments.length}}"><text wx:if="{{commentsLoadingMore}}">正在加载…</text><text wx:elif="{{commentsMoreError}}" bindtap="loadMoreComments">加载失败,点击重试</text><text wx:elif="{{hasMoreComments}}" bindtap="loadMoreComments">加载更多留言</text><text wx:else>已加载全部留言</text></view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入栏 -->
|
||||
<view class="bottom-bar" wx:if="{{!loading && !loadError && diary}}">
|
||||
<view class="bottom-input" catchtap="onCommentTap">
|
||||
<image class="bottom-input-icon" src="/images/comment.svg" mode="aspectFit" />
|
||||
<text class="bottom-input-text">说点什么...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论面板自行跟随键盘,禁用原生页面上推,避免双重位移。 -->
|
||||
<view class="comment-compose-mask" wx:if="{{showCommentInput}}" catchtap="cancelComment" catchtouchmove="noop">
|
||||
<view class="comment-compose {{commentKeyboardHeight > 0 ? 'comment-compose--keyboard' : ''}}" style="bottom:{{commentKeyboardHeight}}px;" catchtap="noop">
|
||||
<view class="comment-compose-heading"><text>{{replyTo ? '回复 @' + replyTo.nickname : '写下你的留言'}}</text><text class="comment-compose-close" catchtap="cancelComment">收起</text></view>
|
||||
<view class="comment-compose-field">
|
||||
<textarea placeholder-class="handwritten-placeholder" class="comment-compose-input" style="height:{{commentEditorHeight}}px;" placeholder="说点什么,让 TA 知道你的心情…" value="{{commentInitialText}}" bindinput="onCommentInput" bindlinechange="onCommentLineChange" bindkeyboardheightchange="onCommentKeyboardHeight" bindblur="onCommentBlur" focus="{{commentFocus}}" fixed="{{true}}" adjust-position="{{false}}" hold-keyboard="{{true}}" show-confirm-bar="{{false}}" cursor-spacing="0" maxlength="500" disabled="{{commentSending}}" />
|
||||
</view>
|
||||
<view class="comment-compose-toolbar"><text class="comment-compose-count">{{commentLength}}/500</text><button class="comment-compose-send" catchtap="submitComment" disabled="{{commentLength === 0 || commentSending}}" loading="{{commentSending}}" hover-class="none">发送</button></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<view class="preview-mask" wx:if="{{showPreview}}" catchtap="closePreview">
|
||||
<swiper class="preview-swiper" current="{{previewIndex}}" bindchange="onPreviewSwiperChange" circular>
|
||||
<swiper-item wx:for="{{previewImages}}" wx:key="*this" class="preview-slide">
|
||||
<image src="{{item}}" mode="widthFix" class="preview-img" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="preview-num">{{previewIndex + 1}} / {{previewImages.length}}</view>
|
||||
<view class="preview-close" catchtap="closePreview">
|
||||
<image class="preview-close-icon" src="/images/close.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
.read-scroll { position:fixed; top:var(--nav-height,64px); left:0; right:0; height:calc(100vh - var(--nav-height,64px) - 100rpx - env(safe-area-inset-bottom)); }
|
||||
.read-state { padding:28rpx 44rpx; }.read-page { padding:32rpx 44rpx 40rpx; }
|
||||
.read-author { display:flex; align-items:center; gap:16rpx; margin-bottom:28rpx; }.read-avatar { width:64rpx; height:64rpx; border-radius:50%; flex-shrink:0; }.read-author-info { flex:1; min-width:0; }.read-name { display:block; font-size:26rpx; font-weight:500; color:#352E2A; }.read-date { display:block; margin-top:6rpx; font-size:21rpx; color:#85776D; }.read-location { display:block; font-size:21rpx; color:#85776D; margin-top:6rpx; line-height:1.5; }
|
||||
.read-menu { width:64rpx; height:64rpx; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-right:-12rpx; }.read-menu image { width:36rpx; height:36rpx; }
|
||||
.read-paper { padding:12rpx; border:1rpx solid #E9DDD0; border-radius:6rpx 28rpx 6rpx 6rpx; background:#FFFDF8; }
|
||||
.img-box { width:100%; height:500rpx; position:relative; background:#F1EBE2; overflow:hidden; border-radius:3rpx 16rpx 3rpx 3rpx; }.img-swiper { width:100%; height:100%; }.img-slide { display:flex; align-items:center; justify-content:center; }.img-item { width:100%; height:100%; display:block; }
|
||||
.img-count { position:absolute; right:16rpx; bottom:16rpx; background:rgba(53,46,42,.6); color:#FFFDF8; padding:6rpx 14rpx; border-radius:10rpx; font-size:21rpx; }
|
||||
.content-card { padding:32rpx 24rpx 28rpx; }.title-text { display:block; font-size:34rpx; line-height:1.5; font-weight:600; color:#352E2A; margin-bottom:20rpx; }.content-text { display:block; font-size:29rpx; line-height:1.9; color:#4D423A; white-space:pre-wrap; overflow-wrap:break-word; }
|
||||
.comment-box { margin-top:38rpx; }.comment-head { padding-bottom:20rpx; border-bottom:1rpx solid #E9DDD0; }.comment-head-text { color:#352E2A; font-size:28rpx; font-weight:600; }.comment-empty { padding:44rpx 8rpx; text-align:center; font-size:24rpx; line-height:1.7; color:#85776D; }
|
||||
.comment-item { display:flex; align-items:flex-start; gap:14rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.comment-item:last-child { border:0; }.comment-avatar { width:48rpx; height:48rpx; border-radius:50%; flex-shrink:0; }.comment-body { flex:1; min-width:0; }.comment-top { display:flex; justify-content:space-between; gap:12rpx; flex-wrap:wrap; margin-bottom:12rpx; }.comment-name { font-size:24rpx; color:#352E2A; font-weight:500; }.comment-date { font-size:20rpx; color:#A39386; }.comment-content { display:block; font-size:26rpx; line-height:1.65; color:#65594F; overflow-wrap:break-word; }.comment-at { color:#B74768; }
|
||||
.bottom-bar { position:fixed; left:0; right:0; bottom:0; z-index:20; background:#FFFDF8; border-top:1rpx solid #E9DDD0; padding:16rpx 44rpx calc(16rpx + env(safe-area-inset-bottom)); height:calc(100rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }.bottom-input { display:flex; align-items:center; gap:14rpx; height:66rpx; border:1rpx solid #E9DDD0; background:#FAF5EE; padding:0 20rpx; border-radius:16rpx; }.bottom-input-icon { width:30rpx; height:30rpx; }.bottom-input-text { color:#85776D; font-size:25rpx; }
|
||||
.comment-compose-mask { position:fixed; inset:0; z-index:1200; background:rgba(53,46,42,.24); }
|
||||
.comment-compose { position:fixed; left:0; right:0; padding:22rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); box-sizing:border-box; border-radius:28rpx 28rpx 0 0; background:#FFFDF8; }
|
||||
.comment-compose--keyboard { padding-bottom:16rpx; }
|
||||
.comment-compose-heading { display:flex; align-items:center; justify-content:space-between; gap:20rpx; margin-bottom:16rpx; font-size:23rpx; line-height:1.5; color:#85776D; }
|
||||
.comment-compose-heading > text:first-child { flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||
.comment-compose-close { flex-shrink:0; padding:8rpx 4rpx; color:#B74768; }
|
||||
.comment-compose-field { background:#FFF0F5; border-radius:22rpx; padding:20rpx 24rpx; }
|
||||
.comment-compose-input { display:block; width:100%; font-size:28rpx; line-height:1.6; color:#352E2A; }
|
||||
.comment-compose-toolbar { display:flex; align-items:center; justify-content:space-between; margin-top:14rpx; }
|
||||
.comment-compose-count { font-size:21rpx; color:#A39386; }
|
||||
.comment-compose-send { margin:0; min-width:124rpx; height:64rpx; padding:0 28rpx; border-radius:32rpx; background:#F47FA5; color:#FFF; font-size:25rpx; line-height:64rpx; }
|
||||
.comment-compose-send::after { border:0; }
|
||||
.comment-compose-send[disabled] { background:#F4E6EB; color:#A39386; }
|
||||
.preview-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:rgba(0,0,0,.95); }.preview-swiper { width:100%; height:100%; }.preview-slide { position:relative; }.preview-img { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; display:block; }.preview-num { position:fixed; bottom:calc(44rpx + env(safe-area-inset-bottom)); left:0; right:0; text-align:center; color:#FFFDF8; font-size:25rpx; }.preview-close { position:fixed; top:var(--nav-height,64px); right:24rpx; width:72rpx; height:72rpx; display:flex; align-items:center; justify-content:center; }.preview-close-icon { width:34rpx; height:34rpx; filter:brightness(0) invert(1); }
|
||||
@media (max-width:350px) { .read-page { padding-left:32rpx; padding-right:32rpx; }.bottom-bar { padding-left:32rpx; padding-right:32rpx; } }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,187 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, API_BASE, getImageUrl, uploadFileCompat } = require('../../utils/request');
|
||||
|
||||
var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌'];
|
||||
var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙'];
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
editorContent: '', loading: true, loadError: false,
|
||||
diaryId: 0,
|
||||
form: { title: '', content: '' },
|
||||
tempImages: [],
|
||||
existingImages: [],
|
||||
submitting: false,
|
||||
contentLength: 0
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (!id) {
|
||||
wx.showToast({ title: '参数错误', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ diaryId: parseInt(id) });
|
||||
this.loadDiary();
|
||||
},
|
||||
|
||||
loadDiary: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
try {
|
||||
var data = await api.diaryDetail(this.data.diaryId);
|
||||
var title = data.title || '';
|
||||
var content = data.content || '';
|
||||
var imgs = data.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
var tempImages = imgs.map(function(p) { return getImageUrl(p); });
|
||||
this.setData({
|
||||
'form.title': title,
|
||||
'form.content': content,
|
||||
editorContent: content,
|
||||
'form.lat': data.lat || 0,
|
||||
'form.lng': data.lng || 0,
|
||||
'form.location': data.location || '',
|
||||
tempImages: tempImages,
|
||||
existingImages: imgs,
|
||||
contentLength: content.length
|
||||
});
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
wx.showToast({ title: '加载日记失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
onTitleInput: function(e) {
|
||||
this.setData({ 'form.title': e.detail.value });
|
||||
},
|
||||
onContentInput: function(e) {
|
||||
var val = e.detail.value || '';
|
||||
// 正文只存于逻辑层,避免每次敲字回传长文本影响原生输入。
|
||||
this.data.form.content = val;
|
||||
this.setData({ contentLength: val.length });
|
||||
},
|
||||
|
||||
chooseImages: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 9 - this.data.tempImages.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
self.setData({ tempImages: self.data.tempImages.concat(res.tempFilePaths) });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 删除单张图片 =====
|
||||
removeImage: function(e) {
|
||||
var index = parseInt(e.currentTarget.dataset.index);
|
||||
var images = this.data.tempImages;
|
||||
var existing = this.data.existingImages;
|
||||
images.splice(index, 1);
|
||||
existing.splice(index, 1);
|
||||
this.setData({ tempImages: images, existingImages: existing });
|
||||
},
|
||||
|
||||
submit: async function() {
|
||||
if (this.data.loading || this.data.loadError) return;
|
||||
if (this.data.submitting) return;
|
||||
var content = this.data.form.content;
|
||||
if (!content || !content.trim()) {
|
||||
wx.showToast({ title: '请填写日记内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ submitting: true });
|
||||
|
||||
try {
|
||||
var uploadPaths = [];
|
||||
var existing = this.data.existingImages || [];
|
||||
var temps = this.data.tempImages || [];
|
||||
|
||||
// 判断哪些是新上传的图片(tempFilePaths 以 http 开头的是已有图片,否则是新选的)
|
||||
for (var i = 0; i < temps.length; i++) {
|
||||
var p = temps[i];
|
||||
if (p.indexOf('http') === 0) {
|
||||
// 已有图片,用原始路径
|
||||
uploadPaths.push(existing[i] || '');
|
||||
} else {
|
||||
// 新图片,需要上传
|
||||
wx.showLoading({ title: '上传图片中...' });
|
||||
var res = await this._uploadImage(p);
|
||||
wx.hideLoading();
|
||||
if (res) uploadPaths.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
await api.diaryUpdate({
|
||||
id: this.data.diaryId,
|
||||
title: this.data.form.title || '',
|
||||
content: content.trim(),
|
||||
images: uploadPaths,
|
||||
// 保留原有地理位置(编辑时位置不变)
|
||||
lat: this.data.form.lat || 0,
|
||||
lng: this.data.form.lng || 0,
|
||||
location: this.data.form.location || ''
|
||||
});
|
||||
|
||||
// 清除详情页缓存
|
||||
|
||||
wx.showToast({ title: '修改成功 ✨', icon: 'success' });
|
||||
setTimeout(function() { wx.navigateBack(); }, 1500);
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: (e && (e.error || e.message || e.errMsg)) || '修改失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
_uploadImage: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
path = retryData && (retryData.path || retryData.url || '');
|
||||
} catch(retryErr) {
|
||||
console.error('图片上传重试失败', retryErr);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('图片上传失败', e);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"disableScroll": true,
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<app-nav title="编辑日记" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" style="height:100vh;">
|
||||
<view class="container"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:elif="{{loadError}}" kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /><block wx:else>
|
||||
<!-- 标题 -->
|
||||
<view class="input-group">
|
||||
<label>标题(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 内容编辑器(支持换行) -->
|
||||
<view class="input-group editor-group">
|
||||
<view class="editor-header">
|
||||
<label>内容</label>
|
||||
<text class="char-count {{contentLength > 5000 ? 'char-warn' : ''}}">{{contentLength}}/5000</text>
|
||||
</view>
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea diary-editor" placeholder="想写些什么..." value="{{editorContent}}" bindinput="onContentInput" maxlength="5000" auto-height="{{true}}" cursor-spacing="24" />
|
||||
</view>
|
||||
|
||||
<!-- 图片(可删除) -->
|
||||
<view class="input-group">
|
||||
<view class="editor-header">
|
||||
<label>图片</label>
|
||||
<text class="img-count">{{tempImages.length}}/9</text>
|
||||
</view>
|
||||
<view class="img-picker-row">
|
||||
<view wx:for="{{tempImages}}" wx:key="*this" class="img-picker-wrap">
|
||||
<image class="img-picker-thumb" src="{{item}}" mode="aspectFill" />
|
||||
<view class="img-picker-del" bindtap="removeImage" data-index="{{index}}" hover-class="del-hover">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="img-picker-add" bindtap="chooseImages" wx:if="{{tempImages.length < 9}}">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn btn-primary submit-btn" bindtap="submit" hover-class="none" loading="{{submitting}}">保存修改</button>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,149 @@
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
.container { padding: 84rpx 32rpx 300rpx; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ===== 输入框美化 ===== */
|
||||
.input-group { margin-bottom: 36rpx; }
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 28rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #FF6B81;
|
||||
box-shadow: 0 0 0 6rpx rgba(255,107,129,0.08);
|
||||
}
|
||||
.textarea {
|
||||
min-height: 200rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 日记标题输入框 ===== */
|
||||
.diary-title-input {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 编辑器头部(标签+计数) ===== */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.editor-header label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.char-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
.char-warn {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.img-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* ===== 日记编辑器 ===== */
|
||||
.diary-editor {
|
||||
min-height: 300rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.7;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 图片选择(可删除) ===== */
|
||||
.img-picker-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.img-picker-wrap {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
.img-picker-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eee;
|
||||
}
|
||||
.img-picker-del {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,71,87,0.9);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255,71,87,0.3);
|
||||
z-index: 2;
|
||||
}
|
||||
.del-hover {
|
||||
transform: scale(1.2);
|
||||
background: #ff4757;
|
||||
}
|
||||
.img-picker-add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.img-picker-add .add-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ccc;
|
||||
line-height: 1;
|
||||
}
|
||||
.img-picker-add .add-text {
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* ===== 提交按钮 ===== */
|
||||
.submit-btn {
|
||||
margin-top: 40rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 正文随输入增高,长文由外层页面滚动,避免嵌套原生输入框滚动。 */
|
||||
textarea.diary-editor { min-height:420rpx; box-sizing:border-box; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,101 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/discover/discover.js
|
||||
const { api } = require('../../utils/request');
|
||||
const { diaryEntry, albumEntry, memoryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
stats: null, loading: true, loadError: false, refreshing: false, publishOpen: false,
|
||||
activeFilter: 'diary', filters: [{value:'diary',label:'日记',icon:'stat-book'}, {value:'album',label:'相册',icon:'stat-album'}, {value:'memory',label:'记忆日',icon:'stat-timeline'}],
|
||||
entries: [], albums: [], memories: [],
|
||||
scrollTop: 0
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.setData({ scrollTop: 0 });
|
||||
},
|
||||
onShow: function() {
|
||||
resetScrollOnTabShow(this, 'discover');
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar) tabBar.syncActive();
|
||||
this._syncPublishTabBar();
|
||||
this.loadStats();
|
||||
},
|
||||
|
||||
// 保存滚动位置
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('discover', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadStats(true);
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadStats: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
|
||||
try {
|
||||
const [timeline, album, diary] = await Promise.all([
|
||||
api.timelineList(), api.albumList(), api.diaryList()
|
||||
]);
|
||||
var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
|
||||
var stats = {
|
||||
timeline: totalOf(timeline),
|
||||
album: photoCount,
|
||||
diary: (diary && diary.total) ? diary.total : 0,
|
||||
};
|
||||
this.setData({ stats: stats, entries: ((diary && diary.items) || []).map(diaryEntry), albums: itemsOf(album).map(albumEntry), memories: itemsOf(timeline).map(memoryEntry) });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setData({loadError:true});
|
||||
} finally { this._loading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
onRefresh: async function() { this.setData({refreshing:true}); try { await this.loadStats(); } finally { this.setData({refreshing:false}); } },
|
||||
selectFilter: function(e) { this.setData({activeFilter:e.currentTarget.dataset.filter}); },
|
||||
_syncPublishTabBar: function() {
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar && tabBar.setHidden) tabBar.setHidden(this.data.publishOpen);
|
||||
},
|
||||
showPublish: function() { this.setData({publishOpen:true}); this._syncPublishTabBar(); },
|
||||
hidePublish: function() { this.setData({publishOpen:false}); this._syncPublishTabBar(); },
|
||||
onHide: function() { this.hidePublish(); },
|
||||
noop: function() {}, preventMove: function() {},
|
||||
_publish: function(type) { this.hidePublish(); this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/publish/publish?type=' + type}); },
|
||||
writeDiary: function() { this._publish('diary'); },
|
||||
uploadPhotos: function() { this.hidePublish(); this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/albums/albums?upload=1'}); },
|
||||
createAlbum: function() { this._publish('album'); },
|
||||
createMemory: function() { this._publish('timeline'); },
|
||||
openJournalEntry: function(e) { this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/diary_detail/diary_detail?id=' + e.currentTarget.dataset.id}); },
|
||||
openAlbum: function(e) { this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/album/album?album_id=' + e.currentTarget.dataset.id}); },
|
||||
// 跳转前保存当前滚动位置
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('discover', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
// Tab 导航(替换,不留栈)
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToDiaryFromDiscover: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/diary/diary?from=discover' }); },
|
||||
goToProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
|
||||
// 功能入口(推入栈,支持左滑返回)
|
||||
goToTimeline: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/timeline/timeline' }); },
|
||||
goToAlbum: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/albums/albums' }); },
|
||||
goToSongs: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/lovewords/lovewords' }); },
|
||||
goToCheckin: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/punch/punch' }); },
|
||||
goToPet: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/pet/pet' }); },
|
||||
goToPeriod: function() {
|
||||
this.hidePublish();
|
||||
this._savePosBeforeLeave();
|
||||
wx.navigateTo({ url: '/pages/period/period' });
|
||||
}
|
||||
}, {"loadStats": ["timelineList", "albumList", "diaryList"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell j-handwritten-page" style="{{layoutStyle}}"><view class="j-fixed-header"><view class="j-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">PAGES OF US</text><text class="j-page-title">我们的生活手账</text></view></view>
|
||||
<scroll-view class="page-bg j-main-scroll" scroll-y show-scrollbar="{{false}}" bindscroll="onScroll" scroll-top="{{scrollTop}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh">
|
||||
<view class="j-body"><view class="records-action-row"><text class="j-small-title">一起的日常</text><button class="j-button j-button-small" bindtap="showPublish">+ 记录</button></view>
|
||||
<view class="j-tools records-tools"><view class="j-tool-grid"><view bindtap="goToSongs"><image src="/images/stat-music.svg" /><text>歌曲</text></view><view bindtap="goToCheckin"><image src="/images/stat-check.svg" /><text>打卡</text></view><view bindtap="goToPet"><image src="/images/stat-pet.svg" /><text>宠物</text></view><view bindtap="goToPeriod"><image src="/images/stat-j-flower.svg" /><text>姨姨</text></view></view></view>
|
||||
<view class="j-filters"><view wx:for="{{filters}}" wx:key="value" class="j-filter {{activeFilter === item.value ? 'is-active' : ''}}" data-filter="{{item.value}}" bindtap="selectFilter"><image src="/images/{{item.icon}}.svg" mode="aspectFit" /><text>{{item.label}}</text></view></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开手账" /><page-state wx:elif="{{loadError}}" kind="error" title="手账暂时没有加载成功" action="重新加载" bind:action="loadStats" />
|
||||
<block wx:else>
|
||||
<block wx:if="{{activeFilter === 'diary'}}"><block wx:for="{{entries}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!entries.length}}" title="故事,从今天开始" description="慢慢积攒属于你们的生活片段" action="写日记" bind:action="writeDiary" /><view class="j-link j-center" wx:else bindtap="goToDiaryFromDiscover">查看全部日记 →</view></block>
|
||||
<block wx:elif="{{activeFilter === 'album'}}"><view class="j-album-list"><view class="j-album-print" wx:for="{{albums}}" wx:key="id" bindtap="openAlbum" data-id="{{item.id}}"><image wx:if="{{item.cover}}" src="{{item.cover}}" mode="aspectFill" /><view wx:else class="j-album-placeholder"><image src="/images/stat-album.svg" /><text>还没有照片</text></view><text class="j-story-title">{{item.title}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view></view><page-state wx:if="{{!albums.length}}" title="相册还空着" description="为旅行、周末或日常,留一本相册" action="创建相册" bind:action="createAlbum" /><view class="j-link j-center" wx:else bindtap="goToAlbum">管理全部相册 →</view></block>
|
||||
<block wx:else><view class="j-memory j-memory-list" wx:for="{{memories}}" wx:key="id" bindtap="goToTimeline"><view class="j-stamp"><text>{{item.dateMonth}}月</text><text>{{item.dateDay}}</text></view><view class="j-memory-info"><text class="j-small-title">{{item.title}}</text><text class="j-caption">{{item.date}}</text></view><text class="j-link">›</text></view><page-state wx:if="{{!memories.length}}" title="记住一个特别的日子" description="第一次见面、旅行,还有每个值得期待的日子" action="添加记忆日" bind:action="createMemory" /><view class="j-link j-center" wx:else bindtap="goToTimeline">查看全部记忆日 →</view></block>
|
||||
</block>
|
||||
|
||||
</view></scroll-view>
|
||||
<view wx:if="{{publishOpen}}" class="j-sheet-mask" bindtap="hidePublish" catchtouchmove="preventMove"><view class="j-sheet" catchtap="noop"><view class="j-sheet-grip" /><view class="j-section-head"><text>留下一页日常</text><view class="j-link" bindtap="hidePublish">关闭</view></view><view class="j-publish-grid"><view bindtap="writeDiary"><image src="/images/stat-book.svg" /><text>写日记</text></view><view bindtap="uploadPhotos"><image src="/images/stat-album.svg" /><text>上传照片</text></view><view bindtap="createMemory"><image src="/images/stat-timeline.svg" /><text>记忆日</text></view></view></view></view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
.records-tools { padding-top:0; margin:0 0 32rpx; border-top:0; }
|
||||
.records-tools .j-tool-grid { margin-top:12rpx; border-bottom:1rpx solid #E9DDD0; padding-bottom:12rpx; gap:12rpx; }
|
||||
.records-tools .j-tool-grid > view { flex-direction:row; gap:8rpx; background:transparent; border:0; border-radius:0; padding:20rpx 0; color:#352E2A; min-height:80rpx; }
|
||||
.records-tools .j-tool-grid image { width:42rpx; height:42rpx; flex-shrink:0; }
|
||||
|
||||
.records-action-row { display:flex; align-items:center; justify-content:space-between; gap:24rpx; margin-bottom:24rpx; }
|
||||
.records-action-row .j-button { margin:0; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
/* 发布抽屉使用同一套手账图标。 */
|
||||
.j-publish-grid > view > image { width:48rpx; height:48rpx; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
/* 分类标签配上同款图标,并保证窄屏仍能放下三个入口。 */
|
||||
.j-filters { gap:24rpx; }
|
||||
.j-filters .j-filter { display:flex; align-items:center; gap:8rpx; flex-shrink:0; }
|
||||
.j-filters .j-filter image { width:34rpx; height:34rpx; flex-shrink:0; }
|
||||
@media (max-width:350px) { .j-filters { gap:16rpx; }.j-filters .j-filter { gap:6rpx; font-size:25rpx; } }
|
||||
@@ -0,0 +1,106 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/history/history.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
// 操作类型对应的图标和颜色
|
||||
var ACTION_CONFIG = {
|
||||
login: { icon: '🔓', color: '#82B1FF', label: '登录' },
|
||||
timeline_create: { icon: '📅', color: '#FF8A65', label: '记忆日' },
|
||||
timeline_update: { icon: '✏️', color: '#FF8A65', label: '记忆日' },
|
||||
album_create: { icon: '📷', color: '#66BB6A', label: '相册' },
|
||||
photo_upload: { icon: '🖼️', color: '#66BB6A', label: '照片' },
|
||||
diary_create: { icon: '📝', color: '#FF6B9D', label: '日记' },
|
||||
diary_update: { icon: '✏️', color: '#FF6B9D', label: '日记' },
|
||||
song_add: { icon: '🎵', color: '#AB47BC', label: '歌曲' },
|
||||
checkin_do: { icon: '✅', color: '#42A5F5', label: '打卡' },
|
||||
profile_edit: { icon: '👤', color: '#FFA726', label: '资料' },
|
||||
partner_edit: { icon: '💑', color: '#FFA726', label: '资料' },
|
||||
couple_edit: { icon: '💕', color: '#EC407A', label: '纪念日' },
|
||||
unbind_request: { icon: '💔', color: '#EF5350', label: '解绑' },
|
||||
unbind_agreed: { icon: '💔', color: '#EF5350', label: '解绑' },
|
||||
unbind_rejected: { icon: '💖', color: '#EF5350', label: '解绑' },
|
||||
unbind_cancelled: { icon: '↩️', color: '#EF5350', label: '解绑' },
|
||||
};
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false, list: [], page: 1, totalPages: 0, pages: [], loading: true },
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout()); this.loadLogs(); },
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
this.setData({ page: 1, list: [] });
|
||||
await this.loadLogs();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadLogs: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
var cp = this.data.page;
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const res = await api.activityLogList({ page: cp, page_size: 20 });
|
||||
var items = (res.items || []).map(function(item) {
|
||||
item.created_at = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var cfg = ACTION_CONFIG[item.action_type] || { icon: '📌', color: '#999', label: '' };
|
||||
item.action_icon = cfg.icon;
|
||||
item.action_color = cfg.color;
|
||||
item.action_label = cfg.label;
|
||||
return item;
|
||||
});
|
||||
var tp = res.total_pages || 1;
|
||||
var pages = [];
|
||||
if (tp <= 7) {
|
||||
for (var i = 1; i <= tp; i++) { pages.push({ num: i, dot: false }); }
|
||||
} else {
|
||||
pages.push({ num: 1, dot: false });
|
||||
pages.push({ num: 2, dot: false });
|
||||
pages.push({ num: 3, dot: false });
|
||||
if (cp > 4) pages.push({ num: 0, dot: true });
|
||||
pages.push({ num: tp - 2, dot: false });
|
||||
pages.push({ num: tp - 1, dot: false });
|
||||
pages.push({ num: tp, dot: false });
|
||||
}
|
||||
var pageData = {
|
||||
list: items,
|
||||
page: cp,
|
||||
totalPages: tp,
|
||||
pages: pages,
|
||||
loading: false
|
||||
};
|
||||
this.setData(pageData);
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
goPage: function(e) {
|
||||
var p = parseInt(e.currentTarget.dataset.page);
|
||||
if (p === this.data.page) return;
|
||||
this.setData({ page: p });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goPrev: function() {
|
||||
if (this.data.page <= 1) return;
|
||||
this.setData({ page: this.data.page - 1 });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goNext: function() {
|
||||
if (this.data.page >= this.data.totalPages) return;
|
||||
this.setData({ page: this.data.page + 1 });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goToSettings: function() { wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/history/history.wxml - 情侣操作记录(优化版) -->
|
||||
<app-nav title="操作记录" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;">
|
||||
<view class="container">
|
||||
<!-- 加载状态 -->
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载操作记录" /><page-state wx:elif="{{loadError}}" kind="error" title="操作记录加载失败" action="重试" bind:action="loadLogs" />
|
||||
<block wx:elif="{{list.length > 0}}">
|
||||
<view class="log-list">
|
||||
<view class="log-item" wx:for="{{list}}" wx:key="id">
|
||||
<view class="log-icon-wrap" style="background:{{item.action_color}}20;">
|
||||
<text class="log-icon">{{item.action_icon}}</text>
|
||||
</view>
|
||||
<view class="log-card">
|
||||
<view class="log-header">
|
||||
<view class="log-user-row">
|
||||
<text class="log-label" style="color:{{item.action_color}};">{{item.action_label}}</text>
|
||||
<text class="log-user" style="color:{{item.gender == 'male' ? '#82B1FF' : '#FF8A80'}};">{{item.nickname}}</text>
|
||||
</view>
|
||||
<text class="log-time">{{item.created_at}}</text>
|
||||
</view>
|
||||
<view class="log-body">{{item.description}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pagination" wx:if="{{totalPages > 1}}">
|
||||
<text class="page-btn {{page <= 1 ? 'disabled' : ''}}" bindtap="goPrev">‹</text>
|
||||
<block wx:for="{{pages}}" wx:key="num">
|
||||
<text class="page-dot" wx:if="{{item.dot}}">…</text>
|
||||
<text class="page-num {{page == item.num ? 'active' : ''}}" wx:else bindtap="goPage" data-page="{{item.num}}">{{item.num}}</text>
|
||||
</block>
|
||||
<text class="page-btn {{page >= totalPages ? 'disabled' : ''}}" bindtap="goNext">›</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="empty-state">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无操作记录</text>
|
||||
<text class="empty-desc">双方的操作都会记录在这里 ✨</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</scroll-view>
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,81 @@
|
||||
/* pages/history/history.wxss - 操作记录优化版 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* ===== 加载状态 ===== */
|
||||
.loading-state { text-align:center; padding:160rpx 40rpx; }
|
||||
.loading-icon { font-size:60rpx; display:block; margin-bottom:16rpx; }
|
||||
.loading-text { font-size:26rpx; color:#bbb; }
|
||||
|
||||
/* ===== 操作列表(卡片式) ===== */
|
||||
.log-list { padding: 0 8rpx; }
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.log-icon-wrap {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.log-icon { font-size: 28rpx; }
|
||||
|
||||
.log-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.log-user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.log-label {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.log-user { font-size: 24rpx; font-weight: 600; }
|
||||
.log-time { font-size: 20rpx; color: #bbb; flex-shrink: 0; }
|
||||
.log-body { font-size: 26rpx; color: #555; line-height: 1.6; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display:flex; justify-content:center; align-items:center; gap:8rpx; padding:32rpx 0; flex-wrap:wrap; }
|
||||
.page-btn { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; border-radius:50%; background:#fff; color:#FF6B81; font-size:28rpx; font-weight:700; }
|
||||
.page-btn.disabled { color:#ddd; }
|
||||
.page-num { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; border-radius:50%; background:#fff; color:#888; font-size:24rpx; }
|
||||
.page-num.active { background:linear-gradient(135deg,#B74768,#B388FF); color:#fff; font-weight:700; }
|
||||
.page-dot { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; color:#ccc; font-size:24rpx; }
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state { text-align:center; padding:120rpx 40rpx; }
|
||||
.empty-icon { font-size:80rpx; display:block; margin-bottom:24rpx; }
|
||||
.empty-text { font-size:30rpx; color:#888; display:block; margin-bottom:8rpx; }
|
||||
.empty-desc { font-size:24rpx; color:#bbb; }
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||