From 02c35c146d8280d4394738aee83870278dc074af Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 16 May 2026 23:59:13 +0800 Subject: [PATCH] 1 --- backend/app/api/announcements.py | 24 ++ miniprogram/app.json | 3 +- miniprogram/app.wxss | 35 +++ .../pages/announcement-detail/index.js | 45 +++ .../pages/announcement-detail/index.json | 3 + .../pages/announcement-detail/index.wxml | 22 ++ .../pages/announcement-detail/index.wxss | 40 +++ miniprogram/pages/home/index.js | 25 +- miniprogram/pages/home/index.wxml | 18 +- miniprogram/pages/interact/index.js | 275 +++++++++++++++++- miniprogram/pages/interact/index.json | 2 +- miniprogram/pages/interact/index.wxml | 57 +++- miniprogram/pages/interact/index.wxss | 244 ++++++++++++++++ miniprogram/pages/member-detail/index.js | 13 +- miniprogram/pages/member-detail/index.wxml | 80 +++-- miniprogram/pages/member-detail/index.wxss | 140 +++++++++ miniprogram/pages/module/index.js | 85 ++++-- miniprogram/pages/module/index.wxml | 7 +- miniprogram/pages/schedule-detail/index.js | 28 +- miniprogram/pages/schedule-detail/index.wxml | 54 ++-- miniprogram/pages/schedule-detail/index.wxss | 120 ++++++++ miniprogram/pages/timeline-detail/index.js | 182 ++++++++++-- miniprogram/pages/timeline-detail/index.wxml | 82 +++--- miniprogram/pages/timeline-detail/index.wxss | 200 +++++++++---- miniprogram/pages/vote-detail/index.wxml | 11 +- miniprogram/pages/vote-detail/index.wxss | 23 ++ miniprogram/utils/modules.js | 4 +- 27 files changed, 1527 insertions(+), 295 deletions(-) create mode 100644 miniprogram/pages/announcement-detail/index.js create mode 100644 miniprogram/pages/announcement-detail/index.json create mode 100644 miniprogram/pages/announcement-detail/index.wxml create mode 100644 miniprogram/pages/announcement-detail/index.wxss diff --git a/backend/app/api/announcements.py b/backend/app/api/announcements.py index 0e1fdb1..6d924ee 100644 --- a/backend/app/api/announcements.py +++ b/backend/app/api/announcements.py @@ -55,6 +55,30 @@ async def get_announcements( ) +@router.get("/{announcement_id}", response_model=AnnouncementOut) +async def get_announcement_detail( + announcement_id: int, + user: User = Depends(require_role("super_admin", "teacher", "student")), + db: AsyncSession = Depends(get_db), +): + announcement = await get_announcement_by_id(db, announcement_id) + if announcement is None: + raise HTTPException(status_code=404, detail="Announcement not found") + ensure_class_access(user, announcement.class_id) + await ensure_class_module_enabled(db, announcement.class_id, "announcements") + return AnnouncementOut( + id=announcement.id, + class_id=announcement.class_id, + author_id=announcement.author_id, + author_name=announcement.author.name if announcement.author else "Unknown", + title=announcement.title, + content=announcement.content, + is_pinned=announcement.is_pinned, + created_at=announcement.created_at, + updated_at=announcement.updated_at, + ) + + @router.post("/", response_model=AnnouncementOut) async def create_new_announcement( data: AnnouncementCreate, diff --git a/miniprogram/app.json b/miniprogram/app.json index bddf9ff..cd5dca6 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -8,6 +8,7 @@ "pages/module/index", "pages/manage/index", "pages/member-detail/index", + "pages/announcement-detail/index", "pages/schedule-detail/index", "pages/vote-detail/index", "pages/fund-detail/index", @@ -42,7 +43,7 @@ }, { "pagePath": "pages/interact/index", - "text": "互动", + "text": "班级圈", "iconPath": "assets/tabbar/interact.png", "selectedIconPath": "assets/tabbar/interact-active.png" }, diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss index f41c7fe..77e4eda 100644 --- a/miniprogram/app.wxss +++ b/miniprogram/app.wxss @@ -462,6 +462,41 @@ page { color: #7a4b2b; } +.member-role-badge.committee { + background: #d6a653; + color: #3a221d; +} + +.schedule-side { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10rpx; + flex: none; +} + +.schedule-countdown { + min-height: 40rpx; + padding: 0 16rpx; + border-radius: 999rpx; + background: #e7efff; + color: #53698e; + font-size: 22rpx; + font-weight: 760; + line-height: 40rpx; + white-space: nowrap; +} + +.schedule-countdown.urgent { + background: #fff0d6; + color: #8b5a14; +} + +.schedule-countdown.overdue { + background: #fde4df; + color: #b42318; +} + .vote-meta { display: flex; flex-wrap: wrap; diff --git a/miniprogram/pages/announcement-detail/index.js b/miniprogram/pages/announcement-detail/index.js new file mode 100644 index 0000000..a1b2a3f --- /dev/null +++ b/miniprogram/pages/announcement-detail/index.js @@ -0,0 +1,45 @@ +const { get } = require("../../utils/api"); +const { showError } = require("../../utils/page-helpers"); + +function formatDateTime(value) { + if (!value) return ""; + return String(value).replace("T", " ").slice(0, 16); +} + +Page({ + data: { + id: null, + item: null, + loading: false + }, + + onLoad(options) { + wx.setNavigationBarTitle({ title: "公告详情" }); + this.setData({ id: options.id || null }); + this.load(options.id); + }, + + async onPullDownRefresh() { + await this.load(this.data.id); + wx.stopPullDownRefresh(); + }, + + async load(id) { + if (!id) return; + this.setData({ loading: true }); + try { + const item = await get(`/api/announcements/${id}`); + this.setData({ + item: { + ...item, + created_at_text: formatDateTime(item.created_at), + updated_at_text: formatDateTime(item.updated_at) + } + }); + } catch (error) { + showError(error, "加载公告失败"); + } finally { + this.setData({ loading: false }); + } + } +}); diff --git a/miniprogram/pages/announcement-detail/index.json b/miniprogram/pages/announcement-detail/index.json new file mode 100644 index 0000000..a97367d --- /dev/null +++ b/miniprogram/pages/announcement-detail/index.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} diff --git a/miniprogram/pages/announcement-detail/index.wxml b/miniprogram/pages/announcement-detail/index.wxml new file mode 100644 index 0000000..6e2d3eb --- /dev/null +++ b/miniprogram/pages/announcement-detail/index.wxml @@ -0,0 +1,22 @@ + + + ANNOUNCEMENT + {{item.title}} + {{item.author_name}} · {{item.created_at_text}} + 置顶公告 + + + + {{item.content || "暂无内容"}} + + 发布 {{item.created_at_text}} + 更新 {{item.updated_at_text}} + + + + + + + 未找到公告 + + diff --git a/miniprogram/pages/announcement-detail/index.wxss b/miniprogram/pages/announcement-detail/index.wxss new file mode 100644 index 0000000..fbc5732 --- /dev/null +++ b/miniprogram/pages/announcement-detail/index.wxss @@ -0,0 +1,40 @@ +.announcement-pin { + position: relative; + display: inline-flex; + align-items: center; + min-height: 48rpx; + margin-top: 28rpx; + padding: 0 22rpx; + border: 1rpx solid rgba(255, 248, 237, 0.18); + border-radius: 999rpx; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 248, 237, 0.9); + font-size: 24rpx; + font-weight: 650; +} + +.announcement-article { + margin-top: 30rpx; + padding: 34rpx; + border-radius: 30rpx; + background: rgba(255, 252, 247, 0.94); + box-shadow: 0 16rpx 42rpx rgba(68, 39, 27, 0.07); +} + +.announcement-content { + color: #4f3930; + font-size: 28rpx; + line-height: 1.7; + white-space: pre-wrap; +} + +.announcement-meta { + display: flex; + flex-wrap: wrap; + gap: 10rpx 22rpx; + margin-top: 30rpx; + padding-top: 22rpx; + border-top: 1rpx solid rgba(121, 84, 54, 0.1); + color: #8a7b70; + font-size: 23rpx; +} diff --git a/miniprogram/pages/home/index.js b/miniprogram/pages/home/index.js index ce2bdb4..bd068ff 100644 --- a/miniprogram/pages/home/index.js +++ b/miniprogram/pages/home/index.js @@ -27,6 +27,18 @@ function scheduleTypeText(type) { }[type] || type || "排期"; } +function countdownText(value) { + if (!value) return ""; + const target = new Date(value).getTime(); + if (Number.isNaN(target)) return ""; + const now = Date.now(); + const diffDays = Math.ceil((target - now) / (24 * 60 * 60 * 1000)); + if (diffDays < 0) return `已逾期 ${Math.abs(diffDays)} 天`; + if (diffDays === 0) return "今天截止"; + if (diffDays === 1) return "明天截止"; + return `还有 ${diffDays} 天`; +} + Page({ data: { className: "HKU ICB", @@ -77,10 +89,6 @@ Page({ names.push("votes"); tasks.push(get("/api/votes/", { page_size: 3, class_id: classId })); } - if (isModuleEnabled("timeline", enabledModules)) { - names.push("timelines"); - tasks.push(get("/api/timeline/", { page_size: 3, class_id: classId })); - } const results = await Promise.all(tasks); const next = { announcements: [], schedules: [], votes: [], timelines: [] }; @@ -92,7 +100,8 @@ Page({ next.schedules = (value || []).map((item) => ({ ...item, schedule_time_text: formatScheduleTime(item), - schedule_type_text: scheduleTypeText(item.type) + schedule_type_text: scheduleTypeText(item.type), + countdown_text: item.type === "deadline" ? countdownText(item.start_time) : "" })); } if (name === "votes") { @@ -115,7 +124,7 @@ Page({ label: "下一项排期", title: schedule.title, detail: schedule.schedule_time_text, - badge: schedule.schedule_type_text + badge: schedule.countdown_text || schedule.schedule_type_text }); } if (pendingVotes.length) { @@ -170,6 +179,10 @@ Page({ wx.navigateTo({ url: `/pages/vote-detail/index?id=${id}` }); return; } + if (type === "announcements") { + wx.navigateTo({ url: `/pages/announcement-detail/index?id=${id}` }); + return; + } wx.navigateTo({ url: `/pages/module/index?module=${type}` }); }, diff --git a/miniprogram/pages/home/index.wxml b/miniprogram/pages/home/index.wxml index f9a3b62..8e2905f 100644 --- a/miniprogram/pages/home/index.wxml +++ b/miniprogram/pages/home/index.wxml @@ -34,23 +34,7 @@ - - - 班级动态 - 浏览 - - - - - - {{item.title}} - {{item.author_name}} - - - - - - + 暂无可展示内容 diff --git a/miniprogram/pages/interact/index.js b/miniprogram/pages/interact/index.js index dcb4c86..cc46176 100644 --- a/miniprogram/pages/interact/index.js +++ b/miniprogram/pages/interact/index.js @@ -1,16 +1,279 @@ const { requireLogin } = require("../../utils/auth"); -const { visibleModules } = require("../../utils/modules"); -const { getEnabledModules } = require("../../utils/page-helpers"); +const { del, get, post } = require("../../utils/api"); +const { getEnabledModules, getActiveClassId, showError } = require("../../utils/page-helpers"); +const { isModuleEnabled } = require("../../utils/modules"); + +function initialOf(name) { + return String(name || "班").slice(0, 1); +} + +function formatTime(value) { + return String(value || "").replace("T", " ").slice(0, 16); +} + +function sameId(left, right) { + return Number(left) === Number(right); +} + +function datasetBoolean(value) { + return value === true || value === "true" || value === 1 || value === "1"; +} + +function normalizeTimeline(item, comments = [], currentUserId = null) { + return { + ...item, + author_initial: initialOf(item.author_name), + created_at_text: formatTime(item.created_at), + like_action_text: item.has_liked ? "已赞" : "赞", + like_action_class: item.has_liked ? "active" : "", + comments: comments.map((comment) => ({ + ...comment, + created_at_text: formatTime(comment.created_at), + can_delete: sameId(currentUserId, comment.author_id) + })) + }; +} Page({ - data: { modules: [] }, + data: { + timelines: [], + timelineEnabled: false, + commentPostId: null, + commentText: "", + replyToName: "", + commentPlaceholder: "写评论", + commentInputFocus: false, + keyboardOpen: false, + loading: false, + likingId: null, + commenting: false + }, onShow() { if (!requireLogin()) return; - this.setData({ modules: visibleModules("interact", getEnabledModules()) }); + this.load(); }, - openModule(event) { - wx.navigateTo({ url: `/pages/module/index?module=${event.currentTarget.dataset.key}` }); + async load() { + const enabledModules = getEnabledModules(); + const classId = getActiveClassId(); + const timelineEnabled = isModuleEnabled("timeline", enabledModules); + if (!timelineEnabled) { + this.setData({ timelines: [], timelineEnabled: false }); + return; + } + this.setData({ loading: true }); + try { + const currentUser = getApp().globalData.user || wx.getStorageSync("auth_user") || {}; + const res = await get("/api/timeline/", { page_size: 20, class_id: classId }); + const commentResults = await Promise.all( + (res.items || []).map((item) => + get(`/api/timeline/${item.id}/comments`, { page_size: 6 }).catch(() => ({ items: [] })) + ) + ); + const timelines = (res.items || []).map((item, index) => ( + normalizeTimeline(item, commentResults[index].items || [], currentUser.id) + )); + this.setData({ timelines, timelineEnabled: true }); + } catch (error) { + showError(error); + } finally { + this.setData({ loading: false }); + } + }, + + openTimeline(event) { + wx.navigateTo({ url: `/pages/timeline-detail/index?id=${event.currentTarget.dataset.id}` }); + }, + + openCompose() { + wx.navigateTo({ url: "/pages/timeline-create/index" }); + }, + + previewImage(event) { + const current = event.currentTarget.dataset.src; + const postId = Number(event.currentTarget.dataset.postId); + const postItem = this.data.timelines.find((item) => item.id === postId); + const urls = postItem && postItem.image_urls ? postItem.image_urls : []; + if (!current || !urls.length) return; + wx.previewImage({ current, urls }); + }, + + async toggleLike(event) { + const id = Number(event.currentTarget.dataset.id); + if (!id || this.data.likingId) return; + this.setData({ likingId: id }); + try { + const result = await post(`/api/timeline/${id}/like`); + const timelines = this.data.timelines.map((item) => { + if (item.id !== id) return item; + return normalizeTimeline({ + ...item, + has_liked: Boolean(result.liked), + like_count: Number(result.like_count || 0) + }, item.comments || [], (getApp().globalData.user || wx.getStorageSync("auth_user") || {}).id); + }); + this.setData({ timelines }); + } catch (error) { + showError(error, "操作失败"); + } finally { + this.setData({ likingId: null }); + } + }, + + openComment(event) { + const id = Number(event.currentTarget.dataset.id); + const postItem = this.data.timelines.find((item) => item.id === id); + this.setData({ + commentPostId: id, + commentText: "", + replyToName: "", + commentPlaceholder: postItem ? `评论 ${postItem.author_name}` : "写评论", + commentInputFocus: false + }, () => { + this.focusCommentInput(); + }); + }, + + replyComment(event) { + const postId = Number(event.currentTarget.dataset.postId); + const commentId = Number(event.currentTarget.dataset.commentId); + const name = event.currentTarget.dataset.name || ""; + const canDelete = datasetBoolean(event.currentTarget.dataset.canDelete); + if (canDelete && commentId) { + this.openOwnCommentActions(postId, commentId, name); + return; + } + this.startReply(postId, name); + }, + + startReply(postId, name) { + this.setData({ + commentPostId: postId, + commentText: "", + replyToName: name, + commentPlaceholder: name ? `回复 ${name}` : "写评论", + commentInputFocus: false + }, () => { + this.focusCommentInput(); + }); + }, + + closeComment() { + if (this.data.commenting) return; + this.setData({ + commentPostId: null, + commentText: "", + replyToName: "", + commentInputFocus: false, + keyboardOpen: false + }); + }, + + noop() {}, + + onCommentInput(event) { + this.setData({ commentText: event.detail.value }); + }, + + onCommentFocus(event) { + const height = event.detail.height || 0; + this.setData({ + commentInputFocus: true, + keyboardOpen: height > 0 + }); + }, + + onKeyboardHeightChange(event) { + const height = event.detail.height || 0; + this.setData({ keyboardOpen: height > 0 }); + }, + + onCommentBlur() { + this.setData({ commentInputFocus: false, keyboardOpen: false }); + }, + + focusCommentInput() { + const focus = () => { + if (this.data.commentPostId) { + this.setData({ commentInputFocus: true }); + } + }; + if (wx.nextTick) { + wx.nextTick(() => setTimeout(focus, 120)); + return; + } + setTimeout(focus, 120); + }, + + async submitComment() { + const postId = this.data.commentPostId; + const text = this.data.commentText.trim(); + if (!postId || !text) { + wx.showToast({ title: "请输入评论", icon: "none" }); + return; + } + const content = this.data.replyToName ? `回复 @${this.data.replyToName}:${text}` : text; + this.setData({ commenting: true }); + try { + await post(`/api/timeline/${postId}/comments`, { content }); + const detail = await get(`/api/timeline/${postId}`); + const currentUser = getApp().globalData.user || wx.getStorageSync("auth_user") || {}; + const timelines = this.data.timelines.map((item) => ( + item.id === postId ? normalizeTimeline(detail, detail.comments || [], currentUser.id) : item + )); + this.setData({ + timelines, + commentPostId: null, + commentText: "", + replyToName: "", + commentInputFocus: false, + keyboardOpen: false + }); + wx.showToast({ title: "已评论", icon: "success" }); + } catch (error) { + showError(error, "评论失败"); + } finally { + this.setData({ commenting: false }); + } + }, + + deleteComment(postId, commentId) { + wx.showModal({ + title: "删除评论", + content: "确认删除这条评论?", + confirmText: "删除", + confirmColor: "#b42318", + success: async (res) => { + if (!res.confirm) return; + try { + await del(`/api/timeline/comments/${commentId}`); + const detail = await get(`/api/timeline/${postId}`); + const currentUser = getApp().globalData.user || wx.getStorageSync("auth_user") || {}; + const timelines = this.data.timelines.map((item) => ( + item.id === postId ? normalizeTimeline(detail, detail.comments || [], currentUser.id) : item + )); + this.setData({ timelines }); + wx.showToast({ title: "已删除", icon: "success" }); + } catch (error) { + showError(error, "删除失败"); + } + } + }); + }, + + openOwnCommentActions(postId, commentId, name) { + wx.showActionSheet({ + itemList: ["回复", "删除评论"], + itemColor: "#6b1f2b", + success: (res) => { + if (res.tapIndex === 0) { + this.startReply(postId, name); + } + if (res.tapIndex === 1) { + this.deleteComment(postId, commentId); + } + } + }); } }); diff --git a/miniprogram/pages/interact/index.json b/miniprogram/pages/interact/index.json index c54080a..7e74b13 100644 --- a/miniprogram/pages/interact/index.json +++ b/miniprogram/pages/interact/index.json @@ -1,3 +1,3 @@ { - "navigationBarTitleText": "互动" + "navigationBarTitleText": "班级圈" } diff --git a/miniprogram/pages/interact/index.wxml b/miniprogram/pages/interact/index.wxml index 4deee97..b9221c6 100644 --- a/miniprogram/pages/interact/index.wxml +++ b/miniprogram/pages/interact/index.wxml @@ -1,21 +1,48 @@ - - CLASS INTERACTION - 互动协作 - 轻量表达、投票决策和阅读分享,适合在手机上快速完成。 + + + 班级圈 + 同学近况、照片和想法都在这里 + + 发布 - - 可用互动 - 按权限开放 - - - - {{item.icon}} - {{item.title}} - {{item.desc}} + + + + {{item.author_initial}} + + {{item.author_name}} + {{item.title}} + {{item.content}} + + + + + {{item.created_at_text}} + + {{item.like_action_text}} {{item.like_count}} + 评论 {{item.comment_count}} + + + + 赞 {{item.like_count}} + + {{comment.author_name}} + :{{comment.content}} + + + - - 当前班级暂无开放的互动模块 + + {{timelineEnabled ? "还没有动态,先发一条吧" : "当前班级未开放班级动态"}} + 发布动态 + + + + + + + diff --git a/miniprogram/pages/interact/index.wxss b/miniprogram/pages/interact/index.wxss index 8b13789..ae4eb76 100644 --- a/miniprogram/pages/interact/index.wxss +++ b/miniprogram/pages/interact/index.wxss @@ -1 +1,245 @@ +.circle-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24rpx; + margin-bottom: 22rpx; + padding: 26rpx 4rpx 12rpx; +} +.circle-title { + color: #2f211c; + font-size: 44rpx; + font-weight: 780; + line-height: 1.15; +} + +.circle-subtitle { + margin-top: 8rpx; + color: #8a7b70; + font-size: 24rpx; +} + +.circle-compose { + flex: none; + min-width: 108rpx; + height: 58rpx; + border-radius: 999rpx; + background: #6b1f2b; + color: #fff8ed; + font-size: 25rpx; + font-weight: 720; + line-height: 58rpx; + text-align: center; +} + +.circle-list { + border-top: 1rpx solid rgba(121, 84, 54, 0.1); +} + +.circle-post { + display: flex; + gap: 20rpx; + padding: 28rpx 0; + border-bottom: 1rpx solid rgba(121, 84, 54, 0.1); +} + +.circle-avatar { + flex: none; + width: 76rpx; + height: 76rpx; + border-radius: 18rpx; + background: linear-gradient(145deg, #6b1f2b, #d6a653); + color: #fff8ed; + font-size: 30rpx; + font-weight: 780; + line-height: 76rpx; + text-align: center; +} + +.circle-body { + flex: 1; + min-width: 0; +} + +.circle-author { + color: #53698e; + font-size: 28rpx; + font-weight: 760; +} + +.circle-post-title { + margin-top: 6rpx; + color: #2f211c; + font-size: 30rpx; + font-weight: 760; + line-height: 1.35; +} + +.circle-content { + margin-top: 8rpx; + color: #3d2c25; + font-size: 28rpx; + line-height: 1.55; + white-space: pre-wrap; +} + +.circle-images { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8rpx; + margin-top: 14rpx; + max-width: 520rpx; +} + +.circle-images image { + width: 100%; + height: 156rpx; + border-radius: 8rpx; + background: #efe0ca; +} + +.circle-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18rpx; + margin-top: 14rpx; + color: #9a8c80; + font-size: 23rpx; +} + +.circle-actions { + display: flex; + align-items: center; + gap: 10rpx; + flex: none; +} + +.circle-action { + min-height: 44rpx; + padding: 0 16rpx; + border-radius: 999rpx; + background: #f1e4d4; + color: #725d4d; + font-size: 23rpx; + font-weight: 700; + line-height: 44rpx; +} + +.circle-action.active { + background: #e7efff; + color: #53698e; +} + +.circle-feedback { + position: relative; + margin-top: 12rpx; + padding: 14rpx 16rpx; + border-radius: 12rpx; + background: #f1e4d4; +} + +.circle-feedback::before { + content: ""; + position: absolute; + top: -10rpx; + left: 24rpx; + width: 0; + height: 0; + border-left: 10rpx solid transparent; + border-right: 10rpx solid transparent; + border-bottom: 10rpx solid #f1e4d4; +} + +.circle-likes { + color: #53698e; + font-size: 24rpx; + font-weight: 720; + line-height: 1.45; +} + +.circle-comment { + margin-top: 6rpx; + color: #4f3930; + font-size: 24rpx; + line-height: 1.5; +} + +.circle-comment:first-child { + margin-top: 0; +} + +.circle-comment-name { + color: #53698e; + font-weight: 720; +} + +.empty-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 66rpx; + margin-top: 22rpx; + padding: 0 26rpx; + border-radius: 999rpx; + background: #6b1f2b; + color: #fff8ed; + font-size: 24rpx; + font-weight: 700; +} + +.comment-mask { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 50; + display: flex; + align-items: flex-end; + background: rgba(47, 33, 28, 0.16); + transition: bottom 0.18s ease; +} + +.comment-bar { + display: flex; + gap: 14rpx; + width: 100%; + box-sizing: border-box; + margin: 0; + padding: 14rpx 14rpx calc(18rpx + env(safe-area-inset-bottom)); + border-radius: 28rpx 28rpx 0 0; + background: #fffaf3; + box-shadow: 0 14rpx 38rpx rgba(47, 33, 28, 0.18); +} + +.comment-mask.keyboard-open .comment-bar { + padding-bottom: 14rpx; + border-radius: 0; +} + +.comment-bar input { + flex: 1; + min-width: 0; + height: 74rpx; + padding: 0 22rpx; + border-radius: 20rpx; + background: #f1e4d4; + color: #2f211c; + font-size: 26rpx; +} + +.comment-bar button { + width: 152rpx; + flex: none; + height: 74rpx; + margin: 0; + padding: 0; + border-radius: 20rpx; + background: #6b1f2b; + color: #fff8ed; + font-size: 25rpx; + font-weight: 720; + line-height: 74rpx; +} diff --git a/miniprogram/pages/member-detail/index.js b/miniprogram/pages/member-detail/index.js index b53db73..f986af3 100644 --- a/miniprogram/pages/member-detail/index.js +++ b/miniprogram/pages/member-detail/index.js @@ -5,7 +5,7 @@ Page({ data: { member: null, loading: false }, onLoad(options) { - wx.setNavigationBarTitle({ title: "同学资料" }); + wx.setNavigationBarTitle({ title: "成员资料" }); this.load(options.id); }, @@ -14,14 +14,17 @@ Page({ this.setData({ loading: true }); try { const member = await get(`/api/directory/${id}`); + const isTeacher = member.membership_role === "teacher"; + const isCommittee = !isTeacher && Boolean(member.committee_role); this.setData({ member: { ...member, - role_text: member.membership_role === "teacher" ? "老师" : "同学", - role_mark: member.membership_role === "teacher" ? "师" : "同", - class_role_text: member.membership_role === "teacher" + role_text: isTeacher ? "老师" : (isCommittee ? member.committee_role : ""), + show_role_text: isTeacher || isCommittee, + role_mark: isTeacher ? "师" : (isCommittee ? "委" : "成"), + class_role_text: isTeacher ? "老师" - : member.committee_role || "同学" + : member.committee_role || "班级成员" } }); } catch (error) { diff --git a/miniprogram/pages/member-detail/index.wxml b/miniprogram/pages/member-detail/index.wxml index bea2bb8..c6d5476 100644 --- a/miniprogram/pages/member-detail/index.wxml +++ b/miniprogram/pages/member-detail/index.wxml @@ -1,50 +1,44 @@ - - {{member.role_text}} - {{member.name}} - {{member.company || "公司未填写"}} · {{member.position || "职位未填写"}} + + {{member.role_mark}} + + + {{member.name}} + {{member.role_text}} + + {{member.company || "公司未填写"}} + {{member.position || "职位未填写"}} + - - - - - - 行业 - {{member.industry || "未填写"}} - - + + + 微信 + {{member.wechat_id || "未填写"}} - - - - - 班级角色 - {{member.class_role_text}} - - - - - - - - 微信 - {{member.wechat_id || "未填写"}} - - - - - - - - 电话 - {{member.phone || "未填写"}} - - - - - 简介 - {{member.bio || "暂无简介"}} + + + 电话 + {{member.phone || "未填写"}} + + + 职业信息 + + + 行业 + {{member.industry || "未填写"}} + + + 班级角色 + {{member.class_role_text}} + + + + + + 个人简介 + {{member.bio || "暂无简介"}} + diff --git a/miniprogram/pages/member-detail/index.wxss b/miniprogram/pages/member-detail/index.wxss index 8b13789..a479a93 100644 --- a/miniprogram/pages/member-detail/index.wxss +++ b/miniprogram/pages/member-detail/index.wxss @@ -1 +1,141 @@ +.member-profile-hero { + display: flex; + align-items: center; + gap: 26rpx; + padding: 34rpx; + border: 1rpx solid rgba(107, 31, 43, 0.12); + border-radius: 34rpx; + background: linear-gradient(145deg, #fffaf3 0%, #f1e4d4 100%); + box-shadow: 0 18rpx 48rpx rgba(68, 39, 27, 0.08); +} +.member-profile-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 112rpx; + height: 112rpx; + flex: none; + border-radius: 34rpx; + background: linear-gradient(145deg, #6b1f2b, #d6a653); + color: #fff8ed; + font-size: 38rpx; + font-weight: 780; +} + +.member-profile-main { + flex: 1; + min-width: 0; +} + +.member-profile-top { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14rpx; +} + +.member-profile-name { + color: #2f211c; + font-size: 42rpx; + font-weight: 780; + line-height: 1.2; +} + +.member-profile-badge { + min-height: 40rpx; + padding: 0 16rpx; + border-radius: 999rpx; + background: #d6a653; + color: #3a221d; + font-size: 22rpx; + font-weight: 760; + line-height: 40rpx; +} + +.member-profile-subtitle { + margin-top: 10rpx; + color: #756458; + font-size: 25rpx; + line-height: 1.45; +} + +.member-contact-card { + display: flex; + margin-top: 28rpx; + padding: 26rpx; + border-radius: 30rpx; + background: #fffcf7; + box-shadow: 0 14rpx 36rpx rgba(68, 39, 27, 0.07); +} + +.contact-item { + flex: 1; + min-width: 0; +} + +.contact-label, +.member-info-label { + color: #8a7b70; + font-size: 23rpx; + font-weight: 650; +} + +.contact-value { + margin-top: 12rpx; + color: #2f211c; + font-size: 29rpx; + font-weight: 720; + line-height: 1.35; + word-break: break-all; +} + +.contact-divider { + width: 1rpx; + margin: 0 24rpx; + background: rgba(121, 84, 54, 0.12); +} + +.member-section { + margin-top: 24rpx; + padding: 28rpx; + border-radius: 28rpx; + background: rgba(255, 252, 247, 0.92); +} + +.member-section-title { + color: #3a221d; + font-size: 30rpx; + font-weight: 760; +} + +.member-info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18rpx; + margin-top: 20rpx; +} + +.member-info-cell { + min-height: 118rpx; + box-sizing: border-box; + padding: 20rpx; + border-radius: 22rpx; + background: #f7f0e8; +} + +.member-info-value { + margin-top: 12rpx; + color: #2f211c; + font-size: 27rpx; + font-weight: 720; + line-height: 1.4; +} + +.member-bio { + margin-top: 14rpx; + color: #5b493f; + font-size: 27rpx; + line-height: 1.7; + white-space: pre-wrap; +} diff --git a/miniprogram/pages/module/index.js b/miniprogram/pages/module/index.js index ef3c200..933adaa 100644 --- a/miniprogram/pages/module/index.js +++ b/miniprogram/pages/module/index.js @@ -36,6 +36,35 @@ function scheduleTypeText(type) { }[type] || type || "排期"; } +function parseDate(value) { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + return date; +} + +function startOfLocalDay(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); +} + +function countdownText(item) { + if (!item || item.type !== "deadline") return ""; + const targetDate = parseDate(item.start_time); + if (!targetDate) return ""; + const diffDays = Math.round((startOfLocalDay(targetDate) - startOfLocalDay(new Date())) / (24 * 60 * 60 * 1000)); + if (diffDays < 0) return `已逾期 ${Math.abs(diffDays)} 天`; + if (diffDays === 0) return "今天截止"; + if (diffDays === 1) return "明天截止"; + return `还有 ${diffDays} 天`; +} + +function countdownClass(text) { + if (!text) return ""; + if (text.indexOf("已逾期") === 0) return "overdue"; + if (text === "今天截止" || text === "明天截止") return "urgent"; + return ""; +} + Page({ data: { moduleKey: "", @@ -111,28 +140,36 @@ Page({ const currentUser = getApp().globalData.user || {}; const currentUserId = currentUser.id; const formatAmount = (value) => Number(value || 0).toFixed(2); - const items = rawItems.map((item) => ({ - ...item, - can_delete: this.data.moduleKey === "timeline" && item.author_id === currentUserId, - initial: String(item.name || item.author_name || this.data.title || "项").slice(0, 1), - member_role_text: item.membership_role === "teacher" ? "老师" : "同学", - member_role_class: item.membership_role === "teacher" ? "teacher" : "student", - show_student_id: item.membership_role !== "teacher" && item.student_id, - schedule_day: item.start_time ? String(item.start_time).slice(8, 10) : "", - schedule_month: item.start_time ? `${String(item.start_time).slice(5, 7)}月` : "", - schedule_time_text: this.data.moduleKey === "schedule" ? formatScheduleTime(item) : "", - schedule_type_text: this.data.moduleKey === "schedule" ? scheduleTypeText(item.type) : "", - vote_status_text: item.status === "open" ? "进行中" : "已关闭", - vote_type_text: item.vote_type === "multiple" ? `多选,最多 ${item.max_choices || 1} 项` : "单选", - vote_action_text: item.has_voted ? "已参与" : "待参与", - vote_pill_class: item.has_voted ? "done" : "", - vote_options_text: Array.isArray(item.options) ? `${item.options.length} 个选项` : "", - committee_text: item.committee_role ? ` · ${item.committee_role}` : "", - fund_type_text: item.type === "income" ? "收入" : "支出", - fund_type_class: item.type === "income" ? "income" : "expense", - amount_text: formatAmount(item.amount), - image_urls: Array.isArray(item.image_urls) ? item.image_urls : [] - })); + const items = rawItems.map((item) => { + const isTeacher = item.membership_role === "teacher"; + const isCommittee = !isTeacher && Boolean(item.committee_role); + const scheduleCountdownText = this.data.moduleKey === "schedule" ? countdownText(item) : ""; + return { + ...item, + can_delete: this.data.moduleKey === "timeline" && item.author_id === currentUserId, + initial: String(item.name || item.author_name || this.data.title || "项").slice(0, 1), + member_role_text: isTeacher ? "老师" : (isCommittee ? item.committee_role : ""), + member_role_class: isTeacher ? "teacher" : (isCommittee ? "committee" : ""), + show_member_role: isTeacher || isCommittee, + show_student_id: item.membership_role !== "teacher" && item.student_id, + schedule_day: item.start_time ? String(item.start_time).slice(8, 10) : "", + schedule_month: item.start_time ? `${String(item.start_time).slice(5, 7)}月` : "", + schedule_time_text: this.data.moduleKey === "schedule" ? formatScheduleTime(item) : "", + schedule_type_text: this.data.moduleKey === "schedule" ? scheduleTypeText(item.type) : "", + schedule_countdown_text: scheduleCountdownText, + schedule_countdown_class: countdownClass(scheduleCountdownText), + vote_status_text: item.status === "open" ? "进行中" : "已关闭", + vote_type_text: item.vote_type === "multiple" ? `多选,最多 ${item.max_choices || 1} 项` : "单选", + vote_action_text: item.has_voted ? "已参与" : "待参与", + vote_pill_class: item.has_voted ? "done" : "", + vote_options_text: Array.isArray(item.options) ? `${item.options.length} 个选项` : "", + committee_text: "", + fund_type_text: item.type === "income" ? "收入" : "支出", + fund_type_class: item.type === "income" ? "income" : "expense", + amount_text: formatAmount(item.amount), + image_urls: Array.isArray(item.image_urls) ? item.image_urls : [] + }; + }); const fundStats = stats ? { ...stats, total_income_text: formatAmount(stats.total_income), @@ -175,6 +212,10 @@ Page({ } if (key === "fund") { wx.navigateTo({ url: `/pages/fund-detail/index?id=${id}` }); + return; + } + if (key === "announcements") { + wx.navigateTo({ url: `/pages/announcement-detail/index?id=${id}` }); } }, diff --git a/miniprogram/pages/module/index.wxml b/miniprogram/pages/module/index.wxml index a18db4d..c2528da 100644 --- a/miniprogram/pages/module/index.wxml +++ b/miniprogram/pages/module/index.wxml @@ -57,7 +57,7 @@ {{item.name}} - {{item.member_role_text}} + {{item.member_role_text}} {{item.company || "公司未填写"}} · {{item.position || "职位未填写"}} {{item.industry || "行业未填写"}}{{item.committee_text}} @@ -75,7 +75,10 @@ {{item.location || "地点待定"}} {{item.schedule_time_text}} - {{item.schedule_type_text}} + + {{item.schedule_countdown_text}} + {{item.schedule_type_text}} + diff --git a/miniprogram/pages/schedule-detail/index.js b/miniprogram/pages/schedule-detail/index.js index bf00bb3..388a8e2 100644 --- a/miniprogram/pages/schedule-detail/index.js +++ b/miniprogram/pages/schedule-detail/index.js @@ -14,6 +14,27 @@ function scheduleTypeText(type) { }[type] || type || "排期"; } +function countdownText(value) { + if (!value) return ""; + const targetDate = new Date(value); + if (Number.isNaN(targetDate.getTime())) return ""; + const today = new Date(); + const targetDay = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime(); + const todayDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); + const diffDays = Math.round((targetDay - todayDay) / (24 * 60 * 60 * 1000)); + if (diffDays < 0) return `已逾期 ${Math.abs(diffDays)} 天`; + if (diffDays === 0) return "今天截止"; + if (diffDays === 1) return "明天截止"; + return `还有 ${diffDays} 天`; +} + +function countdownClass(text) { + if (!text) return ""; + if (text.indexOf("已逾期") === 0) return "overdue"; + if (text === "今天截止" || text === "明天截止") return "urgent"; + return ""; +} + Page({ data: { item: null, loading: false }, @@ -27,13 +48,18 @@ Page({ this.setData({ loading: true }); try { const item = await get(`/api/schedule/${id}`); + const countdown_text = item.type === "deadline" ? countdownText(item.start_time) : ""; this.setData({ item: { ...item, start_time_text: formatDateTime(item.start_time), end_time_text: formatDateTime(item.end_time), type_text: scheduleTypeText(item.type), - start_label: item.type === "deadline" ? "截止时间" : "开始时间" + start_label: item.type === "deadline" ? "截止时间" : "开始时间", + countdown_text, + countdown_class: countdownClass(countdown_text), + location_text: item.location || "地点待定", + description_text: item.description || "暂无说明" } }); } catch (error) { diff --git a/miniprogram/pages/schedule-detail/index.wxml b/miniprogram/pages/schedule-detail/index.wxml index ed416bb..1fa1c60 100644 --- a/miniprogram/pages/schedule-detail/index.wxml +++ b/miniprogram/pages/schedule-detail/index.wxml @@ -1,41 +1,27 @@ - - SCHEDULE - {{item.title}} - {{item.location || "地点待定"}} + + + {{item.type_text}} + {{item.countdown_text}} + + {{item.title}} + {{item.location_text}} - - - - - - 类型 - {{item.type_text}} - - + + + {{item.start_label}} + {{item.start_time_text}} - - - - - {{item.start_label}} - {{item.start_time_text}} - - - - - - - - 结束时间 - {{item.end_time_text}} - - - - - 说明 - {{item.description || "暂无说明"}} + + + 结束时间 + {{item.end_time_text}} + + + 详情说明 + {{item.description_text}} + diff --git a/miniprogram/pages/schedule-detail/index.wxss b/miniprogram/pages/schedule-detail/index.wxss index 8b13789..be22b6e 100644 --- a/miniprogram/pages/schedule-detail/index.wxss +++ b/miniprogram/pages/schedule-detail/index.wxss @@ -1 +1,121 @@ +.schedule-hero { + position: relative; + overflow: hidden; + padding: 34rpx; + border: 1rpx solid rgba(107, 31, 43, 0.14); + border-radius: 34rpx; + background: linear-gradient(145deg, #fffaf3 0%, #f1e4d4 100%); + box-shadow: 0 18rpx 48rpx rgba(68, 39, 27, 0.08); +} +.schedule-hero-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16rpx; +} + +.schedule-type-chip { + min-height: 44rpx; + padding: 0 18rpx; + border-radius: 999rpx; + background: #6b1f2b; + color: #fff8ed; + font-size: 23rpx; + font-weight: 720; + line-height: 44rpx; +} + +.schedule-title { + margin-top: 26rpx; + color: #2f211c; + font-size: 42rpx; + font-weight: 780; + line-height: 1.25; +} + +.schedule-location { + margin-top: 14rpx; + color: #756458; + font-size: 26rpx; + line-height: 1.5; +} + +.deadline-pill { + flex: none; + min-height: 48rpx; + padding: 0 22rpx; + border-radius: 999rpx; + background: #e7efff; + color: #53698e; + font-size: 24rpx; + font-weight: 760; + line-height: 48rpx; +} + +.deadline-pill.urgent { + background: #fff0d6; + color: #8b5a14; +} + +.deadline-pill.overdue { + background: #fde4df; + color: #b42318; +} + +.schedule-time-card { + margin-top: 28rpx; + padding: 30rpx; + border-radius: 30rpx; + background: #fffcf7; + box-shadow: 0 14rpx 36rpx rgba(68, 39, 27, 0.07); +} + +.time-block { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24rpx; +} + +.time-label { + flex: none; + color: #8a7b70; + font-size: 24rpx; + font-weight: 650; +} + +.time-value { + color: #2f211c; + font-size: 30rpx; + font-weight: 760; + line-height: 1.35; + text-align: right; +} + +.time-divider { + height: 1rpx; + margin: 24rpx 0; + background: rgba(121, 84, 54, 0.12); +} + +.schedule-section { + margin-top: 24rpx; + padding: 28rpx; + border-radius: 28rpx; + background: rgba(255, 252, 247, 0.92); +} + +.schedule-section-title { + color: #3a221d; + font-size: 30rpx; + font-weight: 760; +} + +.schedule-description { + margin-top: 14rpx; + color: #5b493f; + font-size: 27rpx; + line-height: 1.7; + white-space: pre-wrap; +} diff --git a/miniprogram/pages/timeline-detail/index.js b/miniprogram/pages/timeline-detail/index.js index 85af290..97ca159 100644 --- a/miniprogram/pages/timeline-detail/index.js +++ b/miniprogram/pages/timeline-detail/index.js @@ -1,21 +1,55 @@ const { del, get, post } = require("../../utils/api"); const { showError } = require("../../utils/page-helpers"); +function initialOf(name) { + return String(name || "班").slice(0, 1); +} + +function formatTime(value) { + return String(value || "").replace("T", " ").slice(0, 16); +} + +function sameId(left, right) { + return Number(left) === Number(right); +} + +function datasetBoolean(value) { + return value === true || value === "true" || value === 1 || value === "1"; +} + +function normalizePost(post, currentUserId = null) { + if (!post) return null; + const comments = Array.isArray(post.comments) ? post.comments : []; + return { + ...post, + author_initial: initialOf(post.author_name), + created_at_text: formatTime(post.created_at), + like_action_text: post.has_liked ? "已赞" : "赞", + like_action_class: post.has_liked ? "active" : "", + comments: comments.map((comment) => ({ + ...comment, + created_at_text: formatTime(comment.created_at), + can_delete: sameId(currentUserId, comment.author_id) + })) + }; +} + Page({ data: { id: null, post: null, canDelete: false, - comments: [], commentText: "", - replyTo: "", - inputPlaceholder: "写下评论", + replyToName: "", + inputPlaceholder: "写评论", + inputFocus: false, + keyboardOpen: false, loading: false, submitting: false }, onLoad(options) { - wx.setNavigationBarTitle({ title: "动态详情" }); + wx.setNavigationBarTitle({ title: "班级圈" }); this.setData({ id: options.id }); this.load(); }, @@ -29,18 +63,11 @@ Page({ if (!this.data.id) return; this.setData({ loading: true }); try { - const [postDetail, commentsRes] = await Promise.all([ - get(`/api/timeline/${this.data.id}`), - get(`/api/timeline/${this.data.id}/comments`, { page_size: 50 }) - ]); - const currentUser = getApp().globalData.user || {}; + const postDetail = await get(`/api/timeline/${this.data.id}`); + const currentUser = getApp().globalData.user || wx.getStorageSync("auth_user") || {}; this.setData({ - post: postDetail, - canDelete: postDetail.author_id === currentUser.id, - comments: (commentsRes.items || []).map((item) => ({ - ...item, - initial: String(item.author_name || "评").slice(0, 1) - })) + post: normalizePost(postDetail, currentUser.id), + canDelete: postDetail.author_id === currentUser.id }); } catch (error) { showError(error, "加载动态失败"); @@ -53,8 +80,14 @@ Page({ if (!this.data.id || this.data.submitting) return; this.setData({ submitting: true }); try { - await post(`/api/timeline/${this.data.id}/like`); - await this.load(); + const result = await post(`/api/timeline/${this.data.id}/like`); + this.setData({ + post: normalizePost({ + ...this.data.post, + has_liked: Boolean(result.liked), + like_count: Number(result.like_count || 0) + }, (getApp().globalData.user || wx.getStorageSync("auth_user") || {}).id) + }); } catch (error) { showError(error, "操作失败"); } finally { @@ -66,6 +99,26 @@ Page({ this.setData({ commentText: event.detail.value }); }, + onCommentFocus(event) { + const height = event.detail.height || 0; + this.setData({ + keyboardOpen: height > 0, + inputFocus: true + }); + }, + + onKeyboardHeightChange(event) { + const height = event.detail.height || 0; + this.setData({ keyboardOpen: height > 0 }); + }, + + onCommentBlur() { + this.setData({ + keyboardOpen: false, + inputFocus: false + }); + }, + previewImage(event) { const current = event.currentTarget.dataset.src; const urls = this.data.post && this.data.post.image_urls ? this.data.post.image_urls : []; @@ -90,7 +143,7 @@ Page({ wx.showToast({ title: "已删除", icon: "success" }); const pages = getCurrentPages(); const previousPage = pages[pages.length - 2]; - if (previousPage && previousPage.setData) previousPage.setData({ needsRefresh: true }); + if (previousPage && previousPage.load) previousPage.load(); setTimeout(() => wx.navigateBack(), 500); } catch (error) { showError(error, "删除失败"); @@ -101,35 +154,76 @@ Page({ }); }, - replyComment(event) { - const name = event.currentTarget.dataset.name || ""; + openComment() { + const post = this.data.post; this.setData({ - replyTo: name, - inputPlaceholder: name ? `回复 ${name}` : "写下评论" + replyToName: "", + inputPlaceholder: post ? `评论 ${post.author_name}` : "写评论", + inputFocus: false + }, () => { + this.focusCommentInput(); + }); + }, + + replyComment(event) { + const commentId = Number(event.currentTarget.dataset.commentId); + const name = event.currentTarget.dataset.name || ""; + const canDelete = datasetBoolean(event.currentTarget.dataset.canDelete); + if (canDelete && commentId) { + this.openOwnCommentActions(commentId, name); + return; + } + this.startReply(name); + }, + + startReply(name) { + this.setData({ + replyToName: name, + inputPlaceholder: name ? `回复 ${name}` : "写评论", + inputFocus: false + }, () => { + this.focusCommentInput(); }); }, cancelReply() { this.setData({ - replyTo: "", - inputPlaceholder: "写下评论" + replyToName: "", + inputPlaceholder: "写评论", + inputFocus: false, + keyboardOpen: false }); }, + focusCommentInput() { + const focus = () => { + if (this.data.post) { + this.setData({ inputFocus: true }); + } + }; + if (wx.nextTick) { + wx.nextTick(() => setTimeout(focus, 120)); + return; + } + setTimeout(focus, 120); + }, + async submitComment() { const text = this.data.commentText.trim(); if (!text) { wx.showToast({ title: "请输入评论", icon: "none" }); return; } - const content = this.data.replyTo ? `回复 @${this.data.replyTo}:${text}` : text; + const content = this.data.replyToName ? `回复 @${this.data.replyToName}:${text}` : text; this.setData({ submitting: true }); try { await post(`/api/timeline/${this.data.id}/comments`, { content }); this.setData({ commentText: "", - replyTo: "", - inputPlaceholder: "写下评论" + replyToName: "", + inputPlaceholder: "写评论", + inputFocus: false, + keyboardOpen: false }); await this.load(); } catch (error) { @@ -137,5 +231,39 @@ Page({ } finally { this.setData({ submitting: false }); } + }, + + deleteComment(commentId) { + wx.showModal({ + title: "删除评论", + content: "确认删除这条评论?", + confirmText: "删除", + confirmColor: "#b42318", + success: async (res) => { + if (!res.confirm) return; + try { + await del(`/api/timeline/comments/${commentId}`); + await this.load(); + wx.showToast({ title: "已删除", icon: "success" }); + } catch (error) { + showError(error, "删除失败"); + } + } + }); + }, + + openOwnCommentActions(commentId, name) { + wx.showActionSheet({ + itemList: ["回复", "删除评论"], + itemColor: "#6b1f2b", + success: (res) => { + if (res.tapIndex === 0) { + this.startReply(name); + } + if (res.tapIndex === 1) { + this.deleteComment(commentId); + } + } + }); } }); diff --git a/miniprogram/pages/timeline-detail/index.wxml b/miniprogram/pages/timeline-detail/index.wxml index 7cb7d76..89b17d4 100644 --- a/miniprogram/pages/timeline-detail/index.wxml +++ b/miniprogram/pages/timeline-detail/index.wxml @@ -1,58 +1,48 @@ - - CLASS FEED + + {{post.author_initial}} + + {{post.author_name}} + {{post.created_at_text}} + ··· - {{post.title}} - {{post.author_name}} · {{post.created_at}} - - - {{post.content || "暂无正文"}} - - - - - - - {{post.has_liked ? "已点赞" : "点赞"}} · {{post.like_count}} - - - - 评论 · {{comments.length}} - - + + {{post.title}} + {{post.content}} + + + + + {{post.like_action_text}} {{post.like_count}} + 评论 {{post.comment_count}} - - - 评论 - {{comments.length}} 条 - - - - {{item.initial}} - - - {{item.author_name}} - 回复 - - {{item.content}} - - - - - 还没有评论 + - - 正在回复 {{replyTo}} - 取消 - - - - + + + 回复 {{replyToName}} + 取消 + + + + + + + + + + + 未找到动态 diff --git a/miniprogram/pages/timeline-detail/index.wxss b/miniprogram/pages/timeline-detail/index.wxss index 3e816e6..677d47f 100644 --- a/miniprogram/pages/timeline-detail/index.wxss +++ b/miniprogram/pages/timeline-detail/index.wxss @@ -1,88 +1,173 @@ -.detail-images image { - height: 210rpx; +.circle-detail-head { + display: flex; + align-items: center; + gap: 20rpx; + padding: 24rpx 0 18rpx; + border-bottom: 1rpx solid rgba(121, 84, 54, 0.1); +} + +.circle-avatar { + flex: none; + width: 76rpx; + height: 76rpx; + border-radius: 18rpx; + background: linear-gradient(145deg, #6b1f2b, #d6a653); + color: #fff8ed; + font-size: 30rpx; + font-weight: 780; + line-height: 76rpx; + text-align: center; +} + +.circle-avatar.large { + width: 88rpx; + height: 88rpx; + border-radius: 22rpx; + font-size: 34rpx; + line-height: 88rpx; +} + +.circle-head-body { + flex: 1; + min-width: 0; +} + +.circle-author { + color: #53698e; + font-size: 30rpx; + font-weight: 760; +} + +.circle-time { + margin-top: 6rpx; + color: #9a8c80; + font-size: 23rpx; } .detail-more { - position: absolute; - top: 28rpx; - right: 28rpx; - z-index: 2; + flex: none; min-width: 64rpx; height: 46rpx; border-radius: 999rpx; - background: rgba(255, 255, 255, 0.12); - color: rgba(255, 248, 237, 0.72); + background: #f1e4d4; + color: #725d4d; font-size: 32rpx; line-height: 38rpx; text-align: center; } -.detail-action-bar { - display: flex; - gap: 16rpx; - margin-top: 24rpx; +.circle-detail { + padding: 24rpx 0; } -.action-chip { - display: flex; - align-items: center; - justify-content: center; +.circle-post-title { + color: #2f211c; + font-size: 34rpx; + font-weight: 780; + line-height: 1.35; +} + +.circle-content { + margin-top: 12rpx; + color: #3d2c25; + font-size: 29rpx; + line-height: 1.65; + white-space: pre-wrap; +} + +.circle-images { + display: grid; + grid-template-columns: repeat(3, 1fr); gap: 8rpx; - min-height: 64rpx; - flex: 1; - border-radius: 22rpx; - background: #f4eadc; + margin-top: 18rpx; +} + +.circle-images image { + width: 100%; + height: 206rpx; + border-radius: 8rpx; + background: #efe0ca; +} + +.circle-actions-row { + display: flex; + gap: 12rpx; + margin-top: 20rpx; +} + +.circle-action { + min-height: 50rpx; + padding: 0 18rpx; + border-radius: 999rpx; + background: #f1e4d4; color: #725d4d; font-size: 24rpx; - font-weight: 650; + font-weight: 700; + line-height: 50rpx; } -.action-chip.active { - background: #6b1f2b; - color: #fff8ed; +.circle-action.active { + background: #e7efff; + color: #53698e; } -.action-icon { - font-size: 22rpx; - font-weight: 780; +.circle-feedback { + position: relative; + margin-top: 4rpx; + padding: 18rpx; + border-radius: 14rpx; + background: #f1e4d4; } -.comment-card { - margin-bottom: 16rpx; - border: 1rpx solid rgba(121, 84, 54, 0.1); - border-radius: 26rpx; - background: rgba(255, 252, 247, 0.96); - padding: 24rpx; +.detail-feedback { + margin-bottom: 170rpx; } -.comment-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 18rpx; +.circle-likes { + color: #53698e; + font-size: 25rpx; + font-weight: 720; + line-height: 1.45; } -.reply-link { - flex: none; - color: #8b5a36; - font-size: 24rpx; - font-weight: 650; -} - -.comment-content { +.circle-comment { margin-top: 8rpx; - color: #59463d; + color: #4f3930; font-size: 26rpx; line-height: 1.55; } +.circle-comment:first-child { + margin-top: 0; +} + +.circle-comment-name { + color: #53698e; + font-weight: 720; +} + +.comment-dock { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + box-sizing: border-box; + padding: 0 14rpx calc(18rpx + env(safe-area-inset-bottom)); + background: #fffaf3; + box-shadow: 0 -12rpx 30rpx rgba(68, 39, 27, 0.1); + transition: bottom 0.18s ease; +} + +.comment-dock.keyboard-open { + padding-bottom: 0; +} + .replying-bar { - position: sticky; - bottom: 116rpx; display: flex; align-items: center; justify-content: space-between; - margin-top: 24rpx; + margin-bottom: 10rpx; padding: 16rpx 24rpx; border-radius: 22rpx; background: #f1e4d4; @@ -91,17 +176,12 @@ } .comment-box { - position: sticky; - bottom: 20rpx; display: flex; align-items: center; gap: 14rpx; - margin-top: 32rpx; padding: 14rpx; - border: 1rpx solid rgba(121, 84, 54, 0.12); - border-radius: 28rpx; - background: rgba(255, 252, 247, 0.96); - box-shadow: 0 18rpx 42rpx rgba(68, 39, 27, 0.1); + border-radius: 0; + background: #fffaf3; } .comment-box input { @@ -111,10 +191,11 @@ padding: 0 22rpx; border-radius: 20rpx; background: #f7f0e8; + color: #2f211c; } .comment-box button { - width: 156rpx; + width: 152rpx; flex: none; height: 72rpx; margin: 0; @@ -122,6 +203,7 @@ border-radius: 20rpx; background: #6b1f2b; color: #fff7ea; - font-size: 26rpx; + font-size: 25rpx; + font-weight: 720; line-height: 72rpx; } diff --git a/miniprogram/pages/vote-detail/index.wxml b/miniprogram/pages/vote-detail/index.wxml index 994289c..f518067 100644 --- a/miniprogram/pages/vote-detail/index.wxml +++ b/miniprogram/pages/vote-detail/index.wxml @@ -39,14 +39,9 @@ - - - - - 截止时间 - {{item.deadline_text}} - - + + 截止时间 + {{item.deadline_text}} diff --git a/miniprogram/pages/vote-detail/index.wxss b/miniprogram/pages/vote-detail/index.wxss index 01c88c3..797e389 100644 --- a/miniprogram/pages/vote-detail/index.wxss +++ b/miniprogram/pages/vote-detail/index.wxss @@ -55,3 +55,26 @@ font-size: 22rpx; line-height: 1.45; } + +.vote-deadline-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24rpx; + margin-top: 24rpx; + padding: 24rpx 28rpx; + border-radius: 26rpx; + background: #fff0d6; + color: #7a4b2b; +} + +.vote-deadline-label { + font-size: 24rpx; + font-weight: 650; +} + +.vote-deadline-value { + font-size: 27rpx; + font-weight: 760; + text-align: right; +} diff --git a/miniprogram/utils/modules.js b/miniprogram/utils/modules.js index 61b5d2b..2044450 100644 --- a/miniprogram/utils/modules.js +++ b/miniprogram/utils/modules.js @@ -3,8 +3,8 @@ const MODULES = { schedule: { key: "schedule", title: "排期", desc: "课程、活动与截止日", group: "class", icon: "日" }, directory: { key: "directory", title: "成员名录", desc: "查找同学与班委", group: "class", icon: "友" }, fund: { key: "fund", title: "班费", desc: "查看公开收支账本", group: "class", icon: "账" }, - timeline: { key: "timeline", title: "班级动态", desc: "分享近况与评论互动", group: "interact", icon: "动" }, - votes: { key: "votes", title: "投票", desc: "参与班级决策", group: "interact", icon: "选" } + votes: { key: "votes", title: "投票", desc: "参与班级决策", group: "class", icon: "选" }, + timeline: { key: "timeline", title: "班级动态", desc: "分享近况与评论互动", group: "interact", icon: "动" } }; const MINI_PROGRAM_MODULE_KEYS = Object.keys(MODULES);