init: initial commit

This commit is contained in:
2026-10-07 09:28:43 +08:00
commit a68a4ef411
251 changed files with 38279 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
const { getLayout } = require('../../utils/layout');
const { api, getImageUrl } = require('../../utils/request');
Page({
noop: function() {},
onResize: function() { this.setData(getLayout()); },
data: { loadError: false,
diary: null,
loading: true,
comments: [],
commentPage: 1,
hasMoreComments: true,
showCommentInput: false,
commentText: '',
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();
},
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);
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: async function() {
try {
var data = await api.diaryCommentList({ diary_id: this.data.diaryId });
if (data && data.length > 0) {
data.forEach(function(c) {
if (c.created_at) {
c._time = c.created_at.substring(0, 16).replace('T', ' ');
} else if (c.create_time) {
c._time = c.create_time.substring(0, 16).replace('T', ' ');
} else {
c._time = '';
}
});
}
this.setData({ comments: data || [] });
} catch (e) { console.error(e); }
},
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.setData({ showCommentInput: true, replyTo: null, commentText: '' });
},
onReplyTap: function(e) {
var item = e.currentTarget.dataset.item;
this.setData({
showCommentInput: true,
replyTo: { id: item.id, user_id: item.user_id, nickname: item.nickname },
commentText: ''
});
},
onCommentInput: function(e) {
this.setData({ commentText: e.detail.value });
},
cancelComment: function() {
this.setData({ showCommentInput: false, replyTo: null, commentText: '' });
},
submitComment: async function() {
var text = this.data.commentText.trim();
if (!text) { wx.showToast({ title: '请输入评论内容', icon: 'none' }); return; }
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) {
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.push(newComment);
this.setData({
comments: newComments,
showCommentInput: false,
replyTo: null,
commentText: '',
'diary.comment_count': (this.data.diary.comment_count || 0) + 1
});
wx.showToast({ title: '评论成功', icon: 'success' });
}
} catch (e) {
wx.showToast({ title: e.error || '评论失败', icon: 'none' });
}
},
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.redirectTo({ url: '/pages/index/index' }); }
});
@@ -0,0 +1,8 @@
{
"navigationStyle": "custom",
"enablePullDownRefresh": false,
"usingComponents": {
"app-nav": "/components/app-nav/app-nav",
"page-state": "/components/page-state/page-state"
}
}
+101
View File
@@ -0,0 +1,101 @@
<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 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>
</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="modal-mask" wx:if="{{showCommentInput}}" catchtap="cancelComment">
<view class="modal-box" catchtap="noop">
<view class="modal-top">
<text class="modal-cancel" catchtap="cancelComment">取消</text>
<text class="modal-title">{{replyTo ? '回复 @' + replyTo.nickname : '发表评论'}}</text>
<text class="modal-send" catchtap="submitComment">发送</text>
</view>
<textarea class="modal-input" placeholder="说点什么..." value="{{commentText}}" bindinput="onCommentInput" auto-focus maxlength="500" />
</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,17 @@
@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; }
.modal-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:1200; display:flex; align-items:flex-end; }.modal-box { width:100%; border-radius:24rpx 24rpx 0 0!important; padding:28rpx 32rpx calc(28rpx + env(safe-area-inset-bottom)); }.modal-top { display:flex; align-items:center; justify-content:space-between; gap:16rpx; margin-bottom:24rpx; }.modal-cancel,.modal-send { padding:12rpx 0; font-size:26rpx; color:#85776D; }.modal-send { color:#B74768; }.modal-title { font-size:28rpx; font-weight:500; color:#352E2A; }.modal-input { width:100%; height:200rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:20rpx; font-size:27rpx; box-sizing:border-box; }
.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; } }