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}}
+
+
+
-
- 当前班级暂无开放的互动模块
+
+ {{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}} 条
-
-
-
- 还没有评论
+
+ 赞 {{post.like_count}}
+
+ 还没有点赞和评论
-
- 正在回复 {{replyTo}}
- 取消
-
-
+
+
+
+ 未找到动态
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);