1
This commit is contained in:
parent
f105cb369c
commit
02c35c146d
@ -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,
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
45
miniprogram/pages/announcement-detail/index.js
Normal file
45
miniprogram/pages/announcement-detail/index.js
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
3
miniprogram/pages/announcement-detail/index.json
Normal file
3
miniprogram/pages/announcement-detail/index.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
22
miniprogram/pages/announcement-detail/index.wxml
Normal file
22
miniprogram/pages/announcement-detail/index.wxml
Normal file
@ -0,0 +1,22 @@
|
||||
<view class="page" wx:if="{{item}}">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">ANNOUNCEMENT</view>
|
||||
<view class="hero-title">{{item.title}}</view>
|
||||
<view class="hero-subtitle">{{item.author_name}} · {{item.created_at_text}}</view>
|
||||
<view wx:if="{{item.is_pinned}}" class="announcement-pin">置顶公告</view>
|
||||
</view>
|
||||
|
||||
<view class="announcement-article">
|
||||
<view class="announcement-content">{{item.content || "暂无内容"}}</view>
|
||||
<view class="announcement-meta">
|
||||
<view>发布 {{item.created_at_text}}</view>
|
||||
<view wx:if="{{item.updated_at_text}}">更新 {{item.updated_at_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page" wx:elif="{{!loading}}">
|
||||
<view class="empty">
|
||||
<view class="muted">未找到公告</view>
|
||||
</view>
|
||||
</view>
|
||||
40
miniprogram/pages/announcement-detail/index.wxss
Normal file
40
miniprogram/pages/announcement-detail/index.wxss
Normal file
@ -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;
|
||||
}
|
||||
@ -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}` });
|
||||
},
|
||||
|
||||
|
||||
@ -34,23 +34,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{timelines.length}}" class="section">
|
||||
<view class="section-head">
|
||||
<view class="section-title">班级动态</view>
|
||||
<view class="section-action" bindtap="openModule" data-key="timeline">浏览</view>
|
||||
</view>
|
||||
<view wx:for="{{timelines}}" wx:key="id" class="card" bindtap="openTimeline" data-id="{{item.id}}">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">动</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
<view class="muted">{{item.author_name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!loading && !focusItems.length && !quickModules.length && !timelines.length}}" class="empty">
|
||||
<view wx:if="{{!loading && !focusItems.length && !quickModules.length}}" class="empty">
|
||||
<view class="muted">暂无可展示内容</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "互动"
|
||||
"navigationBarTitleText": "班级圈"
|
||||
}
|
||||
|
||||
@ -1,21 +1,48 @@
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">CLASS INTERACTION</view>
|
||||
<view class="hero-title">互动协作</view>
|
||||
<view class="hero-subtitle">轻量表达、投票决策和阅读分享,适合在手机上快速完成。</view>
|
||||
<view class="circle-head">
|
||||
<view>
|
||||
<view class="circle-title">班级圈</view>
|
||||
<view class="circle-subtitle">同学近况、照片和想法都在这里</view>
|
||||
</view>
|
||||
<view wx:if="{{timelineEnabled}}" class="circle-compose" bindtap="openCompose">发布</view>
|
||||
</view>
|
||||
<view class="section-head section">
|
||||
<view class="section-title">可用互动</view>
|
||||
<view class="section-action">按权限开放</view>
|
||||
</view>
|
||||
<view class="grid">
|
||||
<view wx:for="{{modules}}" wx:key="key" class="module-tile" data-key="{{item.key}}" bindtap="openModule">
|
||||
<view class="module-icon">{{item.icon}}</view>
|
||||
<view class="module-title">{{item.title}}</view>
|
||||
<view class="module-desc">{{item.desc}}</view>
|
||||
|
||||
<view wx:if="{{timelines.length}}" class="circle-list">
|
||||
<view wx:for="{{timelines}}" wx:key="id" class="circle-post">
|
||||
<view class="circle-avatar">{{item.author_initial}}</view>
|
||||
<view class="circle-body">
|
||||
<view class="circle-author" data-id="{{item.id}}" bindtap="openTimeline">{{item.author_name}}</view>
|
||||
<view class="circle-post-title" data-id="{{item.id}}" bindtap="openTimeline">{{item.title}}</view>
|
||||
<view wx:if="{{item.content}}" class="circle-content" data-id="{{item.id}}" bindtap="openTimeline">{{item.content}}</view>
|
||||
<view wx:if="{{item.image_urls && item.image_urls.length}}" class="circle-images">
|
||||
<image wx:for="{{item.image_urls}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" data-src="{{img}}" data-post-id="{{item.id}}" bindtap="previewImage" />
|
||||
</view>
|
||||
<view class="circle-meta">
|
||||
<view>{{item.created_at_text}}</view>
|
||||
<view class="circle-actions">
|
||||
<view class="circle-action {{item.like_action_class}}" data-id="{{item.id}}" bindtap="toggleLike">{{item.like_action_text}} {{item.like_count}}</view>
|
||||
<view class="circle-action" data-id="{{item.id}}" bindtap="openComment">评论 {{item.comment_count}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.like_count || item.comments.length}}" class="circle-feedback">
|
||||
<view wx:if="{{item.like_count}}" class="circle-likes">赞 {{item.like_count}}</view>
|
||||
<view wx:for="{{item.comments}}" wx:for-item="comment" wx:key="id" class="circle-comment" data-post-id="{{item.id}}" data-comment-id="{{comment.id}}" data-name="{{comment.author_name}}" data-can-delete="{{comment.can_delete}}" bindtap="replyComment">
|
||||
<text class="circle-comment-name">{{comment.author_name}}</text>
|
||||
<text>:{{comment.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!modules.length}}" class="empty">
|
||||
<view class="muted">当前班级暂无开放的互动模块</view>
|
||||
<view wx:else class="empty">
|
||||
<view class="muted">{{timelineEnabled ? "还没有动态,先发一条吧" : "当前班级未开放班级动态"}}</view>
|
||||
<view wx:if="{{timelineEnabled}}" class="empty-action" bindtap="openCompose">发布动态</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{commentPostId}}" class="comment-mask {{keyboardOpen ? 'keyboard-open' : ''}}" catchtap="closeComment">
|
||||
<view class="comment-bar" catchtap="noop">
|
||||
<input value="{{commentText}}" bindinput="onCommentInput" bindfocus="onCommentFocus" bindblur="onCommentBlur" bindkeyboardheightchange="onKeyboardHeightChange" placeholder="{{commentPlaceholder}}" adjust-position="{{true}}" cursor-spacing="{{0}}" focus="{{commentInputFocus}}" hold-keyboard="{{true}}" />
|
||||
<button loading="{{commenting}}" bindtap="submitComment">发送</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,50 +1,44 @@
|
||||
<view class="page" wx:if="{{member}}">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">{{member.role_text}}</view>
|
||||
<view class="hero-title">{{member.name}}</view>
|
||||
<view class="hero-subtitle">{{member.company || "公司未填写"}} · {{member.position || "职位未填写"}}</view>
|
||||
<view class="member-profile-hero">
|
||||
<view class="member-profile-avatar">{{member.role_mark}}</view>
|
||||
<view class="member-profile-main">
|
||||
<view class="member-profile-top">
|
||||
<view class="member-profile-name">{{member.name}}</view>
|
||||
<view wx:if="{{member.show_role_text}}" class="member-profile-badge">{{member.role_text}}</view>
|
||||
</view>
|
||||
<view class="member-profile-subtitle">{{member.company || "公司未填写"}}</view>
|
||||
<view class="member-profile-subtitle">{{member.position || "职位未填写"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">业</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">行业</view>
|
||||
<view class="muted">{{member.industry || "未填写"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-contact-card">
|
||||
<view class="contact-item">
|
||||
<view class="contact-label">微信</view>
|
||||
<view class="contact-value">{{member.wechat_id || "未填写"}}</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">班</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">班级角色</view>
|
||||
<view class="muted">{{member.class_role_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">微</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">微信</view>
|
||||
<view class="muted">{{member.wechat_id || "未填写"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">电</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">电话</view>
|
||||
<view class="muted">{{member.phone || "未填写"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="card-title">简介</view>
|
||||
<view class="muted">{{member.bio || "暂无简介"}}</view>
|
||||
<view class="contact-divider"></view>
|
||||
<view class="contact-item">
|
||||
<view class="contact-label">电话</view>
|
||||
<view class="contact-value">{{member.phone || "未填写"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-section">
|
||||
<view class="member-section-title">职业信息</view>
|
||||
<view class="member-info-grid">
|
||||
<view class="member-info-cell">
|
||||
<view class="member-info-label">行业</view>
|
||||
<view class="member-info-value">{{member.industry || "未填写"}}</view>
|
||||
</view>
|
||||
<view class="member-info-cell">
|
||||
<view class="member-info-label">班级角色</view>
|
||||
<view class="member-info-value">{{member.class_role_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-section">
|
||||
<view class="member-section-title">个人简介</view>
|
||||
<view class="member-bio">{{member.bio || "暂无简介"}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}` });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
<view class="row-body">
|
||||
<view class="member-title-line">
|
||||
<view class="card-title">{{item.name}}</view>
|
||||
<view class="member-role-badge {{item.member_role_class}}">{{item.member_role_text}}</view>
|
||||
<view wx:if="{{item.show_member_role}}" class="member-role-badge {{item.member_role_class}}">{{item.member_role_text}}</view>
|
||||
</view>
|
||||
<view class="muted">{{item.company || "公司未填写"}} · {{item.position || "职位未填写"}}</view>
|
||||
<view class="muted">{{item.industry || "行业未填写"}}{{item.committee_text}}</view>
|
||||
@ -75,7 +75,10 @@
|
||||
<view class="muted">{{item.location || "地点待定"}}</view>
|
||||
<view class="muted">{{item.schedule_time_text}}</view>
|
||||
</view>
|
||||
<view class="pill">{{item.schedule_type_text}}</view>
|
||||
<view class="schedule-side">
|
||||
<view wx:if="{{item.schedule_countdown_text}}" class="schedule-countdown {{item.schedule_countdown_class}}">{{item.schedule_countdown_text}}</view>
|
||||
<view class="pill">{{item.schedule_type_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{isVotes}}" class="vote-row">
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,41 +1,27 @@
|
||||
<view class="page" wx:if="{{item}}">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">SCHEDULE</view>
|
||||
<view class="hero-title">{{item.title}}</view>
|
||||
<view class="hero-subtitle">{{item.location || "地点待定"}}</view>
|
||||
<view class="schedule-hero">
|
||||
<view class="schedule-hero-top">
|
||||
<view class="schedule-type-chip">{{item.type_text}}</view>
|
||||
<view wx:if="{{item.countdown_text}}" class="deadline-pill {{item.countdown_class}}">{{item.countdown_text}}</view>
|
||||
</view>
|
||||
<view class="schedule-title">{{item.title}}</view>
|
||||
<view class="schedule-location">{{item.location_text}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">类</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">类型</view>
|
||||
<view class="muted">{{item.type_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="schedule-time-card">
|
||||
<view class="time-block primary">
|
||||
<view class="time-label">{{item.start_label}}</view>
|
||||
<view class="time-value">{{item.start_time_text}}</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">始</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">{{item.start_label}}</view>
|
||||
<view class="muted">{{item.start_time_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card" wx:if="{{item.end_time}}">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">止</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">结束时间</view>
|
||||
<view class="muted">{{item.end_time_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="card-title">说明</view>
|
||||
<view class="muted">{{item.description || "暂无说明"}}</view>
|
||||
<view wx:if="{{item.end_time}}" class="time-divider"></view>
|
||||
<view wx:if="{{item.end_time}}" class="time-block">
|
||||
<view class="time-label">结束时间</view>
|
||||
<view class="time-value">{{item.end_time_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="schedule-section">
|
||||
<view class="schedule-section-title">详情说明</view>
|
||||
<view class="schedule-description">{{item.description_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,58 +1,48 @@
|
||||
<view class="page" wx:if="{{post}}">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">CLASS FEED</view>
|
||||
<view class="circle-detail-head">
|
||||
<view class="circle-avatar large">{{post.author_initial}}</view>
|
||||
<view class="circle-head-body">
|
||||
<view class="circle-author">{{post.author_name}}</view>
|
||||
<view class="circle-time">{{post.created_at_text}}</view>
|
||||
</view>
|
||||
<view wx:if="{{canDelete}}" class="detail-more" bindtap="openActions">···</view>
|
||||
<view class="hero-title">{{post.title}}</view>
|
||||
<view class="hero-subtitle">{{post.author_name}} · {{post.created_at}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="card">
|
||||
<view class="feed-content">{{post.content || "暂无正文"}}</view>
|
||||
<view wx:if="{{post.image_urls && post.image_urls.length}}" class="feed-images detail-images">
|
||||
<image wx:for="{{post.image_urls}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" data-src="{{img}}" bindtap="previewImage" />
|
||||
</view>
|
||||
<view class="detail-action-bar">
|
||||
<view class="action-chip {{post.has_liked ? 'active' : ''}}" bindtap="toggleLike">
|
||||
<text class="action-icon">赞</text>
|
||||
<text>{{post.has_liked ? "已点赞" : "点赞"}} · {{post.like_count}}</text>
|
||||
</view>
|
||||
<view class="action-chip">
|
||||
<text class="action-icon">评</text>
|
||||
<text>评论 · {{comments.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="circle-detail">
|
||||
<view class="circle-post-title">{{post.title}}</view>
|
||||
<view wx:if="{{post.content}}" class="circle-content">{{post.content}}</view>
|
||||
<view wx:if="{{post.image_urls && post.image_urls.length}}" class="circle-images detail-images">
|
||||
<image wx:for="{{post.image_urls}}" wx:key="*this" src="{{item}}" mode="aspectFill" data-src="{{item}}" bindtap="previewImage" />
|
||||
</view>
|
||||
<view class="circle-actions-row">
|
||||
<view class="circle-action {{post.like_action_class}}" bindtap="toggleLike">{{post.like_action_text}} {{post.like_count}}</view>
|
||||
<view class="circle-action" bindtap="openComment">评论 {{post.comment_count}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<view class="section-title">评论</view>
|
||||
<view class="section-action">{{comments.length}} 条</view>
|
||||
</view>
|
||||
<view wx:for="{{comments}}" wx:key="id" class="comment-card">
|
||||
<view class="list-row">
|
||||
<view class="avatar">{{item.initial}}</view>
|
||||
<view class="row-body">
|
||||
<view class="comment-head">
|
||||
<view class="card-title">{{item.author_name}}</view>
|
||||
<view class="reply-link" data-name="{{item.author_name}}" bindtap="replyComment">回复</view>
|
||||
</view>
|
||||
<view class="comment-content">{{item.content}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!comments.length}}" class="card">
|
||||
<view class="muted">还没有评论</view>
|
||||
<view class="circle-feedback detail-feedback">
|
||||
<view wx:if="{{post.like_count}}" class="circle-likes">赞 {{post.like_count}}</view>
|
||||
<view wx:for="{{post.comments}}" wx:key="id" class="circle-comment" data-comment-id="{{item.id}}" data-name="{{item.author_name}}" data-can-delete="{{item.can_delete}}" bindtap="replyComment">
|
||||
<text class="circle-comment-name">{{item.author_name}}</text>
|
||||
<text>:{{item.content}}</text>
|
||||
</view>
|
||||
<view wx:if="{{!post.like_count && !post.comments.length}}" class="muted">还没有点赞和评论</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{replyTo}}" class="replying-bar">
|
||||
<view>正在回复 {{replyTo}}</view>
|
||||
<view bindtap="cancelReply">取消</view>
|
||||
</view>
|
||||
<view class="comment-box">
|
||||
<input value="{{commentText}}" bindinput="onCommentInput" placeholder="{{inputPlaceholder}}" />
|
||||
<button loading="{{submitting}}" bindtap="submitComment">发送</button>
|
||||
<view class="comment-dock {{keyboardOpen ? 'keyboard-open' : ''}}">
|
||||
<view wx:if="{{replyToName}}" class="replying-bar">
|
||||
<view>回复 {{replyToName}}</view>
|
||||
<view bindtap="cancelReply">取消</view>
|
||||
</view>
|
||||
<view class="comment-box">
|
||||
<input value="{{commentText}}" bindinput="onCommentInput" bindfocus="onCommentFocus" bindblur="onCommentBlur" bindkeyboardheightchange="onKeyboardHeightChange" placeholder="{{inputPlaceholder}}" adjust-position="{{true}}" cursor-spacing="{{0}}" focus="{{inputFocus}}" hold-keyboard="{{true}}" />
|
||||
<button loading="{{submitting}}" bindtap="submitComment">发送</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page" wx:elif="{{!loading}}">
|
||||
<view class="empty">
|
||||
<view class="muted">未找到动态</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -39,14 +39,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{item.deadline_text}}" class="card">
|
||||
<view class="list-row">
|
||||
<view class="row-mark">止</view>
|
||||
<view class="row-body">
|
||||
<view class="card-title">截止时间</view>
|
||||
<view class="muted">{{item.deadline_text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.deadline_text}}" class="vote-deadline-card">
|
||||
<view class="vote-deadline-label">截止时间</view>
|
||||
<view class="vote-deadline-value">{{item.deadline_text}}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-submit-bar">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user